npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

magic-codemods

v1.1.6

Published

Migration codemods. magic-kebab: rename files to kebab-case and rewrite every import that pointed at them.

Readme

How it works

One binary today: magic-kebab, so unicorn/filename-case can be turned on in a repo without a week of hand-editing.

  1. --dry-run asks the repo's own oxlint which filenames break unicorn/filename-case and prints the plan: renames, skips, conflicts. Nothing changes.
  2. --write rewrites every specifier that resolves to a renamed file (imports, requires, mocks, tsconfig aliases), then moves each file through two git mvs via a temp name, so case-only renames survive case-insensitive APFS.
  3. You verify and commit the renames on their own. Git infers renames from content similarity at diff time, so a rename-only commit keeps git log --follow working.
pnpm exec magic-kebab --dry-run   # the plan; changes nothing
pnpm exec magic-kebab --write     # apply it

Install

pnpm add -D magic-codemods

Why it exists

magic-oxlint-config enables unicorn/filename-case at kebabCase in base, so every repo adopting the preset has a pile of Button.tsx and formatDate.ts to deal with at once. Doing that by hand is a large, error-prone diff in the middle of a migration that is already changing everything else. Doing it with find | xargs mv breaks every import in the repo and, on macOS, silently does nothing at all for the case-only renames.

Use it

Always in this order:

# 1. Land on a clean tree. The codemod refuses to run otherwise.
git status

# 2. Look at the plan. This changes nothing.
pnpm exec magic-kebab --dry-run

# 3. Read the SKIPPED, NEEDS REVIEW and CONFLICTS sections. Really read them.
#    Anything under CONFLICTS has to be resolved before --write does anything
#    useful for those files.

# 4. Apply.
pnpm exec magic-kebab --write

# 5. Verify, then commit as one rename-only commit.
pnpm exec tsc --noEmit && pnpm run lint && pnpm run test
git add -A && git commit -m "refactor: kebab-case filenames"

Commit the renames on their own. git log --follow survives this codemod because git infers renames from content similarity at diff time, and a commit that only renames gives it the easiest possible job. Mixing a refactor into the same commit is what breaks history.

Options

magic-kebab --help lists every flag. Positional arguments scope the run: magic-kebab --dry-run src/components.

Exit codes: 0 success, 1 refused (dirty tree, bad arguments, a --rename that matched nothing), the plan has conflicts, or --strict finds something to review.

--rename keys are full basenames

--rename zodI18n.ts=zod-i18n.ts works. --rename zodI18n=zod-i18n is an error. It used to be silently ignored, and the file was renamed to the codemod's own target instead. --rename exists for the files a human looked at and overruled. The message suggests the key you meant.

The same applies to a key naming a file the detector never reported: under --detect oxlint the preset already exempts __mocks__/AsyncStorage.ts, so --rename AsyncStorage.ts=... matches nothing and fails. Use --detect builtin if you want to force one of those.

Path aliases in a monorepo

The resolver reads paths from every tsconfig it can find: the repo root, then each package matched by pnpm-workspace.yaml's packages globs, then a generic */tsconfig.json / */*/tsconfig.json sweep. It used to look only at the run root (which in a monorepo usually has no tsconfig at all), print one line saying so, and then rewrite relative imports while leaving every @/... alias pointing at a file it had just renamed.

If an alias still cannot be resolved (declared only in a bundler config, say) and its last segment names a file being renamed, that import is printed under NEEDS REVIEW and --strict exits non-zero. Pass --tsconfig, repeatable, at the config that defines it.

--detect oxlint vs --detect builtin

The default asks the repo's own oxlint what is wrong and reads the unicorn(filename-case) diagnostics, including the rename target out of the diagnostic's own help text. That is the only way to be sure the codemod and CI agree: the repo's ignore list, its overrides, its ignorePatterns all apply for free, because the linter is the one answering.

--detect builtin applies this package's own copy of the rule to every tracked file. Use it before a repo has adopted the preset, or when a full lint is too slow. It knows nothing about that repo's exemptions, so it reports more, which is where the skip list below comes in. test/kebab.test.mjs generates a corpus, runs the real binary over it, and fails if the two ever disagree on a single name.

Do not try to speed the default up with oxlint -A all -D unicorn/filename-case. -D <rule> re-enables the rule with its default options and throws away the config's ignore list, so a run scoped that way reports every [postId].tsx in the repo.

What it rewrites

