@grafana/design-codemods
v0.2.0
Published
Codemod tooling for migrating consumers onto @grafana/design packages
Readme
@grafana/design-codemods
Codemod tooling for migrating consumers onto @grafana/design packages.
This is the home of:
- A
design-codemodsCLI for bulk source rewrites (the heavy-lifting jscodeshift transforms). - Re-exports of any migration tables that codemods consume — most notably the icon
migrationstable from@grafana/icons, which lives here so application code never has a reason to import it directly from the icons package.
The companion to this package is @grafana/eslint-plugin-design, which carries the per-call-site ESLint rules that auto-fix the same migrations during normal editing. Both reach for the same migration data; the codemods package handles bulk + cross-file transforms, the eslint plugin handles the editor-time path.
Installation
pnpm add -D @grafana/design-codemodsCLI
pnpm dlx @grafana/design-codemods help
pnpm dlx @grafana/design-codemods <subcommand> [paths...] [options]Code-rewriting subcommands
These subcommands read source files, apply transforms in place, and report a summary. All share the same flags and path-walking behavior:
--dry-run/-d— print which files would change without writing.--verbose/-v— also log unchanged files.
Paths may be files or directories; directories are walked recursively for .ts/.tsx/.js/.jsx (+ .cts/.cjs/.mts/.mjs) files. node_modules, dist, build, .turbo, .next, .git and coverage directories are skipped.
Each transform preserves the source position of imports and the surrounding code's formatting: when all specifiers of an import move, the new import replaces the original in place; when only some specifiers move, the new import is inserted immediately after the trimmed original. Existing imports from the target package are merged into rather than duplicated. The codemods never reformat unrelated code (no quote-style changes outside the edits, no JSX whitespace normalization, no auto-format).
icon-migration
One-shot migration of <Icon name="..." /> from @grafana/ui to <Icon component={X} /> from @grafana/icons. Looks each name up in the @grafana/icons/migrations table and rewrites the JSX + manages the imports (adds the matched component and the Icon wrapper to @grafana/icons, removes Icon from @grafana/ui).
pnpm dlx @grafana/design-codemods icon-migration apps/plugin/src
pnpm dlx @grafana/design-codemods icon-migration . --dry-runStage A scope (this codemod):
- Static literal
namevalues (string literal or JSX-expression-wrapped string literal). - Adds the matched component and
Iconto@grafana/icons, merging into an existing import where present. - Removes
Iconfrom@grafana/ui(drops the whole import declaration when it had no other specifiers). - Resolves import-name collisions by aliasing as
<Name>Icon, escalating to<Name>IconN.
Skipped (reported as manual review):
- Dynamic
name(variable, ternary, computed). - Missing
nameprop. IgnoredLegacyIconNamevalues (google-hangouts-alt,hipchat).- Unknown legacy name (not in the migrations table).
- Aliased
Iconimport (import { Icon as UIIcon } from '@grafana/ui').
When any usage in a file falls into one of these categories, the whole file is skipped — half-migrating leaves the file in a non-compiling state (one Icon binding, mixed name= / component= usage). The codemod prints a per-site report so the developer knows what's left:
icon-migration: 64 changed, 126 sites migrated, 42 skipped, 2099 unchanged
52 sites need manual review:
apps/plugin/src/components/Foo.tsx:42:14 dynamic-name
apps/plugin/src/pages/Bar.tsx:18:10 ignored-legacy-name (name="google-hangouts-alt")
…Exit code is 0 even when there are manual-review sites — they're informational, not failures. Re-running the codemod with the same input is a no-op (idempotent).
Companion to prefer-grafana-icons-component in @grafana/eslint-plugin-design: same migration, different mechanism (one-shot CLI vs continuous lint-time --fix). Use the codemod for the bulk initial migration; the ESLint rule catches drift on new code.
icon-imports
Moves icon-related named imports out of @grafana/components into @grafana/icons. Covers the Icon wrapper, every PascalCase icon component, AllIcons, iconMetaData, and the icon-related type exports (SVGComponent, SVGComponentProps, IconName, IconProps, IconSize). Non-icon imports from @grafana/components stay in place.
pnpm dlx @grafana/design-codemods icon-imports apps/plugin/src
pnpm dlx @grafana/design-codemods icon-imports . --dry-runThe name set is derived at codemod build time from Object.keys(@grafana/icons) plus a hardcoded list of type-only exports.
provider-imports
Moves the provider/hook/identifier exports out of @grafana/components into the dedicated @grafana/theme-providers package. Non-provider imports from @grafana/components stay in place. import type { … } and per-specifier type modifiers are preserved.
pnpm dlx @grafana/design-codemods provider-imports apps/plugin/src
pnpm dlx @grafana/design-codemods provider-imports . --dry-runNames handled: ColorMode, ColorModeProvider, ColorModeChangeHandler, PortalProvider, usePortal, ThemeNameProvider, ThemeNameChangeHandler, THEME_IDS, LegacyThemeId, LEGACY_THEME_IDS, useColorMode, useColorModeChange, useThemeNameChange, useThemeId.
Reference: provider-imports migration recipe in @grafana/design-catalog.
Individual codemods land per-migration use case (see plans/codemod-architecture.md in the source tree).
Cross-file subcommands
The recommended entry point is the
migrate-iconsClaude Code skill (see below). It orchestrates every subcommand in this section, including the per-fileicon-migration, with the right pause points for human review. The subcommands below are documented for completeness and for power users / CI scripts that want to drive individual steps directly.
The cross-file icon migration runs in three steps after icon-migration --dry-run has flagged the per-file manual-review list:
- Schema discovery —
icon-prop-surveyperforms TypeScript-aware (ts-morph) analysis and emits a JSON plan of every schema endpoint that flowsIconNameinto a non-static<Icon name={…}>site. - Plan refinement — the
migrate-iconsskill reads the survey output, applies a fixed decision framework to each endpoint (apply/review/skip), and writes a refined plan. - Apply —
icon-prop-applyreads the refined plan and writes the source edits: type rewrites, JSXname=→component=swaps, literal supplier substitutions, and import management.
icon-prop-survey
Phase 2 of the cross-file icon migration. Walks the supplied paths with TypeScript-aware analysis, finds every <Icon name={X} /> JSX site whose X isn't a static literal (those are handled by icon-migration), and traces X back to its declaring schema endpoint — either a function parameter (Pattern B) or an object-type property (Pattern C). For each endpoint, enumerates the supplier sites that flow values into it and classifies each as a static literal we can migrate mechanically or as a dynamic expression that needs context-aware judgement.
pnpm dlx @grafana/design-codemods icon-prop-survey apps/plugin/src --out icon-prop-plan.json
pnpm dlx @grafana/design-codemods icon-prop-survey apps/plugin/src # JSON to stdoutOutput is a JSON plan with this shape (abbreviated):
{
"version": 1,
"migration": "migrate-icons",
"generatedAt": "...",
"endpoints": [
{
"id": "src/components/MenuItem.tsx::icon",
"kind": "object-property",
"currentType": "IconName",
"proposedType": "SVGComponent",
"typeDeclaration": {
"file": "src/types.ts",
"line": 12,
"annotation": "IconName",
},
"jsxConsumers": [
{
"file": "src/Menu.tsx",
"line": 47,
"snippet": "<Icon name={item.icon} />",
},
],
"suppliers": [
{
"file": "src/menuData.ts",
"line": 8,
"value": "'bell-slash'",
"classification": {
"kind": "literal",
"legacyName": "bell-slash",
"componentName": "BellOff",
},
},
{
"file": "src/api.ts",
"line": 22,
"value": "response.iconKey",
"classification": {
"kind": "dynamic",
"reason": "non-literal: PropertyAccessExpression",
},
},
],
"decision": "undecided",
},
],
"untracedSites": [
{
"file": "src/legacy.tsx",
"line": 31,
"snippet": "<Icon name={icons[i]} />",
"reason": "unhandled expression kind: ElementAccessExpression",
},
],
}Flags:
--tsconfig <file>— path to the consumer'stsconfig.json. Defaults to walking up from the first input path.--out <file>/-o <file>— write the plan to a file instead of stdout.
The plan is consumed by the migrate-icons Claude Code skill (see below) to produce a refined plan, and then by icon-prop-apply to write the source edits.
Companion subcommand to icon-migration: run that first to handle every static-literal case, then run this to plan the cross-file work for what's left.
icon-prop-apply
Reads a refined plan (produced by the migrate-icons skill) and rewrites the source files it names. For each endpoint with decision: "apply":
- The type annotation at
typeDeclarationis rewritten toSVGComponent(e.g.IconNameor'foo' | 'bar'→SVGComponent);SVGComponentis added as a type import from@grafana/icons, andIconNameis dropped from@grafana/uiif it has no other references. - Every
<Icon name={X}>consumer is rewritten to<Icon component={X}>. TheIconwrapper import moves from@grafana/uito@grafana/icons. - Every supplier site classified as
literal(with a known migration target) has its string literal replaced by the icon component reference, with the component imported from@grafana/icons. JSX-attribute literals get wrapped in{}so the post-edit JSX stays valid. - Suppliers classified as
dynamic(or markeddecision: "review"per-site) are left alone and reported as residue for human cleanup.
pnpm dlx @grafana/design-codemods icon-prop-apply icon-prop-plan.refined.json
pnpm dlx @grafana/design-codemods icon-prop-apply icon-prop-plan.refined.json --dry-runPer-endpoint freshness checks abort the endpoint atomically if any site has drifted from the plan (type annotation differs, JSX <Icon name=…> no longer present, supplier literal moved, etc.). Half-migrating across files is never safe — the type graph would break. Drifted endpoints are reported as stale-plan; re-running icon-prop-survey followed by the migrate-icons skill refreshes them.
Re-running on already-migrated source is a no-op: every site reports as already-applied and no writes happen.
Flags:
--tsconfig <file>— path to the consumer'stsconfig.json. Defaults to walking up from the plan file's directory.--dry-run/-d— report what would change without writing.--verbose/-v— log each changed file to stderr.
Bundled Claude Code skills
This package ships skill files alongside its CLI so engineers can drive the icon-migration workflow from Claude Code in their own checkout, without needing to copy skill prompts around manually.
install-skill
Installs a bundled skill into the consumer repo's .claude/skills/:
pnpm dlx @grafana/design-codemods install-skill migrate-icons
pnpm dlx @grafana/design-codemods install-skill --listAfter installation the engineer can invoke the skill in Claude Code as /migrate-icons. The same skill content is also surfaced by the @grafana/design-mcp server, so consumer repos using that MCP server can invoke it without the local-install step.
Flags:
--list— print the available bundled skills.--dest <path>— install elsewhere than./.claude/skills/.
migrate-icons skill
End-to-end orchestration of the @grafana/ui <Icon name=…> → @grafana/icons <Icon component=…> migration. The skill is the recommended entry point — it drives every subcommand in the right order, pauses for human confirmation at the writes, and applies the cross-file decision framework on the survey output without the engineer having to read it manually.
Five phases, mapping to the underlying subcommands:
- Preconditions — confirm the repo has
Iconimports from@grafana/ui, a reachabledesign-codemodsCLI, and atsconfig.json. - Static-literal pass —
design-codemods icon-migration <path>(dry-run first, then write). - Cross-file survey —
design-codemods icon-prop-survey <path> --out icon-prop-plan.jsonif Phase 2 reporteddynamic-nameresidue. - Decision framework + apply — the skill annotates each endpoint with
apply/review/skipper a fixed table (loosestringtypes skipped withmanualGuidance, narrow string-literal unions promoted toSVGComponent, cross-package endpoints deferred, etc.), writesicon-prop-plan.refined.json, then runsdesign-codemods icon-prop-apply(dry-run first, then write). - Verify — type-check, grep for leftover
<Icon name={…}>sites and@grafana/uiIconimports.
The full prompt lives at skills/migrate-icons/SKILL.md; sample input + refined plans for the three main shapes (all-literal, narrowed-union, loose-type) live under skills/migrate-icons/examples/.
Invoke in a Claude Code session in the consumer repo:
/migrate-iconsIf the @grafana/design-mcp server is configured in the consumer's .mcp.json, the skill is also reachable without install-skill — the agent finds it via search / list_design_skills against the design MCP server, then drives it inline.
Library
import {
migrations,
type LegacyIconName,
type IgnoredLegacyIconName,
} from '@grafana/design-codemods';migrations— typedRecord<Exclude<LegacyIconName, IgnoredLegacyIconName>, IconName>mapping every@grafana/uiicon-name string with a resolved upgrade target to its canonical@grafana/iconsPascalCase component. Re-exported verbatim from@grafana/icons/migrations.LegacyIconName— the legacy icon-name union (verbatim copy of@grafana/ui'sIconNamere-export from@grafana/data).IgnoredLegacyIconName— the explicit list of legacy names with no migration target (google-hangouts-alt,hipchat).
Codemod authors should consume the table from here rather than reaching for the @grafana/icons/migrations subpath directly — that subpath is reserved for this package and the in-editor ESLint rule.
License
Apache-2.0
