From 77ac5c46852bf48e4d8cf29a0ae48f0ca67c93bb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:00:21 +0000 Subject: [PATCH] feat(scripts): add --output to write icon usage as JSON Writes { [icon]: [...usage] } to a file: grouped use cases with --describe, otherwise every individual usage context. Implies --context. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q2D9GYbnSV56MAVVhHPRhH --- scripts/iconUsage.md | 6 +++++ scripts/iconUsage.mts | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/scripts/iconUsage.md b/scripts/iconUsage.md index 7b3adee8a..835d959bc 100644 --- a/scripts/iconUsage.md +++ b/scripts/iconUsage.md @@ -26,6 +26,12 @@ pnpm icon-usage --icons-file exports.txt --describe --max-contexts 30 `--describe` does the same, then sends the collected contexts to an OpenAI model (`OPENAI_API_KEY` required; `--model` or `OPENAI_MODEL` selects the model, `gpt-5-mini` by default). The model groups them into several use cases per icon, each with a short lowercase `useCase` phrase in the style of the `use-cases` field in `icons/*.json`, a description of where the icon appears in the UI, its approximate share of the sample, and example links. The icon's existing `use-cases` are included in the prompt. Responses are cached in the cache directory. +Use `--output ` (or `-o`) to write the results per icon to a JSON file shaped as `{ [icon]: [...usage] }`. With `--describe`, each entry is a use case (`useCase`, `description`, `sampleShare`, `examples`). Without it, each entry is an individual usage (`description`, `repository`, `url`, `kind`, `parents`, `component`, `labels`, `handlers`, `links`). `--output` implies `--context`, and the normal terminal, `--json` or `--csv` output is still printed. + +```sh +pnpm icon-usage --icons-file exports.txt --describe --output usage.json +``` + To keep runs bounded, searching for an icon stops once `--max-contexts` contexts (default 40) are collected, and at most two contexts are taken from each repository. When the limit is hit, the repository counts for that icon are a lower bound, and a warning says so. JSON output includes every context per match under `repositories.*.matches[].contexts`, and per-icon summaries and descriptions under `usage`. Context extraction uses heuristics, not a full parser. It works best for JSX (React, Preact, Solid), Vue, Svelte and Astro templates in the same file as the import. Angular templates in separate `.html` files are not fetched. diff --git a/scripts/iconUsage.mts b/scripts/iconUsage.mts index 37daa245c..8b9e3a7b9 100644 --- a/scripts/iconUsage.mts +++ b/scripts/iconUsage.mts @@ -156,6 +156,7 @@ type CliOptions = { maxContexts: number; model: string; iconsFiles: string[]; + output: string | null; }; type SearchItem = { @@ -293,6 +294,7 @@ const parseArgs = (argv: string[]): CliOptions => { maxContexts: DEFAULT_MAX_CONTEXTS, model: process.env.OPENAI_MODEL ?? DEFAULT_DESCRIBE_MODEL, iconsFiles: [], + output: null, }; for (let index = 0; index < argv.length; index += 1) { @@ -406,6 +408,18 @@ const parseArgs = (argv: string[]): CliOptions => { options.model = arg.slice('--model='.length); continue; } + if (arg === '--output' || arg === '-o') { + const file = argv[++index]; + if (!file) throw new Error('--output requires a path.'); + options.output = path.resolve(file); + options.context = true; + continue; + } + if (arg.startsWith('--output=')) { + options.output = path.resolve(arg.slice('--output='.length)); + options.context = true; + continue; + } if (arg === '--icons-file') { const file = argv[++index]; if (!file) throw new Error('--icons-file requires a path.'); @@ -529,6 +543,9 @@ Options: --max-files-per-query Max candidate files to validate per query slice (default: ${DEFAULT_MAX_FILES_PER_QUERY}) --thorough Shortcut for --max-pages ${THOROUGH_MAX_PAGES} --max-files-per-query ${THOROUGH_MAX_FILES_PER_QUERY} + --output, -o Write usage per icon to a JSON file as { [icon]: [...usage] }: + use cases with --describe, otherwise individual usages + (implies --context) --icons-file Read icon names (one per line, kebab-case or export names like Trash2) from a file; repeatable --context Extract how each icon is used in context (parent element, @@ -1411,6 +1428,39 @@ const printUsage = (usage: Record) => { } }; +/** + * Writes `{ [icon]: [...usage] }`: the grouped use cases when `--describe` ran, + * otherwise every individual usage context found for the icon. + */ +const writeOutput = async (file: string, result: AnalysisResult, options: CliOptions) => { + const output = Object.fromEntries( + options.icons.map((icon) => { + if (options.describe) return [icon, result.usage?.[icon]?.descriptions ?? []]; + const usages = Object.entries(result.repositories).flatMap(([repository, record]) => + record.matches + .filter((match) => match.icon === icon) + .flatMap((match) => + (match.contexts ?? []).map((context) => ({ + description: context.description, + repository, + url: `${match.htmlUrl}#L${context.line}`, + kind: context.kind, + parents: context.ancestors, + component: context.component, + labels: [...context.labels, ...context.i18nKeys], + handlers: context.handlers, + links: context.links, + })), + ), + ); + return [icon, usages]; + }), + ); + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, `${JSON.stringify(output, null, 2)}\n`); + status(`wrote usage for ${options.icons.length} icon(s) to ${file}`); +}; + const printCsv = (result: AnalysisResult) => { const rows = [['repository', 'icons', 'packages', 'matches', 'contexts']]; for (const [repo, record] of Object.entries(result.repositories)) { @@ -1438,6 +1488,7 @@ try { ...(await readIconsFiles(options.iconsFiles)), ]); const result = await analyze(options); + if (options.output) await writeOutput(options.output, result, options); if (options.json) { console.log(JSON.stringify(result, null, 2)); } else if (options.csv) {