ITADN
jong-kyung/tw-migrate
README.md

tw-migrate

Migrate static React/Next.js, Vue 3 SFC, and HTML stylesheet references to Tailwind v4 utilities.

Prerequisites

Install and configure Tailwind CSS v4 in the package you want to migrate by following the official installation guide. The package must have a CSS entry that loads Tailwind CSS, usually with @import "tailwindcss";, so tw-migrate can validate generated utilities against the project's Tailwind configuration.

Usage

Run it in the package you want to migrate — no separate tw-migrate install step:

npx tw-migrate                                      # Migrate the current package
npx tw-migrate --dry-run                            # Preview the current package
npx tw-migrate path/to/Button.module.scss           # Migrate one stylesheet
npx tw-migrate path/to/Button.module.scss --dry-run # Preview one stylesheet
npx tw-migrate --workspaces                         # Migrate every package

pnpm dlx tw-migrate and yarn dlx tw-migrate take the same arguments.

The CLI applies changes by default; --dry-run prints the diff without touching files. Pass --tailwind-css path/to/globals.css when the current package has multiple Tailwind entries. --force skips package groups that fail discovery or input parsing; plan-integrity and write failures always stop the run.

Run from a clone

The migration planner is a native addon, so a clone needs one build before the CLI runs:

vp install                    # or: pnpm install
vp run build:debug            # compiles the addon for your platform
node src/bin.ts --help

Then use node src/bin.ts wherever the commands above use npx tw-migrate. The CLI always migrates the package it is run from, so to migrate a different project, run the built entrypoint by absolute path from that project's directory:

cd ../some-app
node /path/to/tw-migrate/src/bin.ts

Current support

  • .css, .scss, .sass, and .less stylesheets
  • SCSS/Sass/Less values evaluated with the target project's installed compiler; ambiguous mixin and partial origins are retained
  • .js, .jsx, .ts, and .tsx source files
  • static .html literal class/id attributes scoped by local external stylesheet links (link-level print media supported; other link media conditions are retained)
  • Vue 3 .vue files: literal template class/id attributes and static string/template :class bindings matched against inline CSS, SCSS, Sass, and Less <style scoped> blocks plus statically imported stylesheets, parsed and compiled with the target project's dependencies
  • direct <script setup> component imports can prove child-root and caller class surfaces across SFCs; unresolved components, exported roots, non-static bindings, and callers outside that graph retain scoped rules
  • unscoped Vue styles migrate only in private single-source packages where their global usage is closed; broader unscoped surfaces retain with warnings
  • Vue <style module> blocks migrate against direct :class="$style.x" member bindings; any other $style/useCssModule appearance or a named module retains the block
  • direct CSS Module members, static template literals, and static expression literals
  • global className and id literals
  • React/Next.js className logical (&&, ||, ??) and conditional (?:) expressions: string and direct CSS Module member result leaves migrate without evaluating their conditions, null/undefined/false/empty-string results are warning-free, and unsupported result leaves retain with a warning while supported siblings still migrate
  • multi-compound CSS Module selectors whose element relationships are proven from the JSX graph
  • common state pseudo-classes, global arbitrary descendant variants, and conditional at-rules (@media, @supports, @container, @starting-style)
  • the tier-1 property mapping families with shorthand/longhand normalization
  • exact Tailwind theme tokens and breakpoints with arbitrary-value fallback
  • generated candidates are compiled against the project's Tailwind entry; failures retain the source rule
  • batch migration of every stylesheet in a package, and --workspaces runs across packages
  • CSS Module cleanup when every reference is safely migrated

Everything outside this subset is retained and reported with one of the warning codes below.

Warning codes

