Migrate from Shoelace
Cordwainer Elements is a rename-and-retheme fork of
Shoelace: same component shapes and attributes, different tag names (sl- → cw-), CSS
custom property namespace (--sl- → --cw-), custom event names (sl- →
cw-), and TypeScript class/type names (Sl* → Cw*) — including for
anyone who’s built their own components on top of Shoelace’s. Adopting Cordwainer in an existing Shoelace
project means finding and renaming all four by hand, across every file type in the project.
@cordwainer/cw-migrate
(source) is a standalone CLI that automates it. It’s a tool you run directly against a project you’re migrating —
not something you add as a dependency of that project.
Status: 1.0.0, stable. Every command below is implemented, tested, and has
been run end-to-end against a production Shoelace + Lit codebase before this release — see
Real-world testing below.
Why not find-and-replace?
Naive find-and-replace isn’t safe here — sl-button and sl-button-group need to be
told apart, and a closing tag has to match its opener. cw-migrate uses a real parser for each
domain it rewrites (parse5 for markup, PostCSS for stylesheets, ts-morph for
TypeScript/JSX) instead of regex, so it understands the file it’s rewriting rather than pattern-matching
text.
Quick Start
Run it directly with npx — no separate install step:
npx @cordwainer/cw-migrate <path>
With no subcommand, cw-migrate scans first, then rewrites every domain in order and prints a
summary of what changed.
# Preview every change without writing anything npx @cordwainer/cw-migrate ./my-project --dry-run # Actually rewrite the project (requires a clean working tree) npx @cordwainer/cw-migrate ./my-project
cw-migrate refuses to run against a project with uncommitted changes unless you pass
--force — git is the assumed rollback path if something looks wrong afterward.
What it rewrites
Each of these is also runnable on its own (e.g. cw-migrate elements <path>), for anyone
who’d rather convert incrementally than all at once:
-
Elements — custom element tags (
<sl-button>→<cw-button>) across HTML, JSX/TSX, Vue, and Svelte, plus Lithtml/svgtagged templates in.ts/.js. Also renamessl-*CSS type selectors (sl-button { ... }→cw-button { ... }) in stylesheets andcss/styled-components template literals, andsl-*selector strings passed to.querySelector(...)/.querySelectorAll(...)/.closest(...)/.matches(...) -
Tokens — CSS custom properties (
--sl-color-primary-600→--cw-color-primary-600) in stylesheets, inlinestyleattributes (including inside Lithtml/svgtagged templates), JSX style object props, andstyled-components/emotion template literals -
Events — custom event names (
sl-change→cw-change) inaddEventListenercalls and framework event bindings (Vue, Angular, the React wrapper) -
TypeScript references — exported class/type identifiers (
SlButton→CwButton) everywhere they’re referenced, not just imported - Imports — JS/TS import specifiers and CDN URLs
-
package.json— swaps the@shoelace-style/shoelacedependency for@cordwainer/cw-elementsat a version you choose (run separately, since the tool can’t safely guess which version you want)
You can also run cw-migrate detect <path> on its own at any time — it scans and reports
every Shoelace reference found without writing anything, useful as a CI check or just to see the scope of a
migration before starting.
For the full command reference and flags, see the project README. Version history and the published package itself are on npm.
Real-world testing
cw-migrate has been run end-to-end against
GoogleChrome/chromium-dashboard
(chromestatus.com) — a production Lit + TypeScript app with 80 Shoelace-referencing files — as its first
real-world validation, ahead of this stable release. Sample migration branch and write-up:
matthandus/chromium-dashboard-migrate-shoelace-to-cordwainer#1.
That test surfaced and fixed four gaps in the tool itself, all shipped in this release:
-
elementswasn’t scanning.ts/.jsfiles at all, sosl-*tags inside Lithtmltagged templates — the majority of this project’s Shoelace usage — went untouched.detect’s element-match count went from 2 to 1102 after the fix. -
No command renamed
sl-*CSS type selectors (sl-button { ... }), only--sl-*custom properties — left as-is, this would have silently broken component styling post-migration. -
Plain
style="..."attributes inside Lit templates weren’t covered by the custom-property rewrite. -
sl-*selector strings passed to.querySelector(...)/.querySelectorAll(...)/.closest(...)/.matches(...)weren’t renamed, which would have left tests silently querying for elements that no longer exist.
Post-fix, the migrated result installs and builds cleanly (npm install, tsc,
rollup), with zero Shoelace tag/token/event/import/type references remaining in source.
Run cw-migrate on your own Shoelace project and hit something it doesn’t handle?
Open a work item
— real-world test cases like the one above are exactly how gaps like these get found.
Beyond the Shoelace migration
Once the initial wave of Shoelace-to-Cordwainer conversions is done, cw-migrate doesn’t go away
— it becomes the permanent home for codemods that ship alongside future Cordwainer Elements major-version
bumps, the same role tools like react-codemod or Angular’s ng update schematics
play for their own ecosystems.