| Form | Handled | | --------------------------------------------------------------------- | ------------ | | import x from "./Button" / import type | rewritten | | export { x } from "./Button", export * from | rewritten | | import("./Button"), typeof import("./Button") | rewritten | | require("./Button"), require.resolve(...) | rewritten | | jest.mock, vi.mock, requireActual, importActual, and the rest | rewritten | | tsconfig paths aliases (@/components/Button) | rewritten | | .js specifiers standing in for .ts files (NodeNext) | rewritten | | import(./${name}) and other computed specifiers | reported | | bare string literals that resolve to a renamed file | reported | | an unresolvable @/ ~/ # alias naming a renamed file | reported | | moduleNameMapper, resolve.alias, bundler configs | reported | | package.json main / exports / bin, .md docs, YAML | reported |

The split is deliberate. A moduleNameMapper key is a regex whose escaping belongs to whoever wrote it, and a package.json exports path is a published contract. Guessing at either is how a codemod turns a lint fix into an outage, so those are printed under NEEDS REVIEW and left exactly as they were.

Bare string literals are the newest entry and the one that cost the most: an Expo config plugin (plugins: ["./plugins/withThing"]), the argument to a repo's own require-wrapper, a route manifest. None of those is a specifier to any AST pass, all of them resolve to a file, and the Expo case only fails on Linux/EAS (APFS resolves the stale path fine), so a migration verifies green locally and ships a broken build.

One invariant makes the rest tractable: directories never move. Only the basename stem changes, so only the last segment of any specifier is ever touched.

What it refuses to rename

Framework conventions where the filename is behaviour. These are also exempt in magic-oxlint-config, and the two lists have to agree: anything the codemod skips but the linter reports leaves a repo with an error that has no automated fix.

| Pattern | Why | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [postId].tsx, [[...x]].tsx | The bracketed text is a route parameter name (it becomes params.postId), so renaming it changes the route contract. Next.js, expo-router, TanStack Router. | | __mocks__/AsyncStorage.ts | jest and vitest match __mocks__/<x> against the module being mocked. The name belongs to the package. | | App.tsx | Bare RN's index.js imports ./App, and classic Expo points main at node_modules/expo/AppEntry.js whose import App from "../../App" no codemod can reach. |

A __mocks__/Button.ts sitting next to a Button.tsx is renamed, in lockstep with its module (that one mocks something the repo owns).

--rename Old.tsx=whatever.tsx overrides the target and is the one thing that gets past the skip list, for when a human has decided otherwise. It only overrides targets for files the detector already reported; a key naming anything else fails.

Rename targets come from oxlint

The target is taken verbatim from the diagnostic's help field (Rename the file to 'pascal-thing.ts'), so what you get is exactly what the linter asked for. Its word-splitting has some sharp corners:

| Before | After | | ---------------------- | ----------------------- | | HTTPServer.ts | http-server.ts | | parseURLQuery.ts | parse-url-query.ts | | MyComponent.test.tsx | my-component.test.tsx | | Theme.ios.ts | theme.ios.ts | | _Private.ts | _private.ts | | S3.ts | s-3.ts | | AppV2.ts | app-v-2.ts | | OAuth2Client.ts | o-auth2-client.ts |

The last three are ugly and they are what the rule wants. --dry-run is where you catch them; --rename S3.ts=s3.ts is how you fix them.

Two-step renames

macOS ships APFS case-insensitive, so Button.tsx and button.tsx are the same path. git mv Button.tsx button.tsx there is either refused as "destination exists" or, with -f, becomes a no-op that still updates the index, producing a commit that claims a rename the working tree never performed, and a file that only appears once someone checks out on Linux.

Every rename therefore goes through a third name, unconditionally:

git mv Button.tsx .magic-kebab-tmp-...
git mv .magic-kebab-tmp-... button.tsx

This is invisible in history. Git records no rename operation in a commit at all; it infers renames from content similarity when you ask for a diff. Two git mvs before one commit produce exactly one rename in that commit.

Programmatic use

import { runKebabCodemod, summarise } from "magic-codemods";

const result = runKebabCodemod({
  cwd: process.cwd(),
  paths: [],
  write: false,
  allowDirty: false,
  detect: "oxlint",
  tsconfigs: [],
  overrides: new Map(),
});

console.log(summarise(result));

isKebabCase, kebabifyBasename and skipReasonFor are exported too, for anything that needs to ask the same questions without running the whole codemod.