fontfetch 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/cli.js +927 -0
- package/dist/cli.js.map +1 -0
- package/package.json +70 -0
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pull.ts","../src/utils.ts","../src/parse.ts","../src/emit.ts","../src/license-data.ts","../src/license.ts","../src/headless.ts","../src/emitters/util.ts","../src/emitters/next.ts","../src/emitters/tailwind.ts","../src/emitters/vite.ts","../src/emitters/types.ts","../src/emitters/index.ts","../src/provenance.ts","../src/cli.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { extractStylesheetLinks, extractInlineStyles, extractFontFaces } from './parse.js';\nimport { buildFontsCss, buildFontsJson, buildReadme, buildLicenseReview } from './emit.js';\nimport { classifyFaces, summarize } from './license.js';\nimport { fetchText, fetchBuffer, siteSlug, safeFilename, log } from './utils.js';\nimport type { CssSource, OrphanFile, PullOptions, PullResult } from './types.js';\nimport { FONT_EXT_RE } from './utils.js';\nimport { abs } from './utils.js';\nimport { fetchHeadless } from './headless.js';\nimport { EMITTERS } from './emitters/index.js';\nimport { bucketForUrl } from './provenance.js';\n\nexport async function pull({\n url,\n baseDir,\n headless = false,\n emit = [],\n force = false,\n}: PullOptions): Promise<PullResult> {\n const host = siteSlug(url);\n const outDir = path.join(path.resolve(baseDir), host);\n const filesDir = path.join(outDir, 'files');\n await fs.mkdir(filesDir, { recursive: true });\n\n log.info(`→ Fetching page: ${url}`);\n const html = await fetchText(url);\n\n const cssLinks = extractStylesheetLinks(html, url);\n const inlineCss = extractInlineStyles(html);\n log.info(` ${cssLinks.length} external stylesheet(s), ${inlineCss.length} inline <style> block(s)`);\n\n const cssSources: CssSource[] = inlineCss.map((t) => ({ text: t, base: url }));\n for (const link of cssLinks) {\n try {\n log.info(`→ Fetching CSS: ${link}`);\n cssSources.push({ text: await fetchText(link, { Referer: url }), base: link });\n } catch (e) {\n log.warn(` ! Failed: ${(e as Error).message}`);\n }\n }\n\n let networkFontUrls: string[] = [];\n if (headless) {\n log.info('→ Running headless mode (Playwright)...');\n try {\n const result = await fetchHeadless(url);\n cssSources.push(...result.cssSources);\n networkFontUrls = result.networkFontUrls;\n log.info(` + ${result.cssSources.length} stylesheet block(s) from headless`);\n if (networkFontUrls.length > 0) {\n log.info(` + ${networkFontUrls.length} font URL(s) observed in network`);\n }\n } catch (e) {\n log.warn(` ! Headless mode failed: ${(e as Error).message}`);\n log.warn(' Continuing with static results.');\n }\n }\n\n const allFaces = cssSources.flatMap(({ text, base }) => extractFontFaces(text, base));\n // Dedupe across static + headless sources (same rule can appear in both).\n const seen = new Set<string>();\n const faces = allFaces.filter((f) => {\n const sig = `${f.family}|${f.weight}|${f.style}|${f.sources.map((s) => s.url).sort().join(',')}`;\n if (seen.has(sig)) return false;\n seen.add(sig);\n return true;\n });\n\n // Also catch <link rel=\"preload\" as=\"font\">\n const preloadRe = /<link\\b[^>]*rel=[\"']?preload[\"']?[^>]*as=[\"']?font[\"']?[^>]*>/gi;\n const extraUrls: string[] = [];\n for (const m of html.matchAll(preloadRe)) {\n const href = /href=[\"']([^\"']+)[\"']/i.exec(m[0])?.[1];\n if (href) {\n const u = abs(href, url);\n if (u && FONT_EXT_RE.test(u)) extraUrls.push(u);\n }\n }\n\n const pageHost = new URL(url).hostname;\n\n // Filenames returned by claim() include the bucket subdir, e.g. \"google/Inter-Regular.woff2\".\n const urlToLocal = new Map<string, string>();\n const usedNames = new Set<string>();\n const claim = (u: string): string => {\n if (urlToLocal.has(u)) return urlToLocal.get(u)!;\n const bucket = bucketForUrl(u, pageHost);\n let name = safeFilename(u);\n const candidate = `${bucket}/${name}`;\n if (usedNames.has(candidate)) {\n const h = new URL(u).hostname.replace(/[^a-z0-9]/gi, '_');\n name = `${h}__${name}`;\n }\n const final = `${bucket}/${name}`;\n usedNames.add(final);\n urlToLocal.set(u, final);\n return final;\n };\n\n for (const f of faces) {\n for (const s of f.sources) s.localFile = claim(s.url);\n }\n for (const u of extraUrls) claim(u);\n\n // Orphans: font URLs observed in the network log that aren't referenced by any\n // parsed @font-face source. Usually from cross-origin stylesheets whose cssRules\n // we can't read (e.g. Typekit). We download them but can't emit @font-face for\n // them — no family/weight/style metadata.\n const faceUrls = new Set<string>();\n for (const f of faces) for (const s of f.sources) faceUrls.add(s.url);\n const orphans: OrphanFile[] = [];\n for (const u of networkFontUrls) {\n if (faceUrls.has(u)) continue;\n if (urlToLocal.has(u)) continue;\n const file = claim(u);\n orphans.push({ url: u, file });\n }\n if (orphans.length > 0) {\n log.info(`→ ${orphans.length} orphan file(s) (cross-origin, no @font-face metadata available)`);\n }\n\n log.info(`→ Found ${faces.length} @font-face declaration(s), ${urlToLocal.size} unique file(s)`);\n if (urlToLocal.size === 0) {\n log.info(' (Nothing to download. Site may load fonts via JS, or block automated requests. Try --headless.)');\n return { outDir, faces, orphans: [], downloaded: 0, total: 0 };\n }\n\n const classified = classifyFaces(faces);\n const licenseSummary = summarize(classified);\n log.info(\n `→ License review: ${licenseSummary.open} open / ${licenseSummary.commercial} commercial / ${licenseSummary.unknown} unknown`,\n );\n\n // Fail-fast: every face came from a commercial foundry CDN. Most users will\n // not have a license for these; download them anyway only if --force is set.\n if (licenseSummary.allCommercial && !force) {\n await fs.writeFile(\n path.join(outDir, 'LICENSE_REVIEW.md'),\n buildLicenseReview(host, classified, licenseSummary),\n );\n log.warn('');\n log.warn(`✗ All ${licenseSummary.commercial} detected font(s) are served from known commercial CDNs.`);\n log.warn(' Downloading and shipping these without a license violates foundry EULAs.');\n log.warn(` Wrote ${path.join(outDir, 'LICENSE_REVIEW.md')} with the breakdown.`);\n log.warn(' To download anyway (e.g. for a local mockup you have rights to), re-run with --force.');\n log.warn('');\n return { outDir, faces, orphans: [], downloaded: 0, total: urlToLocal.size };\n }\n\n let downloaded = 0;\n const createdBuckets = new Set<string>();\n for (const [fontUrl, name] of urlToLocal) {\n const dest = path.join(filesDir, name);\n const bucketDir = path.dirname(dest);\n if (!createdBuckets.has(bucketDir)) {\n await fs.mkdir(bucketDir, { recursive: true });\n createdBuckets.add(bucketDir);\n }\n try {\n const buf = await fetchBuffer(fontUrl, { Referer: url });\n await fs.writeFile(dest, buf);\n log.info(` ✓ ${name} (${buf.length.toLocaleString()} bytes)`);\n downloaded++;\n } catch (e) {\n log.warn(` ✗ ${name} — ${(e as Error).message}`);\n }\n }\n\n await fs.writeFile(path.join(outDir, 'fonts.css'), buildFontsCss(faces));\n await fs.writeFile(path.join(outDir, 'fonts.json'), buildFontsJson(faces, orphans));\n await fs.writeFile(path.join(outDir, 'README.md'), buildReadme(host, faces, downloaded, orphans));\n await fs.writeFile(\n path.join(outDir, 'LICENSE_REVIEW.md'),\n buildLicenseReview(host, classified, licenseSummary),\n );\n\n for (const target of emit) {\n const emitter = EMITTERS[target];\n if (!emitter) continue;\n const output = emitter(faces, { siteSlug: host, filesDir: 'files' });\n if (!output) continue;\n await fs.writeFile(path.join(outDir, output.filename), output.content);\n log.info(` + emitted ${output.filename} (--emit ${target})`);\n }\n\n return { outDir, faces, orphans, downloaded, total: urlToLocal.size };\n}\n","import path from 'node:path';\n\nexport const FONT_EXT_RE = /\\.(woff2|woff|ttf|otf|eot)(\\?[^\"')\\s]*)?$/i;\n\nexport const UA =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';\n\nexport async function fetchText(url: string, headers: Record<string, string> = {}): Promise<string> {\n const res = await fetch(url, { headers: { 'User-Agent': UA, ...headers } });\n if (!res.ok) throw new Error(`GET ${url} → ${res.status}`);\n return await res.text();\n}\n\nexport async function fetchBuffer(url: string, headers: Record<string, string> = {}): Promise<Buffer> {\n const res = await fetch(url, { headers: { 'User-Agent': UA, ...headers } });\n if (!res.ok) throw new Error(`GET ${url} → ${res.status}`);\n return Buffer.from(await res.arrayBuffer());\n}\n\nexport function abs(u: string, base: string): string | null {\n try {\n return new URL(u, base).toString();\n } catch {\n return null;\n }\n}\n\nexport function siteSlug(url: string): string {\n return new URL(url).hostname.replace(/^www\\./, '').replace(/[^a-zA-Z0-9.-]/g, '_');\n}\n\nexport function safeFilename(url: string): string {\n const u = new URL(url);\n const base = path.basename(u.pathname) || 'font';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_');\n}\n\nexport const log = {\n info: (msg: string) => console.log(msg),\n warn: (msg: string) => console.warn(msg),\n err: (msg: string) => console.error(msg),\n};\n","import type { FontFace, FontSource } from './types.js';\nimport { FONT_EXT_RE, abs } from './utils.js';\n\nexport function extractStylesheetLinks(html: string, baseUrl: string): string[] {\n const out: string[] = [];\n const linkRe = /<link\\b[^>]*rel=[\"']?stylesheet[\"']?[^>]*>/gi;\n for (const m of html.matchAll(linkRe)) {\n const href = /href=[\"']([^\"']+)[\"']/i.exec(m[0])?.[1];\n if (href) {\n const u = abs(href, baseUrl);\n if (u) out.push(u);\n }\n }\n return out;\n}\n\nexport function extractInlineStyles(html: string): string[] {\n const out: string[] = [];\n const styleRe = /<style\\b[^>]*>([\\s\\S]*?)<\\/style>/gi;\n for (const m of html.matchAll(styleRe)) out.push(m[1]);\n return out;\n}\n\nfunction parseFontFace(body: string, baseUrl: string): FontFace | null {\n const getProp = (prop: string): string | null => {\n const m = new RegExp(`${prop}\\\\s*:\\\\s*([^;]+)`, 'i').exec(body);\n return m ? m[1].trim() : null;\n };\n\n const family = (getProp('font-family') || '').replace(/^['\"]|['\"]$/g, '');\n const weight = getProp('font-weight') || '400';\n const style = getProp('font-style') || 'normal';\n const display = getProp('font-display');\n const unicodeRange = getProp('unicode-range');\n\n const srcRaw = getProp('src') || '';\n const sources: FontSource[] = [];\n const srcRe =\n /url\\(\\s*['\"]?([^'\")]+)['\"]?\\s*\\)(?:\\s*format\\(\\s*['\"]?([^'\")]+)['\"]?\\s*\\))?/gi;\n for (const m of srcRaw.matchAll(srcRe)) {\n const raw = m[1];\n if (raw.startsWith('data:')) continue;\n if (!FONT_EXT_RE.test(raw)) continue;\n const absUrl = abs(raw, baseUrl);\n if (!absUrl) continue;\n sources.push({ url: absUrl, format: m[2] || null });\n }\n\n if (!family || sources.length === 0) return null;\n return { family, weight, style, display, unicodeRange, sources };\n}\n\nexport function extractFontFaces(css: string, baseUrl: string): FontFace[] {\n const out: FontFace[] = [];\n const faceRe = /@font-face\\s*\\{([^}]*)\\}/gi;\n for (const m of css.matchAll(faceRe)) {\n const parsed = parseFontFace(m[1], baseUrl);\n if (parsed) out.push(parsed);\n }\n return out;\n}\n","import type { FontFace, OrphanFile } from './types.js';\nimport type { ClassifiedFace, LicenseSummary } from './license.js';\n\nexport function buildFontsCss(faces: FontFace[]): string {\n const lines: string[] = [\n '/* Auto-generated by fontfetch */',\n '/* Drop this folder into your project and link this CSS file. */',\n '',\n ];\n for (const f of faces) {\n lines.push('@font-face {');\n lines.push(` font-family: '${f.family}';`);\n lines.push(` font-style: ${f.style};`);\n lines.push(` font-weight: ${f.weight};`);\n if (f.display) lines.push(` font-display: ${f.display};`);\n const srcParts = f.sources.map((s) => {\n const local = `url('./files/${s.localFile}')`;\n return s.format ? `${local} format('${s.format}')` : local;\n });\n lines.push(` src: ${srcParts.join(', ')};`);\n if (f.unicodeRange) lines.push(` unicode-range: ${f.unicodeRange};`);\n lines.push('}');\n lines.push('');\n }\n return lines.join('\\n');\n}\n\nexport function buildFontsJson(faces: FontFace[], orphans: OrphanFile[] = []): string {\n return JSON.stringify(\n {\n faces: faces.map((f) => ({\n family: f.family,\n weight: f.weight,\n style: f.style,\n display: f.display,\n unicodeRange: f.unicodeRange,\n files: f.sources.map((s) => ({ file: `files/${s.localFile}`, format: s.format })),\n })),\n orphan_files: orphans.map((o) => ({ file: `files/${o.file}`, url: o.url })),\n },\n null,\n 2,\n );\n}\n\nexport function buildReadme(\n host: string,\n faces: FontFace[],\n totalFiles: number,\n orphans: OrphanFile[] = [],\n): string {\n const byFamily = new Map<string, FontFace[]>();\n for (const f of faces) {\n if (!byFamily.has(f.family)) byFamily.set(f.family, []);\n byFamily.get(f.family)!.push(f);\n }\n\n const lines: string[] = [\n `# Fonts from ${host}`,\n '',\n `Downloaded ${totalFiles} font file(s) across ${faces.length} @font-face declaration(s).`,\n '',\n '## Usage',\n '',\n '1. Copy this whole folder into your project (e.g. `public/fonts/` or `src/assets/fonts/`).',\n '2. Include `fonts.css` in your app (import it, or `<link rel=\"stylesheet\" href=\"...\">`).',\n '3. Reference the families below by `font-family` in your CSS.',\n '',\n '## Families',\n '',\n ];\n\n for (const [family, list] of byFamily) {\n lines.push(`### ${family}`);\n lines.push('');\n const variants = new Map<string, number>();\n for (const f of list) {\n const key = `${f.weight} / ${f.style}`;\n variants.set(key, (variants.get(key) || 0) + 1);\n }\n for (const [key, n] of variants) {\n lines.push(`- ${key}${n > 1 ? ` _(${n} subset files)_` : ''}`);\n }\n lines.push('');\n }\n\n if (orphans.length > 0) {\n lines.push('## Orphan files');\n lines.push('');\n lines.push(`These ${orphans.length} font file(s) were observed loading in the browser but came from a cross-origin stylesheet whose @font-face rules couldn't be read directly (common for Adobe Typekit and similar services). The files are downloaded into \\`files/\\` but **not referenced in \\`fonts.css\\`** — there's no family/weight/style metadata to construct a rule from.`);\n lines.push('');\n lines.push('To use them, inspect the live site\\'s DevTools to find the matching @font-face declarations, then add them to your CSS pointing at the local files.');\n lines.push('');\n for (const o of orphans) {\n lines.push(`- \\`files/${o.file}\\` ← ${o.url}`);\n }\n lines.push('');\n }\n\n lines.push('## Notes');\n lines.push('');\n lines.push('- Multiple files per weight/style usually means the source split the font by `unicode-range` (Latin, Latin-Ext, Cyrillic, etc.). The browser only loads the subsets it needs — keep them all.');\n lines.push('- For local design exploration. Verify licensing before shipping to production.');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Emits LICENSE_REVIEW.md — a heuristic summary of which fonts on the site\n * are open/self-hostable, which are commercial (don't ship without a license),\n * and which couldn't be confidently classified.\n *\n * Not legal advice. The classifier is conservative on purpose; \"unknown\"\n * cases are the user's responsibility to verify.\n */\nexport function buildLicenseReview(\n host: string,\n classified: ClassifiedFace[],\n summary: LicenseSummary,\n): string {\n const lines: string[] = [\n `# License review for ${host}`,\n '',\n '> Heuristic-only. Not legal advice. Verify before shipping.',\n '',\n '## Summary',\n '',\n `- ✅ **${summary.open} open** — safe to self-host`,\n `- ⚠️ **${summary.commercial} commercial** — do not ship without a license`,\n `- ❓ **${summary.unknown} unknown** — manual review needed`,\n '',\n ];\n\n const sections: Array<{ status: 'open' | 'commercial' | 'unknown'; title: string; note?: string }> = [\n { status: 'open', title: '## ✅ Open / self-hostable' },\n {\n status: 'commercial',\n title: '## ⚠️ Commercial — do not ship without a license',\n note: 'These came from a commercial foundry CDN. Bundling them into a production app without a paid license violates the foundry EULA.',\n },\n {\n status: 'unknown',\n title: '## ❓ Unknown — manual review',\n note: \"Couldn't match against known CDN signatures or the known-open family list. Check the foundry, search Google Fonts for a free alternative, or inspect the site's CSS to confirm.\",\n },\n ];\n\n for (const section of sections) {\n const items = classified.filter((c) => c.classification.status === section.status);\n if (items.length === 0) continue;\n lines.push(section.title);\n lines.push('');\n if (section.note) {\n lines.push(`_${section.note}_`);\n lines.push('');\n }\n const byFamily = new Map<string, typeof items>();\n for (const it of items) {\n const list = byFamily.get(it.face.family) ?? [];\n list.push(it);\n byFamily.set(it.face.family, list);\n }\n for (const [family, list] of byFamily) {\n lines.push(`### ${family}`);\n lines.push('');\n lines.push(`- ${list[0].classification.reason}`);\n const fileList = list.flatMap((it) =>\n it.face.sources\n .map((s) => s.localFile)\n .filter((f): f is string => Boolean(f))\n .map((f) => `files/${f} (${it.face.weight}/${it.face.style})`),\n );\n const unique = [...new Set(fileList)];\n if (unique.length > 0) {\n lines.push('- Files:');\n for (const f of unique) lines.push(` - \\`${f}\\``);\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n","/**\n * Heuristic signatures for license classification. Not legal advice.\n *\n * - OPEN_HOSTS: URL substring → high-confidence open/self-hostable signal\n * - COMMERCIAL_HOSTS: URL substring → high-confidence commercial-foundry signal\n * - KNOWN_OPEN_FAMILIES: family-name match → open (covers self-hosted Google Fonts catalog families)\n *\n * Keep additions conservative — a false \"open\" classification could mislead a\n * user into shipping a paid font. False \"commercial\" is the safer failure\n * mode (worst case: a slightly noisier LICENSE_REVIEW.md).\n */\n\nexport interface HostSignature {\n /** Substring matched against the font URL */\n host: string;\n /** Human-readable label shown in LICENSE_REVIEW.md */\n label: string;\n}\n\nexport const OPEN_HOSTS: HostSignature[] = [\n { host: 'fonts.gstatic.com', label: 'Google Fonts CDN' },\n { host: 'fonts.googleapis.com', label: 'Google Fonts API' },\n { host: 'cdn.jsdelivr.net/npm/@fontsource/', label: 'Fontsource (mostly OFL)' },\n { host: 'rsms.me/inter', label: 'Inter — OFL (by Rasmus Andersson)' },\n { host: 'cdn.jsdelivr.net/gh/google/fonts', label: 'Google Fonts GitHub mirror' },\n];\n\nexport const COMMERCIAL_HOSTS: HostSignature[] = [\n { host: 'use.typekit.net', label: 'Adobe Fonts (Typekit)' },\n { host: 'fonts.adobe.com', label: 'Adobe Fonts' },\n { host: 'p.typekit.net', label: 'Adobe Fonts (Typekit)' },\n { host: 'fast.fonts.net', label: 'Monotype (fonts.com)' },\n { host: 'cloud.typenetwork.com', label: 'Type Network' },\n { host: 'cloud.typography.com', label: 'Hoefler & Co (Cloud.typography)' },\n { host: 'use.fontawesome.com', label: 'Font Awesome (commercial tiers)' },\n { host: 'fontstand.com', label: 'Fontstand' },\n];\n\n/**\n * Common families released under SIL OFL / Apache (or otherwise free for\n * self-hosting). Used when the URL host isn't enough — many sites\n * self-host OFL fonts on their own CDN. Family-name match is case-insensitive\n * and trimmed.\n *\n * Source: top families from the Google Fonts catalog plus a handful of\n * well-known free fonts.\n */\nexport const KNOWN_OPEN_FAMILIES: string[] = [\n // Top sans\n 'Inter',\n 'Inter Display',\n 'Inter Tight',\n 'Roboto',\n 'Roboto Condensed',\n 'Roboto Flex',\n 'Open Sans',\n 'Lato',\n 'Montserrat',\n 'Poppins',\n 'Oswald',\n 'Raleway',\n 'Nunito',\n 'Nunito Sans',\n 'Ubuntu',\n 'PT Sans',\n 'Source Sans Pro',\n 'Source Sans 3',\n 'Mulish',\n 'Karla',\n 'Work Sans',\n 'Quicksand',\n 'DM Sans',\n 'Manrope',\n 'Outfit',\n 'Space Grotesk',\n 'Plus Jakarta Sans',\n 'Public Sans',\n 'Hind',\n 'Cabin',\n 'Atkinson Hyperlegible',\n // Geist (Vercel + Basement Studio, OFL)\n 'Geist',\n 'Geist Mono',\n 'Geist Sans',\n // Mono\n 'JetBrains Mono',\n 'Fira Code',\n 'Fira Mono',\n 'Source Code Pro',\n 'IBM Plex Mono',\n 'Space Mono',\n 'DM Mono',\n 'Roboto Mono',\n // Serif\n 'Playfair Display',\n 'Merriweather',\n 'Noto Serif',\n 'Noto Sans',\n 'EB Garamond',\n 'Lora',\n 'Bitter',\n 'Cardo',\n 'Cormorant',\n 'Crimson Text',\n 'PT Serif',\n 'DM Serif Display',\n 'DM Serif Text',\n // IBM Plex (Apache 2.0)\n 'IBM Plex Sans',\n 'IBM Plex Serif',\n // Fira (OFL)\n 'Fira Sans',\n // Display / handwritten\n 'Lobster',\n 'Pacifico',\n 'Sacramento',\n 'Dancing Script',\n 'Caveat',\n 'Indie Flower',\n 'Patrick Hand',\n 'Comic Neue',\n 'Architects Daughter',\n];\n","import type { FontFace } from './types.js';\nimport { OPEN_HOSTS, COMMERCIAL_HOSTS, KNOWN_OPEN_FAMILIES } from './license-data.js';\n\nexport type LicenseStatus = 'open' | 'commercial' | 'unknown';\n\nexport interface LicenseClassification {\n status: LicenseStatus;\n reason: string;\n}\n\nconst NORMALIZED_OPEN_FAMILIES = new Set(KNOWN_OPEN_FAMILIES.map((f) => f.trim().toLowerCase()));\n\n/**\n * Classify a single face. URL signatures win over family-name signatures —\n * if a font is served from a commercial CDN we mark it commercial even if\n * the family name matches an open family (someone selling Inter on Typekit\n * is still selling Inter).\n */\nexport function classifyFace(face: FontFace): LicenseClassification {\n for (const src of face.sources) {\n for (const sig of COMMERCIAL_HOSTS) {\n if (src.url.includes(sig.host)) {\n return { status: 'commercial', reason: `Served from ${sig.label}` };\n }\n }\n }\n\n for (const src of face.sources) {\n for (const sig of OPEN_HOSTS) {\n if (src.url.includes(sig.host)) {\n return { status: 'open', reason: `Served from ${sig.label}` };\n }\n }\n }\n\n const fam = (face.family || '').trim().toLowerCase();\n if (NORMALIZED_OPEN_FAMILIES.has(fam)) {\n return { status: 'open', reason: `'${face.family}' is on the SIL OFL / Google Fonts catalog` };\n }\n\n return { status: 'unknown', reason: 'No matching CDN or known-family signature' };\n}\n\nexport interface ClassifiedFace {\n face: FontFace;\n classification: LicenseClassification;\n}\n\nexport function classifyFaces(faces: FontFace[]): ClassifiedFace[] {\n return faces.map((face) => ({ face, classification: classifyFace(face) }));\n}\n\nexport interface LicenseSummary {\n open: number;\n commercial: number;\n unknown: number;\n total: number;\n /** True if at least one face was classified AND all classifications are commercial. */\n allCommercial: boolean;\n}\n\nexport function summarize(classified: ClassifiedFace[]): LicenseSummary {\n let open = 0;\n let commercial = 0;\n let unknown = 0;\n for (const c of classified) {\n if (c.classification.status === 'open') open++;\n else if (c.classification.status === 'commercial') commercial++;\n else unknown++;\n }\n const total = classified.length;\n return {\n open,\n commercial,\n unknown,\n total,\n allCommercial: total > 0 && commercial === total,\n };\n}\n","import type { CssSource } from './types.js';\nimport { UA } from './utils.js';\n\nexport interface HeadlessResult {\n cssSources: CssSource[];\n networkFontUrls: string[];\n}\n\nconst INSTALL_HINT = `\nPlaywright is required for --headless mode. To install:\n\n npm install playwright\n npx playwright install chromium\n\nOr skip --headless to use the static parser.\n`.trim();\n\nexport async function fetchHeadless(url: string, timeoutMs = 30000): Promise<HeadlessResult> {\n let chromium: typeof import('playwright').chromium;\n try {\n ({ chromium } = await import('playwright'));\n } catch {\n throw new Error(INSTALL_HINT);\n }\n\n let browser;\n try {\n browser = await chromium.launch({ headless: true });\n } catch (e) {\n const msg = (e as Error).message;\n if (/Executable doesn't exist|browserType\\.launch/i.test(msg)) {\n throw new Error(\n `Playwright is installed but Chromium isn't. Run:\\n npx playwright install chromium\\n\\n(${msg})`,\n );\n }\n throw e;\n }\n\n try {\n const context = await browser.newContext({ userAgent: UA });\n const page = await context.newPage();\n\n const networkFontUrls = new Set<string>();\n page.on('response', (response) => {\n const u = response.url();\n const ct = (response.headers()['content-type'] || '').toLowerCase();\n const isFontByExt = /\\.(woff2|woff|ttf|otf|eot)(\\?|$)/i.test(u);\n const isFontByType = ct.startsWith('font/') || ct === 'application/font-woff' || ct === 'application/font-woff2';\n if ((isFontByExt || isFontByType) && response.status() < 400) {\n networkFontUrls.add(u);\n }\n });\n\n await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });\n\n // Wait for the FontFaceSet to settle (resolves once all in-flight fonts are loaded).\n await page.evaluate(() => (document as Document & { fonts: FontFaceSet }).fonts.ready);\n\n // Dump every @font-face rule from every same-origin stylesheet. Cross-origin sheets\n // throw on .cssRules; the network listener covers those URLs as a backstop.\n const cssText = await page.evaluate(() => {\n const out: string[] = [];\n for (const sheet of Array.from(document.styleSheets)) {\n try {\n const rules = sheet.cssRules;\n if (!rules) continue;\n for (const rule of Array.from(rules)) {\n if (rule.type === CSSRule.FONT_FACE_RULE) {\n out.push(rule.cssText);\n }\n }\n } catch {\n // cross-origin stylesheet — skip\n }\n }\n return out.join('\\n');\n });\n\n return {\n cssSources: cssText ? [{ text: cssText, base: url }] : [],\n networkFontUrls: [...networkFontUrls],\n };\n } finally {\n await browser.close();\n }\n}\n","import type { FontFace } from '../types.js';\n\n/**\n * Strip non-alphanumeric chars and camelCase the family for use as a JS identifier.\n * Preserves interior capitals so \"JetBrains Mono\" → \"jetBrainsMono\", not \"jetbrainsMono\".\n */\nexport function familyToIdent(family: string): string {\n const cleaned = family.replace(/[^A-Za-z0-9 ]+/g, ' ').trim();\n if (!cleaned) return 'font';\n const parts = cleaned.split(/\\s+/);\n const first = parts[0][0].toLowerCase() + parts[0].slice(1);\n const rest = parts.slice(1).map((p) => p[0].toUpperCase() + p.slice(1));\n return [first, ...rest].join('');\n}\n\n/** kebab-case the family for use in CSS variables / file-suggesting */\nexport function familyToKebab(family: string): string {\n return family\n .replace(/[^A-Za-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .toLowerCase();\n}\n\n/**\n * Best-guess Tailwind bucket for a family. Order matters: 'mono' wins over\n * 'sans'/'serif', and 'sans' is checked before 'serif' so that \"Sans Serif\"\n * and \"SansSerif\" route to sans rather than getting trapped by the \"serif\"\n * substring match.\n */\nexport function tailwindBucket(family: string): 'sans' | 'serif' | 'mono' {\n const f = family.toLowerCase();\n if (/(mono|code|console|courier|consolas)/.test(f)) return 'mono';\n if (/sans/.test(f)) return 'sans';\n if (/(serif|garamond|caslon|baskerville|times|georgia|plantijn|tiempos)/.test(f)) return 'serif';\n return 'sans';\n}\n\n/** Group faces by family, preserving first-seen order. */\nexport function groupByFamily(faces: FontFace[]): Map<string, FontFace[]> {\n const out = new Map<string, FontFace[]>();\n for (const f of faces) {\n const existing = out.get(f.family);\n if (existing) existing.push(f);\n else out.set(f.family, [f]);\n }\n return out;\n}\n\n/** Pick a single representative file per (weight, style) — prefers woff2. */\nexport function pickPrimaryFile(face: FontFace): { file: string; format: string | null } | null {\n if (face.sources.length === 0) return null;\n const woff2 = face.sources.find((s) => s.format === 'woff2');\n const chosen = woff2 ?? face.sources[0];\n if (!chosen.localFile) return null;\n return { file: chosen.localFile, format: chosen.format };\n}\n","import type { Emitter } from './types.js';\nimport { familyToIdent, familyToKebab, groupByFamily, pickPrimaryFile } from './util.js';\n\n/**\n * Emits a `next.fonts.ts` file using `next/font/local` — one `localFont` call\n * per family, with weights/styles bundled into the `src` array, plus a CSS\n * variable per family ready to spread into a root `<html>` element.\n */\nexport const nextEmitter: Emitter = (faces, ctx) => {\n const byFamily = groupByFamily(faces);\n if (byFamily.size === 0) return null;\n\n const lines: string[] = [\n '/* Auto-generated by fontfetch (`--emit next`). */',\n '/* Copy this folder into your Next.js project and import these exports. */',\n '/* Example: `<html className={`${interFont.variable} ${geistMono.variable}`}>` */',\n '',\n \"import localFont from 'next/font/local';\",\n '',\n ];\n\n for (const [family, list] of byFamily) {\n const ident = familyToIdent(family);\n const cssVar = `--font-${familyToKebab(family)}`;\n const sources = list\n .map((f) => pickPrimaryFile(f))\n .filter((s): s is { file: string; format: string | null } => s !== null);\n\n if (sources.length === 0) continue;\n\n lines.push(`export const ${ident} = localFont({`);\n lines.push(' src: [');\n list.forEach((f, i) => {\n const src = pickPrimaryFile(f);\n if (!src) return;\n const weight = f.weight || '400';\n const style = f.style || 'normal';\n lines.push(\n ` { path: './${ctx.filesDir}/${src.file}', weight: '${weight}', style: '${style}' },` +\n (i === list.length - 1 ? '' : ''),\n );\n });\n lines.push(' ],');\n lines.push(` variable: '${cssVar}',`);\n lines.push(\" display: 'swap',\");\n lines.push('});');\n lines.push('');\n }\n\n return { filename: 'next.fonts.ts', content: lines.join('\\n') };\n};\n","import type { Emitter } from './types.js';\nimport { familyToIdent, familyToKebab, groupByFamily, tailwindBucket } from './util.js';\n\n/**\n * Emits a `tailwind.fonts.ts` snippet that maps each family into a bucket\n * (`sans` / `serif` / `mono`) using a heuristic, plus an explicit alias per\n * family for direct use (`font-<family-slug>`). Pairs with `--emit next`,\n * which provides the CSS variables this config references.\n */\nexport const tailwindEmitter: Emitter = (faces, ctx) => {\n void ctx;\n const byFamily = groupByFamily(faces);\n if (byFamily.size === 0) return null;\n\n const buckets: Record<'sans' | 'serif' | 'mono', string[]> = { sans: [], serif: [], mono: [] };\n const aliases: { alias: string; family: string; cssVar: string }[] = [];\n\n for (const family of byFamily.keys()) {\n const cssVar = `--font-${familyToKebab(family)}`;\n const bucket = tailwindBucket(family);\n buckets[bucket].push(`var(${cssVar})`);\n aliases.push({ alias: familyToIdent(family), family, cssVar });\n }\n\n const fallback: Record<'sans' | 'serif' | 'mono', string> = {\n sans: \"'system-ui', 'sans-serif'\",\n serif: \"'Georgia', 'serif'\",\n mono: \"'ui-monospace', 'monospace'\",\n };\n\n const lines: string[] = [\n '/* Auto-generated by fontfetch (`--emit tailwind`). */',\n '/* Merge `fonts` into your tailwind.config.ts -> theme.extend.fontFamily. */',\n '/* Pair with the Next.js setup from `--emit next` for CSS variables. */',\n '',\n \"import type { Config } from 'tailwindcss';\",\n '',\n \"type FontFamilyConfig = NonNullable<NonNullable<Config['theme']>['extend']>['fontFamily'];\",\n '',\n 'export const fonts: FontFamilyConfig = {',\n ];\n\n for (const bucket of ['sans', 'serif', 'mono'] as const) {\n const vars = buckets[bucket];\n if (vars.length === 0) continue;\n const stack = [...vars.map((v) => `'${v}'`), fallback[bucket]].join(', ');\n lines.push(` ${bucket}: [${stack}],`);\n }\n\n for (const { alias, family, cssVar } of aliases) {\n lines.push(` '${alias}': ['var(${cssVar})', /* ${family} */],`);\n }\n\n lines.push('};');\n lines.push('');\n lines.push('export default fonts;');\n\n return { filename: 'tailwind.fonts.ts', content: lines.join('\\n') };\n};\n","import type { Emitter } from './types.js';\nimport { groupByFamily } from './util.js';\n\n/**\n * Vite (and any plain-bundler) integration. The generated `fonts.css` already\n * does 95% of the work — this emitter produces a `vite.fonts.md` with\n * copy-pasteable import lines and family-name references so users don't have\n * to re-derive them from the manifest.\n */\nexport const viteEmitter: Emitter = (faces, ctx) => {\n const byFamily = groupByFamily(faces);\n if (byFamily.size === 0) return null;\n\n const lines: string[] = [\n '# Vite integration',\n '',\n 'The generated `fonts.css` is already a drop-in `@font-face` stylesheet — no Vite-specific plugin needed.',\n '',\n '## Steps',\n '',\n '1. Copy this folder into your project, e.g. `src/assets/fonts/<site>/`',\n '2. Import the stylesheet from your entry file:',\n '',\n ' ```ts',\n ' // src/main.ts (or src/main.tsx, etc.)',\n ` import './assets/fonts/<site>/fonts.css';`,\n ' ```',\n '',\n '3. Reference the families in your CSS or Tailwind config:',\n '',\n ];\n\n for (const family of byFamily.keys()) {\n lines.push(` - \\`font-family: '${family}';\\``);\n }\n lines.push('');\n\n lines.push('## Notes');\n lines.push('');\n lines.push('- Vite emits the font files as-is during build; they go through `vite-plugin-static-copy` or your asset pipeline automatically when imported via `url()` in the CSS.');\n lines.push(`- File paths in \\`fonts.css\\` are relative (\\`./${ctx.filesDir}/...\\`), so the import works from anywhere you place this folder.`);\n lines.push('');\n\n return { filename: 'vite.fonts.md', content: lines.join('\\n') };\n};\n","import type { FontFace } from '../types.js';\n\nexport interface EmitContext {\n /** Hostname-derived slug, e.g. 'stripe-com' (used for variable names) */\n siteSlug: string;\n /** Path (relative to the output dir) where font files live. Default: 'files' */\n filesDir: string;\n}\n\nexport interface EmitOutput {\n /** File written into the per-site output dir (e.g. 'next.fonts.ts') */\n filename: string;\n /** Full file contents */\n content: string;\n}\n\nexport type Emitter = (faces: FontFace[], context: EmitContext) => EmitOutput | null;\n\nexport type EmitTarget = 'css' | 'next' | 'tailwind' | 'vite';\n\nexport const EMIT_TARGETS: EmitTarget[] = ['css', 'next', 'tailwind', 'vite'];\n\nexport function isEmitTarget(s: string): s is EmitTarget {\n return (EMIT_TARGETS as string[]).includes(s);\n}\n","import type { EmitTarget, Emitter } from './types.js';\nimport { nextEmitter } from './next.js';\nimport { tailwindEmitter } from './tailwind.js';\nimport { viteEmitter } from './vite.js';\n\nexport { isEmitTarget, EMIT_TARGETS } from './types.js';\nexport type { EmitTarget, Emitter, EmitContext, EmitOutput } from './types.js';\n\n/** css is the default (handled by emit.ts/buildFontsCss); non-css emitters live here. */\nexport const EMITTERS: Record<Exclude<EmitTarget, 'css'>, Emitter> = {\n next: nextEmitter,\n tailwind: tailwindEmitter,\n vite: viteEmitter,\n};\n","/**\n * Source-bucket classification for font URLs. Used to organise the per-site\n * `files/` directory into subfolders so the free-vs-licensed split is visible\n * at a glance.\n *\n * Decision order (first match wins):\n * 1. google — Google Fonts CDN family (gstatic, googleapis, mirror)\n * 2. adobe-typekit — Adobe Fonts / Typekit\n * 3. commercial — Other commercial foundry CDNs (Monotype, Hoefler, etc.)\n * 4. open-cdn — Other recognised open CDNs (Fontsource, rsms.me, etc.)\n * 5. self-hosted — Same origin as the page, OR anything we couldn't classify\n */\n\nexport type Bucket = 'google' | 'adobe-typekit' | 'commercial' | 'open-cdn' | 'self-hosted';\n\nexport const BUCKETS: Bucket[] = ['google', 'adobe-typekit', 'commercial', 'open-cdn', 'self-hosted'];\n\ninterface BucketRule {\n bucket: Bucket;\n patterns: string[];\n}\n\nconst RULES: BucketRule[] = [\n {\n bucket: 'google',\n patterns: ['fonts.gstatic.com', 'fonts.googleapis.com', 'cdn.jsdelivr.net/gh/google/fonts'],\n },\n {\n bucket: 'adobe-typekit',\n patterns: ['use.typekit.net', 'p.typekit.net', 'fonts.adobe.com'],\n },\n {\n bucket: 'commercial',\n patterns: [\n 'fast.fonts.net',\n 'cloud.typography.com',\n 'cloud.typenetwork.com',\n 'use.fontawesome.com',\n 'fontstand.com',\n ],\n },\n {\n bucket: 'open-cdn',\n patterns: ['cdn.jsdelivr.net/npm/@fontsource/', 'rsms.me'],\n },\n];\n\n/** Strip leading \"www.\" so `www.example.com` and `example.com` compare equal. */\nfunction strip(host: string): string {\n return host.replace(/^www\\./i, '').toLowerCase();\n}\n\n/** Same-origin if hosts are equal, or one is a subdomain of the other. */\nexport function sameOrigin(urlHost: string, pageHost: string): boolean {\n const a = strip(urlHost);\n const b = strip(pageHost);\n if (a === b) return true;\n return a.endsWith('.' + b) || b.endsWith('.' + a);\n}\n\n/**\n * Classify a single font URL into a bucket given the page that referenced it.\n * `pageHost` is the hostname of the URL the user passed to fontfetch\n * (used to detect \"same-origin = self-hosted\").\n */\nexport function bucketForUrl(url: string, pageHost: string): Bucket {\n for (const rule of RULES) {\n for (const pattern of rule.patterns) {\n if (url.includes(pattern)) return rule.bucket;\n }\n }\n try {\n const u = new URL(url);\n if (sameOrigin(u.hostname, pageHost)) return 'self-hosted';\n } catch {\n // unparseable URL — fall through to self-hosted\n }\n return 'self-hosted';\n}\n","#!/usr/bin/env node\nimport { pull } from './pull.js';\nimport { log } from './utils.js';\nimport { isEmitTarget, EMIT_TARGETS, type EmitTarget } from './emitters/index.js';\n\nconst VERSION = '0.6.0';\n\nfunction printHelp(): void {\n log.info(`fontfetch ${VERSION}\n\nUsage:\n fontfetch <url> [outDir] [flags]\n\nArguments:\n <url> Page to download fonts from (https://example.com)\n [outDir] Directory to write output into (default: ./downloaded-fonts)\n\nFlags:\n --headless Use Playwright to also capture JS-loaded fonts (SPAs,\n late-injected @font-face rules). Requires:\n npm install playwright\n npx playwright install chromium\n --emit <targets> Comma-separated framework targets to emit alongside the\n default fonts.css. One or more of: ${EMIT_TARGETS.join(', ')}\n Examples:\n --emit next Next.js next/font/local file\n --emit tailwind Tailwind fontFamily snippet\n --emit next,tailwind Both (pair for CSS variables)\n --emit vite Vite integration guide\n --force Download even if every detected font is served from a\n known commercial-foundry CDN. Default behaviour is to\n abort early and emit only LICENSE_REVIEW.md.\n -h, --help Show this help\n -v, --version Print version\n\nExamples:\n fontfetch https://stripe.com\n fontfetch https://stripe.com ./fonts\n fontfetch https://linear.app --headless\n fontfetch https://vercel.com --emit next,tailwind\n npx fontfetch https://stripe.com\n\nOutput (per site):\n <outDir>/<hostname>/\n files/ Raw font files (woff2/woff/ttf/otf/eot)\n fonts.css Ready-to-use @font-face block with local URLs\n fonts.json Manifest grouped by family/weight/style\n README.md Human-readable summary\n\nFor local design exploration. You're responsible for licensing the fonts you use.\n`);\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n\n if (args.length === 0 || args.includes('-h') || args.includes('--help')) {\n printHelp();\n process.exit(args.length === 0 ? 1 : 0);\n }\n\n if (args.includes('-v') || args.includes('--version')) {\n log.info(VERSION);\n process.exit(0);\n }\n\n const headless = args.includes('--headless');\n const force = args.includes('--force');\n\n // --emit <targets> may be either separated by space or '=' (e.g. --emit=next,tailwind)\n let emit: Exclude<EmitTarget, 'css'>[] = [];\n const emitIdx = args.findIndex((a: string) => a === '--emit' || a.startsWith('--emit='));\n if (emitIdx !== -1) {\n const raw =\n args[emitIdx] === '--emit'\n ? args[emitIdx + 1]\n : args[emitIdx].slice('--emit='.length);\n if (!raw) {\n log.err(`--emit requires a value. One or more of: ${EMIT_TARGETS.join(', ')}`);\n process.exit(1);\n }\n const requested = raw.split(',').map((s: string) => s.trim()).filter(Boolean);\n for (const r of requested) {\n if (!isEmitTarget(r)) {\n log.err(`Unknown --emit target: '${r}'. Valid: ${EMIT_TARGETS.join(', ')}`);\n process.exit(1);\n }\n if (r !== 'css') emit.push(r);\n }\n }\n\n const reservedFlags = new Set(['--headless', '--emit', '--force']);\n const positional = args.filter((a: string, i: number) => {\n if (a.startsWith('--')) return false;\n // Skip the value that follows a space-separated --emit\n if (i > 0 && args[i - 1] === '--emit') return false;\n if (reservedFlags.has(a)) return false;\n return true;\n });\n const [url, outDir = './downloaded-fonts'] = positional;\n\n if (!url) {\n log.err('Missing <url> argument. Run with --help for usage.');\n process.exit(1);\n }\n\n try {\n new URL(url);\n } catch {\n log.err(`Invalid URL: ${url}`);\n process.exit(1);\n }\n\n const result = await pull({ url, baseDir: outDir, headless, emit, force });\n\n log.info('');\n if (result.total === 0) {\n log.info('No fonts found.');\n process.exit(0);\n }\n log.info(`Done. ${result.downloaded}/${result.total} files saved to ${result.outDir}`);\n}\n\nmain().catch((e: unknown) => {\n log.err(String(e));\n process.exit(1);\n});\n"],"mappings":";;;AAAA,OAAO,QAAQ;AACf,OAAOA,WAAU;;;ACDjB,OAAO,UAAU;AAEV,IAAM,cAAc;AAEpB,IAAM,KACX;AAEF,eAAsB,UAAU,KAAa,UAAkC,CAAC,GAAoB;AAClG,QAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,EAAE,cAAc,IAAI,GAAG,QAAQ,EAAE,CAAC;AAC1E,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,OAAO,GAAG,WAAM,IAAI,MAAM,EAAE;AACzD,SAAO,MAAM,IAAI,KAAK;AACxB;AAEA,eAAsB,YAAY,KAAa,UAAkC,CAAC,GAAoB;AACpG,QAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,EAAE,cAAc,IAAI,GAAG,QAAQ,EAAE,CAAC;AAC1E,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,OAAO,GAAG,WAAM,IAAI,MAAM,EAAE;AACzD,SAAO,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC;AAC5C;AAEO,SAAS,IAAI,GAAW,MAA6B;AAC1D,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,IAAI,EAAE,SAAS;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,SAAS,KAAqB;AAC5C,SAAO,IAAI,IAAI,GAAG,EAAE,SAAS,QAAQ,UAAU,EAAE,EAAE,QAAQ,mBAAmB,GAAG;AACnF;AAEO,SAAS,aAAa,KAAqB;AAChD,QAAM,IAAI,IAAI,IAAI,GAAG;AACrB,QAAM,OAAO,KAAK,SAAS,EAAE,QAAQ,KAAK;AAC1C,SAAO,KAAK,QAAQ,oBAAoB,GAAG;AAC7C;AAEO,IAAM,MAAM;AAAA,EACjB,MAAM,CAAC,QAAgB,QAAQ,IAAI,GAAG;AAAA,EACtC,MAAM,CAAC,QAAgB,QAAQ,KAAK,GAAG;AAAA,EACvC,KAAK,CAAC,QAAgB,QAAQ,MAAM,GAAG;AACzC;;;ACtCO,SAAS,uBAAuB,MAAc,SAA2B;AAC9E,QAAM,MAAgB,CAAC;AACvB,QAAM,SAAS;AACf,aAAW,KAAK,KAAK,SAAS,MAAM,GAAG;AACrC,UAAM,OAAO,yBAAyB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC;AACpD,QAAI,MAAM;AACR,YAAM,IAAI,IAAI,MAAM,OAAO;AAC3B,UAAI,EAAG,KAAI,KAAK,CAAC;AAAA,IACnB;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,oBAAoB,MAAwB;AAC1D,QAAM,MAAgB,CAAC;AACvB,QAAM,UAAU;AAChB,aAAW,KAAK,KAAK,SAAS,OAAO,EAAG,KAAI,KAAK,EAAE,CAAC,CAAC;AACrD,SAAO;AACT;AAEA,SAAS,cAAc,MAAc,SAAkC;AACrE,QAAM,UAAU,CAAC,SAAgC;AAC/C,UAAM,IAAI,IAAI,OAAO,GAAG,IAAI,oBAAoB,GAAG,EAAE,KAAK,IAAI;AAC9D,WAAO,IAAI,EAAE,CAAC,EAAE,KAAK,IAAI;AAAA,EAC3B;AAEA,QAAM,UAAU,QAAQ,aAAa,KAAK,IAAI,QAAQ,gBAAgB,EAAE;AACxE,QAAM,SAAS,QAAQ,aAAa,KAAK;AACzC,QAAM,QAAQ,QAAQ,YAAY,KAAK;AACvC,QAAM,UAAU,QAAQ,cAAc;AACtC,QAAM,eAAe,QAAQ,eAAe;AAE5C,QAAM,SAAS,QAAQ,KAAK,KAAK;AACjC,QAAM,UAAwB,CAAC;AAC/B,QAAM,QACJ;AACF,aAAW,KAAK,OAAO,SAAS,KAAK,GAAG;AACtC,UAAM,MAAM,EAAE,CAAC;AACf,QAAI,IAAI,WAAW,OAAO,EAAG;AAC7B,QAAI,CAAC,YAAY,KAAK,GAAG,EAAG;AAC5B,UAAM,SAAS,IAAI,KAAK,OAAO;AAC/B,QAAI,CAAC,OAAQ;AACb,YAAQ,KAAK,EAAE,KAAK,QAAQ,QAAQ,EAAE,CAAC,KAAK,KAAK,CAAC;AAAA,EACpD;AAEA,MAAI,CAAC,UAAU,QAAQ,WAAW,EAAG,QAAO;AAC5C,SAAO,EAAE,QAAQ,QAAQ,OAAO,SAAS,cAAc,QAAQ;AACjE;AAEO,SAAS,iBAAiB,KAAa,SAA6B;AACzE,QAAM,MAAkB,CAAC;AACzB,QAAM,SAAS;AACf,aAAW,KAAK,IAAI,SAAS,MAAM,GAAG;AACpC,UAAM,SAAS,cAAc,EAAE,CAAC,GAAG,OAAO;AAC1C,QAAI,OAAQ,KAAI,KAAK,MAAM;AAAA,EAC7B;AACA,SAAO;AACT;;;ACzDO,SAAS,cAAc,OAA2B;AACvD,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,aAAW,KAAK,OAAO;AACrB,UAAM,KAAK,cAAc;AACzB,UAAM,KAAK,mBAAmB,EAAE,MAAM,IAAI;AAC1C,UAAM,KAAK,iBAAiB,EAAE,KAAK,GAAG;AACtC,UAAM,KAAK,kBAAkB,EAAE,MAAM,GAAG;AACxC,QAAI,EAAE,QAAS,OAAM,KAAK,mBAAmB,EAAE,OAAO,GAAG;AACzD,UAAM,WAAW,EAAE,QAAQ,IAAI,CAAC,MAAM;AACpC,YAAM,QAAQ,gBAAgB,EAAE,SAAS;AACzC,aAAO,EAAE,SAAS,GAAG,KAAK,YAAY,EAAE,MAAM,OAAO;AAAA,IACvD,CAAC;AACD,UAAM,KAAK,UAAU,SAAS,KAAK,IAAI,CAAC,GAAG;AAC3C,QAAI,EAAE,aAAc,OAAM,KAAK,oBAAoB,EAAE,YAAY,GAAG;AACpE,UAAM,KAAK,GAAG;AACd,UAAM,KAAK,EAAE;AAAA,EACf;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,eAAe,OAAmB,UAAwB,CAAC,GAAW;AACpF,SAAO,KAAK;AAAA,IACV;AAAA,MACE,OAAO,MAAM,IAAI,CAAC,OAAO;AAAA,QACvB,QAAQ,EAAE;AAAA,QACV,QAAQ,EAAE;AAAA,QACV,OAAO,EAAE;AAAA,QACT,SAAS,EAAE;AAAA,QACX,cAAc,EAAE;AAAA,QAChB,OAAO,EAAE,QAAQ,IAAI,CAAC,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,IAAI,QAAQ,EAAE,OAAO,EAAE;AAAA,MAClF,EAAE;AAAA,MACF,cAAc,QAAQ,IAAI,CAAC,OAAO,EAAE,MAAM,SAAS,EAAE,IAAI,IAAI,KAAK,EAAE,IAAI,EAAE;AAAA,IAC5E;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEO,SAAS,YACd,MACA,OACA,YACA,UAAwB,CAAC,GACjB;AACR,QAAM,WAAW,oBAAI,IAAwB;AAC7C,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,SAAS,IAAI,EAAE,MAAM,EAAG,UAAS,IAAI,EAAE,QAAQ,CAAC,CAAC;AACtD,aAAS,IAAI,EAAE,MAAM,EAAG,KAAK,CAAC;AAAA,EAChC;AAEA,QAAM,QAAkB;AAAA,IACtB,gBAAgB,IAAI;AAAA,IACpB;AAAA,IACA,cAAc,UAAU,wBAAwB,MAAM,MAAM;AAAA,IAC5D;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,CAAC,QAAQ,IAAI,KAAK,UAAU;AACrC,UAAM,KAAK,OAAO,MAAM,EAAE;AAC1B,UAAM,KAAK,EAAE;AACb,UAAM,WAAW,oBAAI,IAAoB;AACzC,eAAW,KAAK,MAAM;AACpB,YAAM,MAAM,GAAG,EAAE,MAAM,MAAM,EAAE,KAAK;AACpC,eAAS,IAAI,MAAM,SAAS,IAAI,GAAG,KAAK,KAAK,CAAC;AAAA,IAChD;AACA,eAAW,CAAC,KAAK,CAAC,KAAK,UAAU;AAC/B,YAAM,KAAK,KAAK,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,oBAAoB,EAAE,EAAE;AAAA,IAChE;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,KAAK,iBAAiB;AAC5B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,SAAS,QAAQ,MAAM,wVAAmV;AACrX,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,oJAAqJ;AAChK,UAAM,KAAK,EAAE;AACb,eAAW,KAAK,SAAS;AACvB,YAAM,KAAK,aAAa,EAAE,IAAI,aAAQ,EAAE,GAAG,EAAE;AAAA,IAC/C;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM,KAAK,UAAU;AACrB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,oMAA+L;AAC1M,QAAM,KAAK,iFAAiF;AAC5F,QAAM,KAAK,EAAE;AACb,SAAO,MAAM,KAAK,IAAI;AACxB;AAUO,SAAS,mBACd,MACA,YACA,SACQ;AACR,QAAM,QAAkB;AAAA,IACtB,wBAAwB,IAAI;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAS,QAAQ,IAAI;AAAA,IACrB,oBAAU,QAAQ,UAAU;AAAA,IAC5B,cAAS,QAAQ,OAAO;AAAA,IACxB;AAAA,EACF;AAEA,QAAM,WAA+F;AAAA,IACnG,EAAE,QAAQ,QAAQ,OAAO,iCAA4B;AAAA,IACrD;AAAA,MACE,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AAEA,aAAW,WAAW,UAAU;AAC9B,UAAM,QAAQ,WAAW,OAAO,CAAC,MAAM,EAAE,eAAe,WAAW,QAAQ,MAAM;AACjF,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,KAAK,QAAQ,KAAK;AACxB,UAAM,KAAK,EAAE;AACb,QAAI,QAAQ,MAAM;AAChB,YAAM,KAAK,IAAI,QAAQ,IAAI,GAAG;AAC9B,YAAM,KAAK,EAAE;AAAA,IACf;AACA,UAAM,WAAW,oBAAI,IAA0B;AAC/C,eAAW,MAAM,OAAO;AACtB,YAAM,OAAO,SAAS,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;AAC9C,WAAK,KAAK,EAAE;AACZ,eAAS,IAAI,GAAG,KAAK,QAAQ,IAAI;AAAA,IACnC;AACA,eAAW,CAAC,QAAQ,IAAI,KAAK,UAAU;AACrC,YAAM,KAAK,OAAO,MAAM,EAAE;AAC1B,YAAM,KAAK,EAAE;AACb,YAAM,KAAK,KAAK,KAAK,CAAC,EAAE,eAAe,MAAM,EAAE;AAC/C,YAAM,WAAW,KAAK;AAAA,QAAQ,CAAC,OAC7B,GAAG,KAAK,QACL,IAAI,CAAC,MAAM,EAAE,SAAS,EACtB,OAAO,CAAC,MAAmB,QAAQ,CAAC,CAAC,EACrC,IAAI,CAAC,MAAM,SAAS,CAAC,KAAK,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,KAAK,GAAG;AAAA,MACjE;AACA,YAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC;AACpC,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,KAAK,UAAU;AACrB,mBAAW,KAAK,OAAQ,OAAM,KAAK,SAAS,CAAC,IAAI;AAAA,MACnD;AACA,YAAM,KAAK,EAAE;AAAA,IACf;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;;;ACnKO,IAAM,aAA8B;AAAA,EACzC,EAAE,MAAM,qBAAqB,OAAO,mBAAmB;AAAA,EACvD,EAAE,MAAM,wBAAwB,OAAO,mBAAmB;AAAA,EAC1D,EAAE,MAAM,qCAAqC,OAAO,0BAA0B;AAAA,EAC9E,EAAE,MAAM,iBAAiB,OAAO,yCAAoC;AAAA,EACpE,EAAE,MAAM,oCAAoC,OAAO,6BAA6B;AAClF;AAEO,IAAM,mBAAoC;AAAA,EAC/C,EAAE,MAAM,mBAAmB,OAAO,wBAAwB;AAAA,EAC1D,EAAE,MAAM,mBAAmB,OAAO,cAAc;AAAA,EAChD,EAAE,MAAM,iBAAiB,OAAO,wBAAwB;AAAA,EACxD,EAAE,MAAM,kBAAkB,OAAO,uBAAuB;AAAA,EACxD,EAAE,MAAM,yBAAyB,OAAO,eAAe;AAAA,EACvD,EAAE,MAAM,wBAAwB,OAAO,kCAAkC;AAAA,EACzE,EAAE,MAAM,uBAAuB,OAAO,kCAAkC;AAAA,EACxE,EAAE,MAAM,iBAAiB,OAAO,YAAY;AAC9C;AAWO,IAAM,sBAAgC;AAAA;AAAA,EAE3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AChHA,IAAM,2BAA2B,IAAI,IAAI,oBAAoB,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;AAQxF,SAAS,aAAa,MAAuC;AAClE,aAAW,OAAO,KAAK,SAAS;AAC9B,eAAW,OAAO,kBAAkB;AAClC,UAAI,IAAI,IAAI,SAAS,IAAI,IAAI,GAAG;AAC9B,eAAO,EAAE,QAAQ,cAAc,QAAQ,eAAe,IAAI,KAAK,GAAG;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAEA,aAAW,OAAO,KAAK,SAAS;AAC9B,eAAW,OAAO,YAAY;AAC5B,UAAI,IAAI,IAAI,SAAS,IAAI,IAAI,GAAG;AAC9B,eAAO,EAAE,QAAQ,QAAQ,QAAQ,eAAe,IAAI,KAAK,GAAG;AAAA,MAC9D;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,KAAK,UAAU,IAAI,KAAK,EAAE,YAAY;AACnD,MAAI,yBAAyB,IAAI,GAAG,GAAG;AACrC,WAAO,EAAE,QAAQ,QAAQ,QAAQ,IAAI,KAAK,MAAM,6CAA6C;AAAA,EAC/F;AAEA,SAAO,EAAE,QAAQ,WAAW,QAAQ,4CAA4C;AAClF;AAOO,SAAS,cAAc,OAAqC;AACjE,SAAO,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,gBAAgB,aAAa,IAAI,EAAE,EAAE;AAC3E;AAWO,SAAS,UAAU,YAA8C;AACtE,MAAI,OAAO;AACX,MAAI,aAAa;AACjB,MAAI,UAAU;AACd,aAAW,KAAK,YAAY;AAC1B,QAAI,EAAE,eAAe,WAAW,OAAQ;AAAA,aAC/B,EAAE,eAAe,WAAW,aAAc;AAAA,QAC9C;AAAA,EACP;AACA,QAAM,QAAQ,WAAW;AACzB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,QAAQ,KAAK,eAAe;AAAA,EAC7C;AACF;;;ACtEA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOnB,KAAK;AAEP,eAAsB,cAAc,KAAa,YAAY,KAAgC;AAC3F,MAAI;AACJ,MAAI;AACF,KAAC,EAAE,SAAS,IAAI,MAAM,OAAO,YAAY;AAAA,EAC3C,QAAQ;AACN,UAAM,IAAI,MAAM,YAAY;AAAA,EAC9B;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,SAAS,OAAO,EAAE,UAAU,KAAK,CAAC;AAAA,EACpD,SAAS,GAAG;AACV,UAAM,MAAO,EAAY;AACzB,QAAI,gDAAgD,KAAK,GAAG,GAAG;AAC7D,YAAM,IAAI;AAAA,QACR;AAAA;AAAA;AAAA,GAA2F,GAAG;AAAA,MAChG;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAEA,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,WAAW,EAAE,WAAW,GAAG,CAAC;AAC1D,UAAM,OAAO,MAAM,QAAQ,QAAQ;AAEnC,UAAM,kBAAkB,oBAAI,IAAY;AACxC,SAAK,GAAG,YAAY,CAAC,aAAa;AAChC,YAAM,IAAI,SAAS,IAAI;AACvB,YAAM,MAAM,SAAS,QAAQ,EAAE,cAAc,KAAK,IAAI,YAAY;AAClE,YAAM,cAAc,oCAAoC,KAAK,CAAC;AAC9D,YAAM,eAAe,GAAG,WAAW,OAAO,KAAK,OAAO,2BAA2B,OAAO;AACxF,WAAK,eAAe,iBAAiB,SAAS,OAAO,IAAI,KAAK;AAC5D,wBAAgB,IAAI,CAAC;AAAA,MACvB;AAAA,IACF,CAAC;AAED,UAAM,KAAK,KAAK,KAAK,EAAE,WAAW,eAAe,SAAS,UAAU,CAAC;AAGrE,UAAM,KAAK,SAAS,MAAO,SAA+C,MAAM,KAAK;AAIrF,UAAM,UAAU,MAAM,KAAK,SAAS,MAAM;AACxC,YAAM,MAAgB,CAAC;AACvB,iBAAW,SAAS,MAAM,KAAK,SAAS,WAAW,GAAG;AACpD,YAAI;AACF,gBAAM,QAAQ,MAAM;AACpB,cAAI,CAAC,MAAO;AACZ,qBAAW,QAAQ,MAAM,KAAK,KAAK,GAAG;AACpC,gBAAI,KAAK,SAAS,QAAQ,gBAAgB;AACxC,kBAAI,KAAK,KAAK,OAAO;AAAA,YACvB;AAAA,UACF;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AACA,aAAO,IAAI,KAAK,IAAI;AAAA,IACtB,CAAC;AAED,WAAO;AAAA,MACL,YAAY,UAAU,CAAC,EAAE,MAAM,SAAS,MAAM,IAAI,CAAC,IAAI,CAAC;AAAA,MACxD,iBAAiB,CAAC,GAAG,eAAe;AAAA,IACtC;AAAA,EACF,UAAE;AACA,UAAM,QAAQ,MAAM;AAAA,EACtB;AACF;;;AC/EO,SAAS,cAAc,QAAwB;AACpD,QAAM,UAAU,OAAO,QAAQ,mBAAmB,GAAG,EAAE,KAAK;AAC5D,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,QAAQ,MAAM,KAAK;AACjC,QAAM,QAAQ,MAAM,CAAC,EAAE,CAAC,EAAE,YAAY,IAAI,MAAM,CAAC,EAAE,MAAM,CAAC;AAC1D,QAAM,OAAO,MAAM,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,CAAC;AACtE,SAAO,CAAC,OAAO,GAAG,IAAI,EAAE,KAAK,EAAE;AACjC;AAGO,SAAS,cAAc,QAAwB;AACpD,SAAO,OACJ,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,YAAY,EAAE,EACtB,YAAY;AACjB;AAQO,SAAS,eAAe,QAA2C;AACxE,QAAM,IAAI,OAAO,YAAY;AAC7B,MAAI,uCAAuC,KAAK,CAAC,EAAG,QAAO;AAC3D,MAAI,OAAO,KAAK,CAAC,EAAG,QAAO;AAC3B,MAAI,qEAAqE,KAAK,CAAC,EAAG,QAAO;AACzF,SAAO;AACT;AAGO,SAAS,cAAc,OAA4C;AACxE,QAAM,MAAM,oBAAI,IAAwB;AACxC,aAAW,KAAK,OAAO;AACrB,UAAM,WAAW,IAAI,IAAI,EAAE,MAAM;AACjC,QAAI,SAAU,UAAS,KAAK,CAAC;AAAA,QACxB,KAAI,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;AAAA,EAC5B;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,MAAgE;AAC9F,MAAI,KAAK,QAAQ,WAAW,EAAG,QAAO;AACtC,QAAM,QAAQ,KAAK,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,OAAO;AAC3D,QAAM,SAAS,SAAS,KAAK,QAAQ,CAAC;AACtC,MAAI,CAAC,OAAO,UAAW,QAAO;AAC9B,SAAO,EAAE,MAAM,OAAO,WAAW,QAAQ,OAAO,OAAO;AACzD;;;AC/CO,IAAM,cAAuB,CAAC,OAAO,QAAQ;AAClD,QAAM,WAAW,cAAc,KAAK;AACpC,MAAI,SAAS,SAAS,EAAG,QAAO;AAEhC,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,CAAC,QAAQ,IAAI,KAAK,UAAU;AACrC,UAAM,QAAQ,cAAc,MAAM;AAClC,UAAM,SAAS,UAAU,cAAc,MAAM,CAAC;AAC9C,UAAM,UAAU,KACb,IAAI,CAAC,MAAM,gBAAgB,CAAC,CAAC,EAC7B,OAAO,CAAC,MAAoD,MAAM,IAAI;AAEzE,QAAI,QAAQ,WAAW,EAAG;AAE1B,UAAM,KAAK,gBAAgB,KAAK,gBAAgB;AAChD,UAAM,KAAK,UAAU;AACrB,SAAK,QAAQ,CAAC,GAAG,MAAM;AACrB,YAAM,MAAM,gBAAgB,CAAC;AAC7B,UAAI,CAAC,IAAK;AACV,YAAM,SAAS,EAAE,UAAU;AAC3B,YAAM,QAAQ,EAAE,SAAS;AACzB,YAAM;AAAA,QACJ,kBAAkB,IAAI,QAAQ,IAAI,IAAI,IAAI,eAAe,MAAM,cAAc,KAAK,UAC/E,MAAM,KAAK,SAAS,IAAI,KAAK;AAAA,MAClC;AAAA,IACF,CAAC;AACD,UAAM,KAAK,MAAM;AACjB,UAAM,KAAK,gBAAgB,MAAM,IAAI;AACrC,UAAM,KAAK,oBAAoB;AAC/B,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,SAAO,EAAE,UAAU,iBAAiB,SAAS,MAAM,KAAK,IAAI,EAAE;AAChE;;;ACzCO,IAAM,kBAA2B,CAAC,OAAO,QAAQ;AACtD,OAAK;AACL,QAAM,WAAW,cAAc,KAAK;AACpC,MAAI,SAAS,SAAS,EAAG,QAAO;AAEhC,QAAM,UAAuD,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,MAAM,CAAC,EAAE;AAC7F,QAAM,UAA+D,CAAC;AAEtE,aAAW,UAAU,SAAS,KAAK,GAAG;AACpC,UAAM,SAAS,UAAU,cAAc,MAAM,CAAC;AAC9C,UAAM,SAAS,eAAe,MAAM;AACpC,YAAQ,MAAM,EAAE,KAAK,OAAO,MAAM,GAAG;AACrC,YAAQ,KAAK,EAAE,OAAO,cAAc,MAAM,GAAG,QAAQ,OAAO,CAAC;AAAA,EAC/D;AAEA,QAAM,WAAsD;AAAA,IAC1D,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAEA,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,UAAU,CAAC,QAAQ,SAAS,MAAM,GAAY;AACvD,UAAM,OAAO,QAAQ,MAAM;AAC3B,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,GAAG,SAAS,MAAM,CAAC,EAAE,KAAK,IAAI;AACxE,UAAM,KAAK,KAAK,MAAM,MAAM,KAAK,IAAI;AAAA,EACvC;AAEA,aAAW,EAAE,OAAO,QAAQ,OAAO,KAAK,SAAS;AAC/C,UAAM,KAAK,MAAM,KAAK,YAAY,MAAM,UAAU,MAAM,OAAO;AAAA,EACjE;AAEA,QAAM,KAAK,IAAI;AACf,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,uBAAuB;AAElC,SAAO,EAAE,UAAU,qBAAqB,SAAS,MAAM,KAAK,IAAI,EAAE;AACpE;;;ACjDO,IAAM,cAAuB,CAAC,OAAO,QAAQ;AAClD,QAAM,WAAW,cAAc,KAAK;AACpC,MAAI,SAAS,SAAS,EAAG,QAAO;AAEhC,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,UAAU,SAAS,KAAK,GAAG;AACpC,UAAM,KAAK,wBAAwB,MAAM,MAAM;AAAA,EACjD;AACA,QAAM,KAAK,EAAE;AAEb,QAAM,KAAK,UAAU;AACrB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,sKAAsK;AACjL,QAAM,KAAK,mDAAmD,IAAI,QAAQ,mEAAmE;AAC7I,QAAM,KAAK,EAAE;AAEb,SAAO,EAAE,UAAU,iBAAiB,SAAS,MAAM,KAAK,IAAI,EAAE;AAChE;;;ACxBO,IAAM,eAA6B,CAAC,OAAO,QAAQ,YAAY,MAAM;AAErE,SAAS,aAAa,GAA4B;AACvD,SAAQ,aAA0B,SAAS,CAAC;AAC9C;;;ACfO,IAAM,WAAwD;AAAA,EACnE,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AACR;;;ACSA,IAAM,QAAsB;AAAA,EAC1B;AAAA,IACE,QAAQ;AAAA,IACR,UAAU,CAAC,qBAAqB,wBAAwB,kCAAkC;AAAA,EAC5F;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,UAAU,CAAC,mBAAmB,iBAAiB,iBAAiB;AAAA,EAClE;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,UAAU;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,UAAU,CAAC,qCAAqC,SAAS;AAAA,EAC3D;AACF;AAGA,SAAS,MAAM,MAAsB;AACnC,SAAO,KAAK,QAAQ,WAAW,EAAE,EAAE,YAAY;AACjD;AAGO,SAAS,WAAW,SAAiB,UAA2B;AACrE,QAAM,IAAI,MAAM,OAAO;AACvB,QAAM,IAAI,MAAM,QAAQ;AACxB,MAAI,MAAM,EAAG,QAAO;AACpB,SAAO,EAAE,SAAS,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,CAAC;AAClD;AAOO,SAAS,aAAa,KAAa,UAA0B;AAClE,aAAW,QAAQ,OAAO;AACxB,eAAW,WAAW,KAAK,UAAU;AACnC,UAAI,IAAI,SAAS,OAAO,EAAG,QAAO,KAAK;AAAA,IACzC;AAAA,EACF;AACA,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,QAAI,WAAW,EAAE,UAAU,QAAQ,EAAG,QAAO;AAAA,EAC/C,QAAQ;AAAA,EAER;AACA,SAAO;AACT;;;AbjEA,eAAsB,KAAK;AAAA,EACzB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,OAAO,CAAC;AAAA,EACR,QAAQ;AACV,GAAqC;AACnC,QAAM,OAAO,SAAS,GAAG;AACzB,QAAM,SAASC,MAAK,KAAKA,MAAK,QAAQ,OAAO,GAAG,IAAI;AACpD,QAAM,WAAWA,MAAK,KAAK,QAAQ,OAAO;AAC1C,QAAM,GAAG,MAAM,UAAU,EAAE,WAAW,KAAK,CAAC;AAE5C,MAAI,KAAK,yBAAoB,GAAG,EAAE;AAClC,QAAM,OAAO,MAAM,UAAU,GAAG;AAEhC,QAAM,WAAW,uBAAuB,MAAM,GAAG;AACjD,QAAM,YAAY,oBAAoB,IAAI;AAC1C,MAAI,KAAK,KAAK,SAAS,MAAM,4BAA4B,UAAU,MAAM,0BAA0B;AAEnG,QAAM,aAA0B,UAAU,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,IAAI,EAAE;AAC7E,aAAW,QAAQ,UAAU;AAC3B,QAAI;AACF,UAAI,KAAK,wBAAmB,IAAI,EAAE;AAClC,iBAAW,KAAK,EAAE,MAAM,MAAM,UAAU,MAAM,EAAE,SAAS,IAAI,CAAC,GAAG,MAAM,KAAK,CAAC;AAAA,IAC/E,SAAS,GAAG;AACV,UAAI,KAAK,eAAgB,EAAY,OAAO,EAAE;AAAA,IAChD;AAAA,EACF;AAEA,MAAI,kBAA4B,CAAC;AACjC,MAAI,UAAU;AACZ,QAAI,KAAK,8CAAyC;AAClD,QAAI;AACF,YAAM,SAAS,MAAM,cAAc,GAAG;AACtC,iBAAW,KAAK,GAAG,OAAO,UAAU;AACpC,wBAAkB,OAAO;AACzB,UAAI,KAAK,OAAO,OAAO,WAAW,MAAM,oCAAoC;AAC5E,UAAI,gBAAgB,SAAS,GAAG;AAC9B,YAAI,KAAK,OAAO,gBAAgB,MAAM,kCAAkC;AAAA,MAC1E;AAAA,IACF,SAAS,GAAG;AACV,UAAI,KAAK,6BAA8B,EAAY,OAAO,EAAE;AAC5D,UAAI,KAAK,mCAAmC;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,WAAW,WAAW,QAAQ,CAAC,EAAE,MAAM,KAAK,MAAM,iBAAiB,MAAM,IAAI,CAAC;AAEpF,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,QAAQ,SAAS,OAAO,CAAC,MAAM;AACnC,UAAM,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,MAAM,IAAI,EAAE,KAAK,IAAI,EAAE,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC;AAC9F,QAAI,KAAK,IAAI,GAAG,EAAG,QAAO;AAC1B,SAAK,IAAI,GAAG;AACZ,WAAO;AAAA,EACT,CAAC;AAGD,QAAM,YAAY;AAClB,QAAM,YAAsB,CAAC;AAC7B,aAAW,KAAK,KAAK,SAAS,SAAS,GAAG;AACxC,UAAM,OAAO,yBAAyB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC;AACpD,QAAI,MAAM;AACR,YAAM,IAAI,IAAI,MAAM,GAAG;AACvB,UAAI,KAAK,YAAY,KAAK,CAAC,EAAG,WAAU,KAAK,CAAC;AAAA,IAChD;AAAA,EACF;AAEA,QAAM,WAAW,IAAI,IAAI,GAAG,EAAE;AAG9B,QAAM,aAAa,oBAAI,IAAoB;AAC3C,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,QAAQ,CAAC,MAAsB;AACnC,QAAI,WAAW,IAAI,CAAC,EAAG,QAAO,WAAW,IAAI,CAAC;AAC9C,UAAM,SAAS,aAAa,GAAG,QAAQ;AACvC,QAAI,OAAO,aAAa,CAAC;AACzB,UAAM,YAAY,GAAG,MAAM,IAAI,IAAI;AACnC,QAAI,UAAU,IAAI,SAAS,GAAG;AAC5B,YAAM,IAAI,IAAI,IAAI,CAAC,EAAE,SAAS,QAAQ,eAAe,GAAG;AACxD,aAAO,GAAG,CAAC,KAAK,IAAI;AAAA,IACtB;AACA,UAAM,QAAQ,GAAG,MAAM,IAAI,IAAI;AAC/B,cAAU,IAAI,KAAK;AACnB,eAAW,IAAI,GAAG,KAAK;AACvB,WAAO;AAAA,EACT;AAEA,aAAW,KAAK,OAAO;AACrB,eAAW,KAAK,EAAE,QAAS,GAAE,YAAY,MAAM,EAAE,GAAG;AAAA,EACtD;AACA,aAAW,KAAK,UAAW,OAAM,CAAC;AAMlC,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,KAAK,MAAO,YAAW,KAAK,EAAE,QAAS,UAAS,IAAI,EAAE,GAAG;AACpE,QAAM,UAAwB,CAAC;AAC/B,aAAW,KAAK,iBAAiB;AAC/B,QAAI,SAAS,IAAI,CAAC,EAAG;AACrB,QAAI,WAAW,IAAI,CAAC,EAAG;AACvB,UAAM,OAAO,MAAM,CAAC;AACpB,YAAQ,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC;AAAA,EAC/B;AACA,MAAI,QAAQ,SAAS,GAAG;AACtB,QAAI,KAAK,UAAK,QAAQ,MAAM,kEAAkE;AAAA,EAChG;AAEA,MAAI,KAAK,gBAAW,MAAM,MAAM,+BAA+B,WAAW,IAAI,iBAAiB;AAC/F,MAAI,WAAW,SAAS,GAAG;AACzB,QAAI,KAAK,mGAAmG;AAC5G,WAAO,EAAE,QAAQ,OAAO,SAAS,CAAC,GAAG,YAAY,GAAG,OAAO,EAAE;AAAA,EAC/D;AAEA,QAAM,aAAa,cAAc,KAAK;AACtC,QAAM,iBAAiB,UAAU,UAAU;AAC3C,MAAI;AAAA,IACF,0BAAqB,eAAe,IAAI,WAAW,eAAe,UAAU,iBAAiB,eAAe,OAAO;AAAA,EACrH;AAIA,MAAI,eAAe,iBAAiB,CAAC,OAAO;AAC1C,UAAM,GAAG;AAAA,MACPA,MAAK,KAAK,QAAQ,mBAAmB;AAAA,MACrC,mBAAmB,MAAM,YAAY,cAAc;AAAA,IACrD;AACA,QAAI,KAAK,EAAE;AACX,QAAI,KAAK,cAAS,eAAe,UAAU,0DAA0D;AACrG,QAAI,KAAK,4EAA4E;AACrF,QAAI,KAAK,WAAWA,MAAK,KAAK,QAAQ,mBAAmB,CAAC,sBAAsB;AAChF,QAAI,KAAK,yFAAyF;AAClG,QAAI,KAAK,EAAE;AACX,WAAO,EAAE,QAAQ,OAAO,SAAS,CAAC,GAAG,YAAY,GAAG,OAAO,WAAW,KAAK;AAAA,EAC7E;AAEA,MAAI,aAAa;AACjB,QAAM,iBAAiB,oBAAI,IAAY;AACvC,aAAW,CAAC,SAAS,IAAI,KAAK,YAAY;AACxC,UAAM,OAAOA,MAAK,KAAK,UAAU,IAAI;AACrC,UAAM,YAAYA,MAAK,QAAQ,IAAI;AACnC,QAAI,CAAC,eAAe,IAAI,SAAS,GAAG;AAClC,YAAM,GAAG,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAC7C,qBAAe,IAAI,SAAS;AAAA,IAC9B;AACA,QAAI;AACF,YAAM,MAAM,MAAM,YAAY,SAAS,EAAE,SAAS,IAAI,CAAC;AACvD,YAAM,GAAG,UAAU,MAAM,GAAG;AAC5B,UAAI,KAAK,YAAO,IAAI,MAAM,IAAI,OAAO,eAAe,CAAC,SAAS;AAC9D;AAAA,IACF,SAAS,GAAG;AACV,UAAI,KAAK,YAAO,IAAI,WAAO,EAAY,OAAO,EAAE;AAAA,IAClD;AAAA,EACF;AAEA,QAAM,GAAG,UAAUA,MAAK,KAAK,QAAQ,WAAW,GAAG,cAAc,KAAK,CAAC;AACvE,QAAM,GAAG,UAAUA,MAAK,KAAK,QAAQ,YAAY,GAAG,eAAe,OAAO,OAAO,CAAC;AAClF,QAAM,GAAG,UAAUA,MAAK,KAAK,QAAQ,WAAW,GAAG,YAAY,MAAM,OAAO,YAAY,OAAO,CAAC;AAChG,QAAM,GAAG;AAAA,IACPA,MAAK,KAAK,QAAQ,mBAAmB;AAAA,IACrC,mBAAmB,MAAM,YAAY,cAAc;AAAA,EACrD;AAEA,aAAW,UAAU,MAAM;AACzB,UAAM,UAAU,SAAS,MAAM;AAC/B,QAAI,CAAC,QAAS;AACd,UAAM,SAAS,QAAQ,OAAO,EAAE,UAAU,MAAM,UAAU,QAAQ,CAAC;AACnE,QAAI,CAAC,OAAQ;AACb,UAAM,GAAG,UAAUA,MAAK,KAAK,QAAQ,OAAO,QAAQ,GAAG,OAAO,OAAO;AACrE,QAAI,KAAK,eAAe,OAAO,QAAQ,YAAY,MAAM,GAAG;AAAA,EAC9D;AAEA,SAAO,EAAE,QAAQ,OAAO,SAAS,YAAY,OAAO,WAAW,KAAK;AACtE;;;ActLA,IAAM,UAAU;AAEhB,SAAS,YAAkB;AACzB,MAAI,KAAK,aAAa,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yDAe0B,aAAa,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CA2B/E;AACD;AAEA,eAAe,OAAsB;AACnC,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AAEjC,MAAI,KAAK,WAAW,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,QAAQ,GAAG;AACvE,cAAU;AACV,YAAQ,KAAK,KAAK,WAAW,IAAI,IAAI,CAAC;AAAA,EACxC;AAEA,MAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,WAAW,GAAG;AACrD,QAAI,KAAK,OAAO;AAChB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,WAAW,KAAK,SAAS,YAAY;AAC3C,QAAM,QAAQ,KAAK,SAAS,SAAS;AAGrC,MAAI,OAAqC,CAAC;AAC1C,QAAM,UAAU,KAAK,UAAU,CAAC,MAAc,MAAM,YAAY,EAAE,WAAW,SAAS,CAAC;AACvF,MAAI,YAAY,IAAI;AAClB,UAAM,MACJ,KAAK,OAAO,MAAM,WACd,KAAK,UAAU,CAAC,IAChB,KAAK,OAAO,EAAE,MAAM,UAAU,MAAM;AAC1C,QAAI,CAAC,KAAK;AACR,UAAI,IAAI,4CAA4C,aAAa,KAAK,IAAI,CAAC,EAAE;AAC7E,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM,YAAY,IAAI,MAAM,GAAG,EAAE,IAAI,CAAC,MAAc,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAC5E,eAAW,KAAK,WAAW;AACzB,UAAI,CAAC,aAAa,CAAC,GAAG;AACpB,YAAI,IAAI,2BAA2B,CAAC,aAAa,aAAa,KAAK,IAAI,CAAC,EAAE;AAC1E,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA,UAAI,MAAM,MAAO,MAAK,KAAK,CAAC;AAAA,IAC9B;AAAA,EACF;AAEA,QAAM,gBAAgB,oBAAI,IAAI,CAAC,cAAc,UAAU,SAAS,CAAC;AACjE,QAAM,aAAa,KAAK,OAAO,CAAC,GAAW,MAAc;AACvD,QAAI,EAAE,WAAW,IAAI,EAAG,QAAO;AAE/B,QAAI,IAAI,KAAK,KAAK,IAAI,CAAC,MAAM,SAAU,QAAO;AAC9C,QAAI,cAAc,IAAI,CAAC,EAAG,QAAO;AACjC,WAAO;AAAA,EACT,CAAC;AACD,QAAM,CAAC,KAAK,SAAS,oBAAoB,IAAI;AAE7C,MAAI,CAAC,KAAK;AACR,QAAI,IAAI,oDAAoD;AAC5D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI;AACF,QAAI,IAAI,GAAG;AAAA,EACb,QAAQ;AACN,QAAI,IAAI,gBAAgB,GAAG,EAAE;AAC7B,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,SAAS,MAAM,KAAK,EAAE,KAAK,SAAS,QAAQ,UAAU,MAAM,MAAM,CAAC;AAEzE,MAAI,KAAK,EAAE;AACX,MAAI,OAAO,UAAU,GAAG;AACtB,QAAI,KAAK,iBAAiB;AAC1B,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,KAAK,SAAS,OAAO,UAAU,IAAI,OAAO,KAAK,mBAAmB,OAAO,MAAM,EAAE;AACvF;AAEA,KAAK,EAAE,MAAM,CAAC,MAAe;AAC3B,MAAI,IAAI,OAAO,CAAC,CAAC;AACjB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["path","path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fontfetch",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Download every web font from any site into a project-ready folder — with CSS, manifest, and framework configs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fontfetch": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"start": "node ./dist/cli.js",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"font",
|
|
31
|
+
"fonts",
|
|
32
|
+
"webfont",
|
|
33
|
+
"web-font",
|
|
34
|
+
"font-face",
|
|
35
|
+
"scrape",
|
|
36
|
+
"scraper",
|
|
37
|
+
"downloader",
|
|
38
|
+
"google-fonts",
|
|
39
|
+
"self-host",
|
|
40
|
+
"self-hosted-fonts",
|
|
41
|
+
"cli",
|
|
42
|
+
"nextjs",
|
|
43
|
+
"tailwind"
|
|
44
|
+
],
|
|
45
|
+
"author": "Niyam Vora <niyamvora@gmail.com>",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/niyamvora/fontfetch.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/niyamvora/fontfetch/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/niyamvora/fontfetch#readme",
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.10.0",
|
|
57
|
+
"playwright": "^1.49.0",
|
|
58
|
+
"tsup": "^8.3.0",
|
|
59
|
+
"typescript": "^5.6.0",
|
|
60
|
+
"vitest": "^2.1.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"playwright": "^1.49.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"playwright": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|