@olwiba/dx 0.0.22 → 0.0.24

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.
@@ -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[];
@@ -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
- let inner;
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
- const ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\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\"\nimport type sharp from \"sharp\"\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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\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 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"]}
@@ -17,6 +17,20 @@ interface GeneratePreviewsConfig {
17
17
  width?: number;
18
18
  height?: number;
19
19
  };
20
+ /**
21
+ * Cookies to set before navigating, for pages that only render when signed in.
22
+ *
23
+ * Without these, capturing an authenticated page silently photographs its
24
+ * signed-out state — a sign-in form or an empty shell — and the result looks
25
+ * like a working screenshot. Use a fixture account; never a real user's
26
+ * session, since whatever it can see ends up in an image on disk.
27
+ */
28
+ cookies?: Array<{
29
+ name: string;
30
+ value: string;
31
+ domain?: string;
32
+ path?: string;
33
+ }>;
20
34
  }
21
35
  interface ManifestEntry {
22
36
  name: string;
@@ -25,6 +39,10 @@ interface ManifestEntry {
25
39
  width: number;
26
40
  height: number;
27
41
  }
42
+ declare function resolvePreviewOutputPath(outputDir: string, componentName: string, theme: 'light' | 'dark', includeTheme: boolean): {
43
+ filename: string;
44
+ filepath: string;
45
+ };
28
46
  declare function generatePreviews(config: GeneratePreviewsConfig): Promise<void>;
29
47
 
30
- export { type GeneratePreviewsConfig, type ManifestEntry, type PreviewEntry, generatePreviews };
48
+ export { type GeneratePreviewsConfig, type ManifestEntry, type PreviewEntry, generatePreviews, resolvePreviewOutputPath };
@@ -3,6 +3,19 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
 
5
5
  // src/generate-previews.ts
6
+ function resolvePreviewOutputPath(outputDir, componentName, theme, includeTheme) {
7
+ if (!componentName || componentName === "." || componentName === ".." || path.isAbsolute(componentName) || componentName.includes("/") || componentName.includes("\\")) {
8
+ throw new Error(`Invalid preview component name: ${componentName}`);
9
+ }
10
+ const filename = includeTheme ? `${componentName}-${theme}.png` : `${componentName}.png`;
11
+ const resolvedOutputDir = path.resolve(outputDir);
12
+ const filepath = path.resolve(resolvedOutputDir, filename);
13
+ const relativePath = path.relative(resolvedOutputDir, filepath);
14
+ if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
15
+ throw new Error(`Invalid preview component name: ${componentName}`);
16
+ }
17
+ return { filename, filepath };
18
+ }
6
19
  function findChromePath() {
7
20
  const candidates = process.platform === "win32" ? [
8
21
  "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
@@ -31,19 +44,22 @@ async function generatePreviews(config) {
31
44
  manifestPath,
32
45
  executablePath,
33
46
  padding = 0,
34
- viewport
47
+ viewport,
48
+ cookies
35
49
  } = config;
36
50
  const vw = viewport?.width ?? 1280;
37
51
  const vh = viewport?.height ?? 800;
52
+ for (const component of components) {
53
+ for (const theme of themes) {
54
+ resolvePreviewOutputPath(outputDir, component.name, theme, themes.length > 1);
55
+ }
56
+ }
38
57
  try {
39
58
  const res = await fetch(baseUrl);
40
- if (!res.ok) throw new Error();
41
- } catch {
42
- console.error(`
43
- Dev server not running at ${baseUrl}.
44
- Start it first, then re-run iso:generate.
45
- `);
46
- process.exit(1);
59
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`.trim());
60
+ } catch (error) {
61
+ const detail = error instanceof Error ? `: ${error.message}` : "";
62
+ throw new Error(`Preview dev server is unreachable at ${baseUrl}${detail}`);
47
63
  }
48
64
  const chromePath = executablePath ?? findChromePath();
49
65
  console.log(`Using browser: ${chromePath}`);
@@ -54,6 +70,7 @@ Start it first, then re-run iso:generate.
54
70
  args: ["--no-sandbox", "--disable-setuid-sandbox"]
55
71
  });
56
72
  const manifest = [];
73
+ const captureFailures = [];
57
74
  try {
58
75
  for (const component of components) {
59
76
  const urlPath = component.urlPath ?? `/docs/components/${component.name}`;
@@ -82,6 +99,16 @@ Start it first, then re-run iso:generate.
82
99
  })(${JSON.stringify(theme)})`
83
100
  );
84
101
  await page.setViewport({ width: vw, height: vh });
102
+ if (cookies && cookies.length > 0) {
103
+ const { hostname } = new URL(baseUrl);
104
+ await page.setCookie(
105
+ ...cookies.map((cookie) => ({
106
+ path: "/",
107
+ domain: hostname,
108
+ ...cookie
109
+ }))
110
+ );
111
+ }
85
112
  await page.goto(url, { waitUntil: "networkidle0" });
86
113
  await page.evaluate(
87
114
  `((scheme) => {
@@ -97,8 +124,12 @@ Start it first, then re-run iso:generate.
97
124
  theme
98
125
  );
99
126
  await new Promise((r) => setTimeout(r, 300));
100
- const filename = themes.length > 1 ? `${component.name}-${theme}.png` : `${component.name}.png`;
101
- const filepath = path.join(outputDir, filename);
127
+ const { filename, filepath } = resolvePreviewOutputPath(
128
+ outputDir,
129
+ component.name,
130
+ theme,
131
+ themes.length > 1
132
+ );
102
133
  if (captureSelector === null) {
103
134
  await page.screenshot({ path: filepath, clip: { x: 0, y: 0, width: vw, height: vh } });
104
135
  manifest.push({ name: component.name, theme, file: filename, width: vw, height: vh });
@@ -140,6 +171,10 @@ Start it first, then re-run iso:generate.
140
171
  console.log(`\u2713 ${filename}`);
141
172
  } catch (err) {
142
173
  console.error(`\u2717 ${component.name} (${theme}):`, err);
174
+ const detail = err instanceof Error ? err.message : String(err);
175
+ captureFailures.push(
176
+ new Error(`Failed to capture ${component.name} (${theme}): ${detail}`)
177
+ );
143
178
  } finally {
144
179
  await page.close();
145
180
  }
@@ -148,6 +183,12 @@ Start it first, then re-run iso:generate.
148
183
  } finally {
149
184
  await browser.close();
150
185
  }
186
+ if (captureFailures.length > 0) {
187
+ throw new AggregateError(
188
+ captureFailures,
189
+ `Failed to generate ${captureFailures.length} preview capture(s)`
190
+ );
191
+ }
151
192
  const resolvedManifestPath = manifestPath ?? path.join(outputDir, "manifest.json");
152
193
  fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });
153
194
  fs.writeFileSync(resolvedManifestPath, JSON.stringify(manifest, null, 2));
@@ -155,6 +196,6 @@ Start it first, then re-run iso:generate.
155
196
  Generated ${manifest.length} previews \u2192 ${outputDir}`);
156
197
  }
157
198
 
158
- export { generatePreviews };
199
+ export { generatePreviews, resolvePreviewOutputPath };
159
200
  //# sourceMappingURL=generate-previews.js.map
160
201
  //# sourceMappingURL=generate-previews.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/generate-previews.ts"],"names":[],"mappings":"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","file":"generate-previews.js","sourcesContent":["import puppeteer from 'puppeteer-core';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nexport interface PreviewEntry {\n name: string;\n urlPath?: string;\n /** Override config.selector for this entry (e.g. null for full-page captures). */\n selector?: string | null;\n}\n\nexport interface GeneratePreviewsConfig {\n baseUrl: string;\n outputDir: string;\n components: PreviewEntry[];\n selector?: string | null;\n themes?: ('light' | 'dark')[];\n manifestPath?: string;\n executablePath?: string;\n padding?: number;\n viewport?: { width?: number; height?: number };\n}\n\nexport interface ManifestEntry {\n name: string;\n theme: 'light' | 'dark';\n file: string;\n width: number;\n height: number;\n}\n\nfunction findChromePath(): string {\n const candidates =\n process.platform === 'win32'\n ? [\n 'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n 'C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n `${process.env.LOCALAPPDATA}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n 'C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe',\n `${process.env.ProgramFiles}\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe`,\n ]\n : process.platform === 'darwin'\n ? [\n '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',\n ]\n : ['/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium'];\n\n for (const p of candidates) {\n if (p && fs.existsSync(p)) return p;\n }\n throw new Error(\n 'No Chrome/Edge executable found. Pass executablePath in config or install Chrome.',\n );\n}\n\nexport async function generatePreviews(config: GeneratePreviewsConfig): Promise<void> {\n const {\n baseUrl,\n outputDir,\n components,\n selector = '[data-slot=\"component-preview-canvas\"]',\n themes = ['light', 'dark'],\n manifestPath,\n executablePath,\n padding = 0,\n viewport,\n } = config;\n\n const vw = viewport?.width ?? 1280;\n const vh = viewport?.height ?? 800;\n\n try {\n const res = await fetch(baseUrl);\n if (!res.ok) throw new Error();\n } catch {\n console.error(`\\nDev server not running at ${baseUrl}.\\nStart it first, then re-run iso:generate.\\n`);\n process.exit(1);\n }\n\n const chromePath = executablePath ?? findChromePath();\n console.log(`Using browser: ${chromePath}`);\n\n fs.mkdirSync(outputDir, { recursive: true });\n\n const browser = await puppeteer.launch({\n executablePath: chromePath,\n headless: true,\n args: ['--no-sandbox', '--disable-setuid-sandbox'],\n });\n\n const manifest: ManifestEntry[] = [];\n\n try {\n for (const component of components) {\n const urlPath = component.urlPath ?? `/docs/components/${component.name}`;\n const url = `${baseUrl}${urlPath}`;\n const captureSelector =\n component.selector !== undefined ? component.selector : selector;\n\n for (const theme of themes) {\n const page = await browser.newPage();\n try {\n // next-themes (Fumadocs RootProvider) defaults to system — match OS unless we override early.\n await page.emulateMediaFeatures([\n { name: 'prefers-color-scheme', value: theme },\n ]);\n\n // Run before page scripts (next-themes / ModeSwitcher) so light captures stay light.\n await page.evaluateOnNewDocument(\n `((scheme) => {\n const isDark = scheme === 'dark';\n const apply = () => {\n const root = document.documentElement;\n root.classList.toggle('dark', isDark);\n root.style.colorScheme = scheme;\n };\n try { localStorage.setItem('theme', scheme); } catch {}\n apply();\n new MutationObserver(apply).observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class'],\n });\n })(${JSON.stringify(theme)})`,\n );\n\n await page.setViewport({ width: vw, height: vh });\n await page.goto(url, { waitUntil: 'networkidle0' });\n\n await page.evaluate(\n `((scheme) => {\n const isDark = scheme === 'dark';\n document.documentElement.classList.toggle('dark', isDark);\n document.documentElement.style.colorScheme = scheme;\n try { localStorage.setItem('theme', scheme); } catch {}\n })(${JSON.stringify(theme)})`,\n );\n\n await page.waitForFunction(\n `(scheme) => document.documentElement.classList.contains('dark') === (scheme === 'dark')`,\n { timeout: 5000 },\n theme,\n );\n await new Promise((r) => setTimeout(r, 300));\n\n const filename =\n themes.length > 1 ? `${component.name}-${theme}.png` : `${component.name}.png`;\n const filepath = path.join(outputDir, filename);\n\n if (captureSelector === null) {\n await page.screenshot({ path: filepath, clip: { x: 0, y: 0, width: vw, height: vh } });\n manifest.push({ name: component.name, theme, file: filename, width: vw, height: vh });\n } else {\n const el = await page.waitForSelector(captureSelector, { timeout: 10_000 });\n if (!el) throw new Error(`Selector not found: ${captureSelector}`);\n\n await page.evaluate(\n `document.querySelector(${JSON.stringify(captureSelector)}).style.minHeight = '0'`,\n );\n await new Promise((r) => setTimeout(r, 100));\n\n const box = await el.boundingBox();\n if (!box) throw new Error(`Could not get bounding box for: ${captureSelector}`);\n\n if (padding > 0) {\n const clip = {\n x: Math.max(0, box.x - padding),\n y: Math.max(0, box.y - padding),\n width: box.width + padding * 2,\n height: box.height + padding * 2,\n };\n await page.screenshot({ path: filepath, clip });\n manifest.push({\n name: component.name,\n theme,\n file: filename,\n width: Math.round(clip.width),\n height: Math.round(clip.height),\n });\n } else {\n await el.screenshot({ path: filepath });\n manifest.push({\n name: component.name,\n theme,\n file: filename,\n width: Math.round(box.width),\n height: Math.round(box.height),\n });\n }\n }\n\n console.log(`✓ ${filename}`);\n } catch (err) {\n console.error(`✗ ${component.name} (${theme}):`, err);\n } finally {\n await page.close();\n }\n }\n }\n } finally {\n await browser.close();\n }\n\n const resolvedManifestPath = manifestPath ?? path.join(outputDir, 'manifest.json');\n fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });\n fs.writeFileSync(resolvedManifestPath, JSON.stringify(manifest, null, 2));\n\n console.log(`\\nGenerated ${manifest.length} previews → ${outputDir}`);\n}\n"]}
1
+ {"version":3,"sources":["../src/generate-previews.ts"],"names":[],"mappings":"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","file":"generate-previews.js","sourcesContent":["import puppeteer from 'puppeteer-core';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nexport interface PreviewEntry {\n name: string;\n urlPath?: string;\n /** Override config.selector for this entry (e.g. null for full-page captures). */\n selector?: string | null;\n}\n\nexport interface GeneratePreviewsConfig {\n baseUrl: string;\n outputDir: string;\n components: PreviewEntry[];\n selector?: string | null;\n themes?: ('light' | 'dark')[];\n manifestPath?: string;\n executablePath?: string;\n padding?: number;\n viewport?: { width?: number; height?: number };\n /**\n * Cookies to set before navigating, for pages that only render when signed in.\n *\n * Without these, capturing an authenticated page silently photographs its\n * signed-out state — a sign-in form or an empty shell — and the result looks\n * like a working screenshot. Use a fixture account; never a real user's\n * session, since whatever it can see ends up in an image on disk.\n */\n cookies?: Array<{ name: string; value: string; domain?: string; path?: string }>;\n}\n\nexport interface ManifestEntry {\n name: string;\n theme: 'light' | 'dark';\n file: string;\n width: number;\n height: number;\n}\n\nexport function resolvePreviewOutputPath(\n outputDir: string,\n componentName: string,\n theme: 'light' | 'dark',\n includeTheme: boolean,\n): { filename: string; filepath: string } {\n if (\n !componentName ||\n componentName === '.' ||\n componentName === '..' ||\n path.isAbsolute(componentName) ||\n componentName.includes('/') ||\n componentName.includes('\\\\')\n ) {\n throw new Error(`Invalid preview component name: ${componentName}`);\n }\n\n const filename = includeTheme\n ? `${componentName}-${theme}.png`\n : `${componentName}.png`;\n const resolvedOutputDir = path.resolve(outputDir);\n const filepath = path.resolve(resolvedOutputDir, filename);\n const relativePath = path.relative(resolvedOutputDir, filepath);\n\n if (\n relativePath === '..' ||\n relativePath.startsWith(`..${path.sep}`) ||\n path.isAbsolute(relativePath)\n ) {\n throw new Error(`Invalid preview component name: ${componentName}`);\n }\n\n return { filename, filepath };\n}\n\nfunction findChromePath(): string {\n const candidates =\n process.platform === 'win32'\n ? [\n 'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n 'C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n `${process.env.LOCALAPPDATA}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n 'C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe',\n `${process.env.ProgramFiles}\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe`,\n ]\n : process.platform === 'darwin'\n ? [\n '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',\n ]\n : ['/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium'];\n\n for (const p of candidates) {\n if (p && fs.existsSync(p)) return p;\n }\n throw new Error(\n 'No Chrome/Edge executable found. Pass executablePath in config or install Chrome.',\n );\n}\n\nexport async function generatePreviews(config: GeneratePreviewsConfig): Promise<void> {\n const {\n baseUrl,\n outputDir,\n components,\n selector = '[data-slot=\"component-preview-canvas\"]',\n themes = ['light', 'dark'],\n manifestPath,\n executablePath,\n padding = 0,\n viewport,\n cookies,\n } = config;\n\n const vw = viewport?.width ?? 1280;\n const vh = viewport?.height ?? 800;\n\n for (const component of components) {\n for (const theme of themes) {\n resolvePreviewOutputPath(outputDir, component.name, theme, themes.length > 1);\n }\n }\n\n try {\n const res = await fetch(baseUrl);\n if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`.trim());\n } catch (error) {\n const detail = error instanceof Error ? `: ${error.message}` : '';\n throw new Error(`Preview dev server is unreachable at ${baseUrl}${detail}`);\n }\n\n const chromePath = executablePath ?? findChromePath();\n console.log(`Using browser: ${chromePath}`);\n\n fs.mkdirSync(outputDir, { recursive: true });\n\n const browser = await puppeteer.launch({\n executablePath: chromePath,\n headless: true,\n args: ['--no-sandbox', '--disable-setuid-sandbox'],\n });\n\n const manifest: ManifestEntry[] = [];\n const captureFailures: Error[] = [];\n\n try {\n for (const component of components) {\n const urlPath = component.urlPath ?? `/docs/components/${component.name}`;\n const url = `${baseUrl}${urlPath}`;\n const captureSelector =\n component.selector !== undefined ? component.selector : selector;\n\n for (const theme of themes) {\n const page = await browser.newPage();\n try {\n // next-themes (Fumadocs RootProvider) defaults to system — match OS unless we override early.\n await page.emulateMediaFeatures([\n { name: 'prefers-color-scheme', value: theme },\n ]);\n\n // Run before page scripts (next-themes / ModeSwitcher) so light captures stay light.\n await page.evaluateOnNewDocument(\n `((scheme) => {\n const isDark = scheme === 'dark';\n const apply = () => {\n const root = document.documentElement;\n root.classList.toggle('dark', isDark);\n root.style.colorScheme = scheme;\n };\n try { localStorage.setItem('theme', scheme); } catch {}\n apply();\n new MutationObserver(apply).observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class'],\n });\n })(${JSON.stringify(theme)})`,\n );\n\n await page.setViewport({ width: vw, height: vh });\n\n if (cookies && cookies.length > 0) {\n const { hostname } = new URL(baseUrl);\n await page.setCookie(\n ...cookies.map((cookie) => ({\n path: '/',\n domain: hostname,\n ...cookie,\n })),\n );\n }\n\n await page.goto(url, { waitUntil: 'networkidle0' });\n\n await page.evaluate(\n `((scheme) => {\n const isDark = scheme === 'dark';\n document.documentElement.classList.toggle('dark', isDark);\n document.documentElement.style.colorScheme = scheme;\n try { localStorage.setItem('theme', scheme); } catch {}\n })(${JSON.stringify(theme)})`,\n );\n\n await page.waitForFunction(\n `(scheme) => document.documentElement.classList.contains('dark') === (scheme === 'dark')`,\n { timeout: 5000 },\n theme,\n );\n await new Promise((r) => setTimeout(r, 300));\n\n const { filename, filepath } = resolvePreviewOutputPath(\n outputDir,\n component.name,\n theme,\n themes.length > 1,\n );\n\n if (captureSelector === null) {\n await page.screenshot({ path: filepath, clip: { x: 0, y: 0, width: vw, height: vh } });\n manifest.push({ name: component.name, theme, file: filename, width: vw, height: vh });\n } else {\n const el = await page.waitForSelector(captureSelector, { timeout: 10_000 });\n if (!el) throw new Error(`Selector not found: ${captureSelector}`);\n\n await page.evaluate(\n `document.querySelector(${JSON.stringify(captureSelector)}).style.minHeight = '0'`,\n );\n await new Promise((r) => setTimeout(r, 100));\n\n const box = await el.boundingBox();\n if (!box) throw new Error(`Could not get bounding box for: ${captureSelector}`);\n\n if (padding > 0) {\n const clip = {\n x: Math.max(0, box.x - padding),\n y: Math.max(0, box.y - padding),\n width: box.width + padding * 2,\n height: box.height + padding * 2,\n };\n await page.screenshot({ path: filepath, clip });\n manifest.push({\n name: component.name,\n theme,\n file: filename,\n width: Math.round(clip.width),\n height: Math.round(clip.height),\n });\n } else {\n await el.screenshot({ path: filepath });\n manifest.push({\n name: component.name,\n theme,\n file: filename,\n width: Math.round(box.width),\n height: Math.round(box.height),\n });\n }\n }\n\n console.log(`✓ ${filename}`);\n } catch (err) {\n console.error(`✗ ${component.name} (${theme}):`, err);\n const detail = err instanceof Error ? err.message : String(err);\n captureFailures.push(\n new Error(`Failed to capture ${component.name} (${theme}): ${detail}`),\n );\n } finally {\n await page.close();\n }\n }\n }\n } finally {\n await browser.close();\n }\n\n if (captureFailures.length > 0) {\n throw new AggregateError(\n captureFailures,\n `Failed to generate ${captureFailures.length} preview capture(s)`,\n );\n }\n\n const resolvedManifestPath = manifestPath ?? path.join(outputDir, 'manifest.json');\n fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });\n fs.writeFileSync(resolvedManifestPath, JSON.stringify(manifest, null, 2));\n\n console.log(`\\nGenerated ${manifest.length} previews → ${outputDir}`);\n}\n"]}
package/dist/skills.d.ts CHANGED
@@ -12,5 +12,7 @@ interface SkillsManifest {
12
12
  version: string;
13
13
  skills: ManifestSkill[];
14
14
  }
15
+ /** Returns true when a manifest slug is a safe, single directory name. */
16
+ declare function isSafeSkillSlug(slug: string): boolean;
15
17
 
16
- export type { ManifestSkill, SkillsManifest };
18
+ export { type ManifestSkill, type SkillsManifest, isSafeSkillSlug };
package/dist/skills.js CHANGED
@@ -1,3 +1,8 @@
1
+ // src/skills.ts
2
+ function isSafeSkillSlug(slug) {
3
+ return slug !== "." && slug !== ".." && /^[A-Za-z0-9._-]+$/.test(slug);
4
+ }
1
5
 
6
+ export { isSafeSkillSlug };
2
7
  //# sourceMappingURL=skills.js.map
3
8
  //# sourceMappingURL=skills.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"skills.js"}
1
+ {"version":3,"sources":["../src/skills.ts"],"names":[],"mappings":";AAiBO,SAAS,gBAAgB,IAAA,EAAuB;AACrD,EAAA,OAAO,SAAS,GAAA,IAAO,IAAA,KAAS,IAAA,IAAQ,mBAAA,CAAoB,KAAK,IAAI,CAAA;AACvE","file":"skills.js","sourcesContent":["export interface ManifestSkill {\n slug: string\n name: string\n description: string\n category?: string\n providers?: string[]\n examples?: string[]\n tip?: string | null\n contentUrl: string\n}\n\nexport interface SkillsManifest {\n version: string\n skills: ManifestSkill[]\n}\n\n/** Returns true when a manifest slug is a safe, single directory name. */\nexport function isSafeSkillSlug(slug: string): boolean {\n return slug !== \".\" && slug !== \"..\" && /^[A-Za-z0-9._-]+$/.test(slug)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olwiba/dx",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -47,14 +47,15 @@
47
47
  "scripts": {
48
48
  "build": "tsup",
49
49
  "dev": "tsup --watch",
50
+ "test": "bun test",
50
51
  "tsc": "tsc --noEmit"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/node": "^22.5.4",
54
- "eslint": "^9.0.0",
55
+ "eslint": "^9.39.5",
55
56
  "lucide-static": "^0.488.0",
56
- "puppeteer-core": "^24.0.0",
57
- "sharp": "^0.33.0",
57
+ "puppeteer-core": "^25.9.0",
58
+ "sharp": "^0.35.0",
58
59
  "tsup": "^8.4.0",
59
60
  "typescript": "^5.7.2"
60
61
  },
@@ -64,8 +65,8 @@
64
65
  "eslint": ">=9.0.0",
65
66
  "eslint-plugin-react-hooks": ">=5.0.0",
66
67
  "lucide-static": ">=0.400.0",
67
- "puppeteer-core": ">=22.0.0",
68
- "sharp": ">=0.32.0"
68
+ "puppeteer-core": ">=25.9.0",
69
+ "sharp": ">=0.35.0"
69
70
  },
70
71
  "peerDependenciesMeta": {
71
72
  "@typescript-eslint/eslint-plugin": {
@@ -95,6 +96,9 @@
95
96
  "bugs": {
96
97
  "url": "https://github.com/Olwiba/olwibaDX/issues"
97
98
  },
99
+ "engines": {
100
+ "node": ">=22.12.0"
101
+ },
98
102
  "keywords": [
99
103
  "olwiba",
100
104
  "dx",