npm.io
0.3.0 • Published 7h agoCLI

@grafana/design-codemods

Licence
Apache-2.0
Version
0.3.0
Deps
4
Size
637 kB
Vulns
0
Weekly
0

@grafana/design-codemods

Codemod tooling for migrating consumers onto @grafana/design packages.

This is the home of:

  • A design-codemods CLI for bulk source rewrites (the heavy-lifting jscodeshift transforms).
  • Re-exports of any migration tables that codemods consume — most notably the icon migrations table 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-codemods

CLI

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-run

Stage A scope (this codemod):

  • Static literal name values (string literal or JSX-expression-wrapped string literal).
  • Adds the matched component and Icon to @grafana/icons, merging into an existing import where present.
  • Removes Icon from @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 name prop.
  • IgnoredLegacyIconName values (google-hangouts-alt, hipchat).
  • Unknown legacy name (not in the migrations table).
  • Aliased Icon import (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-run

The 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-run

Names 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-icons Claude Code skill (see below). It orchestrates every subcommand in this section, including the per-file icon-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:

  1. Schema discoveryicon-prop-survey performs TypeScript-aware (ts-morph) analysis and emits a JSON plan of every schema endpoint that flows IconName into a non-static <Icon name={…}> site.
  2. Plan refinement — the migrate-icons skill reads the survey output, applies a fixed decision framework to each endpoint (apply / review / skip), and writes a refined plan.
  3. Applyicon-prop-apply reads the refined plan and writes the source edits: type rewrites, JSX name=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 stdout

Output 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's tsconfig.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 typeDeclaration is rewritten to SVGComponent (e.g. IconName or 'foo' | 'bar'SVGComponent); SVGComponent is added as a type import from @grafana/icons, and IconName is dropped from @grafana/ui if it has no other references.
  • Every <Icon name={X}> consumer is rewritten to <Icon component={X}>. The Icon wrapper import moves from @grafana/ui to @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 marked decision: "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-run

Per-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's tsconfig.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 these workflows from Claude Code in their own checkout, without needing to copy skill prompts around manually. The same skills are surfaced by the @grafana/design-mcp server, so any repo with that MCP server configured can invoke them without a local install.

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 --list

After 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:

  1. Preconditions — confirm the repo has Icon imports from @grafana/ui, a reachable design-codemods CLI, and a tsconfig.json.
  2. Static-literal passdesign-codemods icon-migration <path> (dry-run first, then write).
  3. Cross-file surveydesign-codemods icon-prop-survey <path> --out icon-prop-plan.json if Phase 2 reported dynamic-name residue.
  4. Decision framework + apply — the skill annotates each endpoint with apply / review / skip per a fixed table (loose string types skipped with manualGuidance, narrow string-literal unions promoted to SVGComponent, cross-package endpoints deferred, etc.), writes icon-prop-plan.refined.json, then runs design-codemods icon-prop-apply (dry-run first, then write).
  5. Verify — type-check, grep for leftover <Icon name={…}> sites and @grafana/ui Icon imports.

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-icons

If 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.

slack-issue skill

Turns one or more Slack post links (plus optional free-text context) into a well-formed GitHub issue. Unlike the migration skills, this one isn't tied to the design system — it files the issue against the repository you're working in by default (resolved via gh repo view), or a repo you name in the context, so it's useful in any consumer repo that has this package or the @grafana/design-mcp server installed. It reads the source thread through the Slack MCP server, synthesizes the ask, drafts a titled issue with attribution back to Slack, and confirms with you before creating it with gh.

The full prompt lives at skills/slack-issue/SKILL.md. It is generated from the monorepo's hatch source (.hatch/_skills/slack-issue/SKILL.md) by scripts/sync-design-codemods-skills.mjs so the in-repo /slack-issue command and the bundled copy stay in lockstep — edit the hatch source, not the bundled file.

Prerequisites: an authenticated Slack MCP server (to read the threads) and gh with access to the target repo.

/slack-issue <slack-permalink(s)> [additional context]

Library

import {
  migrations,
  type LegacyIconName,
  type IgnoredLegacyIconName,
} from '@grafana/design-codemods';
  • migrations — typed Record<Exclude<LegacyIconName, IgnoredLegacyIconName>, IconName> mapping every @grafana/ui icon-name string with a resolved upgrade target to its canonical @grafana/icons PascalCase component. Re-exported verbatim from @grafana/icons/migrations.
  • LegacyIconName — the legacy icon-name union (verbatim copy of @grafana/ui's IconName re-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