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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q2D9GYbnSV56MAVVhHPRhH
This commit is contained in:
Claude
2026-10-09 07:00:21 +00:00
parent 924b5fda27
commit 77ac5c4685
2 changed files with 57 additions and 0 deletions
+6
View File
@@ -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 <file>` (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.
+51
View File
@@ -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 <n>
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 <path> Write usage per icon to a JSON file as { [icon]: [...usage] }:
use cases with --describe, otherwise individual usages
(implies --context)
--icons-file <path> 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<string, IconUsage>) => {
}
};
/**
* 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) {