CodeMeaning
aliased-css-module-referenceA CSS Module class is aliased to a local binding, so the module is retained.
batch-stylesheet-conflictUtilities generated from different stylesheets conflict on the same JSX element, so the contributing rule is retained.
candidate-compilation-failureA generated candidate did not compile under the project's Tailwind entry, so its rule is retained.
component-class-targetA child component's root element can carry classes a Vue scoped rule matches, so the rule is retained.
computed-css-module-referenceA computed CSS Module access cannot be verified, so the module is retained.
cross-package-stylesheet-linkA linked stylesheet is owned by another package, so it is not analyzed outside workspace mode.
css-module-composesThe rule uses or is targeted by composes, so it is retained.
dynamic-class-nameA className value or expression result is dynamic, so that class site cannot be migrated.
dynamic-html-attributeAn HTML attribute is not a safely writable quoted literal, so the element cannot be migrated.
dynamic-template-classA dynamic class binding makes a Vue template's class set unprovable, so its scoped rules are retained.
existing-tailwind-conflictA generated utility may conflict with a Tailwind class already on the element.
inferred-preprocessor-sourceA linked CSS file was matched to a uniquely named preprocessor source file.
module-utilities-conflictUtilities generated from different module classes on one element overlap, so their rules are retained.
non-classname-css-module-referenceA CSS Module class is used outside a supported className, so the module is retained.
open-root-fallthroughA parent component can merge classes onto a Vue SFC's single root element, so its scoped rules are retained.
preprocessor-style-blockA Vue <style> block uses a preprocessor language outside the supported SCSS, Sass, and Less set.
rebuild-requiredA preprocessor entry was migrated; rebuild it to refresh its generated CSS.
reference-only-css-module-consumerA reference-only (non-writable) source uses the CSS Module, so it is retained.
retained-global-ruleGlobal CSS is never deleted automatically.
shadowed-scoped-ruleOther package CSS also targets a class a Vue scoped rule matches, so the rule is retained to preserve the cascade.
shared-preprocessor-sourceA Sass partial must be analyzed through every consuming entry, so it is retained.
unproven-css-module-relationshipA compound selector's element relationship could not be proven for every usage.
unproven-script-referenceAn inline script names a CSS Module class, so the module is retained.
unproven-source-mapA generated rule does not map uniquely to one authored source rule, so it is retained.
unresolved-selector-targetNo exclusively supported className references were found for the rule.
unscoped-style-blockAn unscoped Vue style does not have a closed private single-source usage surface, so it is retained.
unsupported-animationThe animation references keyframes that cannot be converted.
unsupported-at-ruleThe rule contains or sits inside an at-rule outside the supported set.
unsupported-container-queryThe @container condition has no Tailwind variant equivalent.
unsupported-css-module-referenceThe CSS Module has an import or reference that cannot be migrated safely.
unsupported-declarationA declaration is outside the supported property subset.
unsupported-html-baseA remote or unrepresentable base URL prevents safe stylesheet link resolution.
unsupported-html-stylesheet-linkOnly local package stylesheet links are analyzed.
unsupported-important!important declarations are not migrated.
unsupported-link-mediaA stylesheet link or @import media condition cannot be represented safely.
unsupported-media-queryThe @media condition has no Tailwind variant equivalent.
unsupported-nested-at-ruleA nested conditional at-rule could not be fully converted.
unsupported-overlapShorthand and longhand declarations overlap in a way that cannot be normalized.
unsupported-rule-contentThe rule contains non-declaration content that cannot be converted.
unsupported-selectorThe selector is outside the supported subset.
unsupported-sfc-blockA Vue SFC or one of its blocks cannot be analyzed safely, so it is retained.
unsupported-starting-styleThe @starting-style condition could not be converted.
unsupported-supports-queryThe @supports condition has no Tailwind variant equivalent.
unsupported-valueA declaration value cannot be represented as a Tailwind utility.
unsupported-vue-versionThe project's Vue version is not Vue 3, so its SFCs are retained.

See the core RFC and preprocessor/HTML RFC for the complete design and remaining scope.

Testing the packaged CLI

Run vp run test:snapshots to build, pack, install, and test the published CLI shape against the cross-platform snapshot corpus. The command uses the npm registry and performs one shared install per test process. See crates/snapshots/README.md for targeted runs and snapshot review commands.

Testing browser ecosystem compatibility

Build the current-platform package and install Chromium before a focused browser run:

vp run build
vp run artifacts
vp exec playwright install chromium
vp run test:ecosystem --case react-vite-css

Use --case production-react-vite-css for the installed CLI production-build smoke, or --all for all twelve controlled runtime/stylesheet cells. The default vp run test and packaged snapshots remain browser-free.

Pinned external projects run only in the Ecosystem browser GitHub Actions workflow on main, manual dispatch, or a pull request carrying the test:e2e label. The workflow checks them out under the runner's temporary directory without credentials or secrets; there is intentionally no contributor-facing local external command.

On failure, each OS/case job uploads only its bounded phase ledger, computed-style captures, screenshots, migration output, source diff, and registry/install/build/server logs. See the browser ecosystem RFC for immutable external evidence and the manifest, isolation, and oracle contracts.