@olwiba/dx 0.0.23 → 0.0.27

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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Compares a real environment against the example that documents it.
3
+ *
4
+ * Pure string comparison, deliberately. The input is a file full of live
5
+ * credentials, so it is never sent anywhere, never written to disk, and never
6
+ * echoed back — the report names keys and says nothing about values.
7
+ *
8
+ * `.env.example` is the schema. Every repository already has one, which is what
9
+ * makes this usable outside the repositories that have a typed schema, and
10
+ * those are exactly the ones where environments have been rotting unnoticed.
11
+ */
12
+ type EnvFinding = {
13
+ kind: 'missing';
14
+ key: string;
15
+ } | {
16
+ kind: 'unknown';
17
+ key: string;
18
+ } | {
19
+ kind: 'renamed';
20
+ key: string;
21
+ looksLike: string;
22
+ } | {
23
+ kind: 'empty';
24
+ key: string;
25
+ } | {
26
+ kind: 'malformed';
27
+ line: number;
28
+ text: string;
29
+ } | {
30
+ kind: 'duplicate';
31
+ key: string;
32
+ };
33
+ interface EnvCheckResult {
34
+ findings: EnvFinding[];
35
+ /** Keys present in both, non-empty. Counted, never listed. */
36
+ okCount: number;
37
+ exampleCount: number;
38
+ actualCount: number;
39
+ }
40
+ interface ParsedEnv {
41
+ /** Key to whether it has a non-empty value. Values themselves are discarded. */
42
+ keys: Map<string, boolean>;
43
+ duplicates: string[];
44
+ malformed: Array<{
45
+ line: number;
46
+ text: string;
47
+ }>;
48
+ }
49
+ /**
50
+ * Reads keys and discards values immediately.
51
+ *
52
+ * The value never leaves this function, so nothing downstream can leak one by
53
+ * accident — including a future change to the report format.
54
+ */
55
+ declare function parseEnv(source: string): ParsedEnv;
56
+ declare function checkEnv({ example, actual,
57
+ /** Keys allowed to be absent, e.g. optional credentials. */
58
+ optional, }: {
59
+ example: string;
60
+ actual: string;
61
+ optional?: string[];
62
+ }): EnvCheckResult;
63
+ /** Formats a report. Contains key names and counts only, never a value. */
64
+ declare function formatEnvReport(result: EnvCheckResult): string;
65
+
66
+ export { type EnvCheckResult, type EnvFinding, checkEnv, formatEnvReport, parseEnv };
@@ -0,0 +1,125 @@
1
+ // src/env-check.ts
2
+ function parseEnv(source) {
3
+ const keys = /* @__PURE__ */ new Map();
4
+ const duplicates = [];
5
+ const malformed = [];
6
+ source.split(/\r?\n/).forEach((raw, index) => {
7
+ const line = raw.trim();
8
+ if (line === "" || line.startsWith("#")) return;
9
+ const withoutExport = line.replace(/^export\s+/, "");
10
+ const eq = withoutExport.indexOf("=");
11
+ if (eq <= 0) {
12
+ malformed.push({ line: index + 1, text: withoutExport.slice(0, 24) });
13
+ return;
14
+ }
15
+ const key = withoutExport.slice(0, eq).trim();
16
+ const value = withoutExport.slice(eq + 1).trim();
17
+ if (keys.has(key)) duplicates.push(key);
18
+ keys.set(key, value !== "" && value !== '""' && value !== "''");
19
+ });
20
+ return { keys, duplicates, malformed };
21
+ }
22
+ function findRenameCandidate(unknownKey, knownKeys) {
23
+ const normalise = (key) => key.replace(/[^A-Z0-9]/gi, "").toUpperCase();
24
+ const target = normalise(unknownKey);
25
+ return knownKeys.find((known) => {
26
+ const candidate = normalise(known);
27
+ if (candidate === target) return true;
28
+ return candidate.length >= 6 && (target.endsWith(candidate) || target.startsWith(candidate));
29
+ });
30
+ }
31
+ function checkEnv({
32
+ example,
33
+ actual,
34
+ /** Keys allowed to be absent, e.g. optional credentials. */
35
+ optional = []
36
+ }) {
37
+ const exampleEnv = parseEnv(example);
38
+ const actualEnv = parseEnv(actual);
39
+ const optionalSet = new Set(optional);
40
+ const exampleKeys = [...exampleEnv.keys.keys()];
41
+ const findings = [];
42
+ let okCount = 0;
43
+ for (const key of exampleKeys) {
44
+ if (!actualEnv.keys.has(key)) {
45
+ if (!optionalSet.has(key)) findings.push({ kind: "missing", key });
46
+ continue;
47
+ }
48
+ const hasValue = actualEnv.keys.get(key) === true;
49
+ const exampleHasValue = exampleEnv.keys.get(key) === true;
50
+ if (!hasValue && exampleHasValue && !optionalSet.has(key)) {
51
+ findings.push({ kind: "empty", key });
52
+ continue;
53
+ }
54
+ okCount += 1;
55
+ }
56
+ for (const key of actualEnv.keys.keys()) {
57
+ if (exampleEnv.keys.has(key)) continue;
58
+ const looksLike = findRenameCandidate(key, exampleKeys);
59
+ findings.push(looksLike ? { kind: "renamed", key, looksLike } : { kind: "unknown", key });
60
+ }
61
+ for (const key of actualEnv.duplicates) findings.push({ kind: "duplicate", key });
62
+ for (const line of actualEnv.malformed) findings.push({ kind: "malformed", ...line });
63
+ return {
64
+ findings,
65
+ okCount,
66
+ exampleCount: exampleEnv.keys.size,
67
+ actualCount: actualEnv.keys.size
68
+ };
69
+ }
70
+ var ORDER = [
71
+ "renamed",
72
+ "missing",
73
+ "empty",
74
+ "malformed",
75
+ "duplicate",
76
+ "unknown"
77
+ ];
78
+ var LABELS = {
79
+ renamed: "Renamed \u2014 set under a name nothing reads",
80
+ missing: "Missing",
81
+ empty: "Present but empty",
82
+ malformed: "Malformed line",
83
+ duplicate: "Set more than once",
84
+ unknown: "Unknown \u2014 not in the example"
85
+ };
86
+ function formatEnvReport(result) {
87
+ const lines = [];
88
+ lines.push(
89
+ ` ${result.actualCount} keys checked against ${result.exampleCount} in the example, ${result.okCount} correct`
90
+ );
91
+ if (result.findings.length === 0) {
92
+ lines.push("\nPASS \u2014 environment matches the example.");
93
+ return lines.join("\n");
94
+ }
95
+ for (const kind of ORDER) {
96
+ const group = result.findings.filter((finding) => finding.kind === kind);
97
+ if (group.length === 0) continue;
98
+ lines.push(`
99
+ ${LABELS[kind]}:`);
100
+ for (const finding of group) {
101
+ if (finding.kind === "renamed") {
102
+ lines.push(` \u2717 ${finding.key} \u2192 did you mean ${finding.looksLike}?`);
103
+ } else if (finding.kind === "malformed") {
104
+ lines.push(` \u2717 line ${finding.line}: ${finding.text}\u2026`);
105
+ } else {
106
+ lines.push(` \u2717 ${finding.key}`);
107
+ }
108
+ }
109
+ }
110
+ const renamed = result.findings.filter((finding) => finding.kind === "renamed").length;
111
+ if (renamed > 0) {
112
+ lines.push(
113
+ `
114
+ ${renamed} key(s) look renamed. The setting they were meant to carry is unset,
115
+ which usually means a default is in force that nobody chose.`
116
+ );
117
+ }
118
+ lines.push(`
119
+ FAIL \u2014 ${result.findings.length} finding(s).`);
120
+ return lines.join("\n");
121
+ }
122
+
123
+ export { checkEnv, formatEnvReport, parseEnv };
124
+ //# sourceMappingURL=env-check.js.map
125
+ //# sourceMappingURL=env-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/env-check.ts"],"names":[],"mappings":"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","file":"env-check.js","sourcesContent":["/**\n * Compares a real environment against the example that documents it.\n *\n * Pure string comparison, deliberately. The input is a file full of live\n * credentials, so it is never sent anywhere, never written to disk, and never\n * echoed back — the report names keys and says nothing about values.\n *\n * `.env.example` is the schema. Every repository already has one, which is what\n * makes this usable outside the repositories that have a typed schema, and\n * those are exactly the ones where environments have been rotting unnoticed.\n */\n\nexport type EnvFinding =\n | { kind: 'missing'; key: string }\n | { kind: 'unknown'; key: string }\n | { kind: 'renamed'; key: string; looksLike: string }\n | { kind: 'empty'; key: string }\n | { kind: 'malformed'; line: number; text: string }\n | { kind: 'duplicate'; key: string };\n\nexport interface EnvCheckResult {\n findings: EnvFinding[];\n /** Keys present in both, non-empty. Counted, never listed. */\n okCount: number;\n exampleCount: number;\n actualCount: number;\n}\n\ninterface ParsedEnv {\n /** Key to whether it has a non-empty value. Values themselves are discarded. */\n keys: Map<string, boolean>;\n duplicates: string[];\n malformed: Array<{ line: number; text: string }>;\n}\n\n/**\n * Reads keys and discards values immediately.\n *\n * The value never leaves this function, so nothing downstream can leak one by\n * accident — including a future change to the report format.\n */\nexport function parseEnv(source: string): ParsedEnv {\n const keys = new Map<string, boolean>();\n const duplicates: string[] = [];\n const malformed: Array<{ line: number; text: string }> = [];\n\n source.split(/\\r?\\n/).forEach((raw, index) => {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) return;\n\n // `export FOO=bar` is valid in a shell-sourced file.\n const withoutExport = line.replace(/^export\\s+/, '');\n const eq = withoutExport.indexOf('=');\n\n if (eq <= 0) {\n // Reported by line number without the text where it could hold a value:\n // a line missing its `=` may still be a pasted secret.\n malformed.push({ line: index + 1, text: withoutExport.slice(0, 24) });\n return;\n }\n\n const key = withoutExport.slice(0, eq).trim();\n const value = withoutExport.slice(eq + 1).trim();\n\n if (keys.has(key)) duplicates.push(key);\n // A later assignment wins in most loaders, so the last one decides.\n keys.set(key, value !== '' && value !== '\"\"' && value !== \"''\");\n });\n\n return { keys, duplicates, malformed };\n}\n\n/**\n * Whether an unrecognised key looks like a renamed version of a known one.\n *\n * This is the finding that matters. A stale key reads as configured — someone\n * scanning the file sees the name they expect in all but a prefix and stops\n * looking — so reporting it as a generic unknown would not tell anyone that the\n * setting it was meant to carry is unset.\n */\nfunction findRenameCandidate(unknownKey: string, knownKeys: string[]): string | undefined {\n const normalise = (key: string) => key.replace(/[^A-Z0-9]/gi, '').toUpperCase();\n const target = normalise(unknownKey);\n\n return knownKeys.find((known) => {\n const candidate = normalise(known);\n if (candidate === target) return true;\n // A prefix or suffix on an otherwise identical name: VITE_FOO against FOO.\n return (\n candidate.length >= 6 &&\n (target.endsWith(candidate) || target.startsWith(candidate))\n );\n });\n}\n\nexport function checkEnv({\n example,\n actual,\n /** Keys allowed to be absent, e.g. optional credentials. */\n optional = [],\n}: {\n example: string;\n actual: string;\n optional?: string[];\n}): EnvCheckResult {\n const exampleEnv = parseEnv(example);\n const actualEnv = parseEnv(actual);\n const optionalSet = new Set(optional);\n\n const exampleKeys = [...exampleEnv.keys.keys()];\n const findings: EnvFinding[] = [];\n let okCount = 0;\n\n for (const key of exampleKeys) {\n if (!actualEnv.keys.has(key)) {\n if (!optionalSet.has(key)) findings.push({ kind: 'missing', key });\n continue;\n }\n\n const hasValue = actualEnv.keys.get(key) === true;\n const exampleHasValue = exampleEnv.keys.get(key) === true;\n\n // Blank where the example shows a value: the example is demonstrating that\n // something belongs there. Blank in both is a deliberate opt-out.\n if (!hasValue && exampleHasValue && !optionalSet.has(key)) {\n findings.push({ kind: 'empty', key });\n continue;\n }\n\n okCount += 1;\n }\n\n for (const key of actualEnv.keys.keys()) {\n if (exampleEnv.keys.has(key)) continue;\n\n const looksLike = findRenameCandidate(key, exampleKeys);\n findings.push(looksLike ? { kind: 'renamed', key, looksLike } : { kind: 'unknown', key });\n }\n\n for (const key of actualEnv.duplicates) findings.push({ kind: 'duplicate', key });\n for (const line of actualEnv.malformed) findings.push({ kind: 'malformed', ...line });\n\n return {\n findings,\n okCount,\n exampleCount: exampleEnv.keys.size,\n actualCount: actualEnv.keys.size,\n };\n}\n\nconst ORDER: EnvFinding['kind'][] = [\n 'renamed',\n 'missing',\n 'empty',\n 'malformed',\n 'duplicate',\n 'unknown',\n];\n\nconst LABELS: Record<EnvFinding['kind'], string> = {\n renamed: 'Renamed — set under a name nothing reads',\n missing: 'Missing',\n empty: 'Present but empty',\n malformed: 'Malformed line',\n duplicate: 'Set more than once',\n unknown: 'Unknown — not in the example',\n};\n\n/** Formats a report. Contains key names and counts only, never a value. */\nexport function formatEnvReport(result: EnvCheckResult): string {\n const lines: string[] = [];\n lines.push(\n ` ${result.actualCount} keys checked against ${result.exampleCount} in the example, ${result.okCount} correct`,\n );\n\n if (result.findings.length === 0) {\n lines.push('\\nPASS — environment matches the example.');\n return lines.join('\\n');\n }\n\n for (const kind of ORDER) {\n const group = result.findings.filter((finding) => finding.kind === kind);\n if (group.length === 0) continue;\n\n lines.push(`\\n${LABELS[kind]}:`);\n for (const finding of group) {\n if (finding.kind === 'renamed') {\n lines.push(` ✗ ${finding.key} → did you mean ${finding.looksLike}?`);\n } else if (finding.kind === 'malformed') {\n lines.push(` ✗ line ${finding.line}: ${finding.text}…`);\n } else {\n lines.push(` ✗ ${finding.key}`);\n }\n }\n }\n\n // Renames are called out because they are the ones that read as configured.\n const renamed = result.findings.filter((finding) => finding.kind === 'renamed').length;\n if (renamed > 0) {\n lines.push(\n `\\n${renamed} key(s) look renamed. The setting they were meant to carry is unset,` +\n '\\nwhich usually means a default is in force that nobody chose.',\n );\n }\n\n lines.push(`\\nFAIL — ${result.findings.length} finding(s).`);\n return lines.join('\\n');\n}\n"]}
@@ -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 * 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 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"]}
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"]}