modern-cmdk-codemod 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Albert Badalov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,144 @@
1
- # Temporary Holding Version
1
+ # modern-cmdk-codemod
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Codemods that migrate code written for [cmdk](https://github.com/dip/cmdk) to [modern-cmdk](https://www.npmjs.com/package/modern-cmdk). Four transforms, one per run, each reporting every site it cannot migrate instead of guessing.
4
+
5
+ Nothing to install: run it with `npx` (or `pnpm dlx`) from the root of a git work tree.
6
+
7
+ ```bash
8
+ npx modern-cmdk-codemod <transform> <dir|glob>
9
+ ```
10
+
11
+ Requires Node >= 26.4.0 (`engines.node` of this package).
12
+
13
+ ## Safe first run: a dry run
14
+
15
+ `--dry-run` needs no git tree, writes nothing and prints a unified diff of every file that would change:
16
+
17
+ ```bash
18
+ npx modern-cmdk-codemod --dry-run should-filter ./src
19
+ ```
20
+
21
+ ## Real runs need a clean git tree
22
+
23
+ A run that writes needs a git work tree in which every matched file is committed. If a matched file is modified, staged, deleted or untracked, lies outside the work tree, or git cannot be found, nothing is written and the exit code is `2`:
24
+
25
+ ```text
26
+ error: refusing to write: 1 matched file has uncommitted changes:
27
+ src/Palette.tsx
28
+ Commit or stash them, or pass --force to write anyway (--dry-run previews without writing).
29
+ ```
30
+
31
+ Outside a git tree: `error: refusing to write outside a git work tree: nothing could undo it. Pass --force to write anyway.`
32
+
33
+ So every real run below starts from a committed tree, and you commit after each transform so the next run starts clean and each step is a reviewable diff. To write into a tree that is not clean, or outside git, pass `--force`; a bad run then cannot be undone with git.
34
+
35
+ ```bash
36
+ git status --short # empty: everything is committed
37
+ npx modern-cmdk-codemod should-filter ./src
38
+ git commit -am "codemod: should-filter"
39
+ npx modern-cmdk-codemod forward-ref ./src
40
+ git commit -am "codemod: forward-ref"
41
+ npx modern-cmdk-codemod data-attrs ./src
42
+ git commit -am "codemod: data-attrs"
43
+ npx modern-cmdk-codemod import-rewrite ./src
44
+ git commit -am "codemod: import-rewrite"
45
+ ```
46
+
47
+ ```bash
48
+ # a tree that is not clean, or not in git: you accept that nothing can undo the run
49
+ npx modern-cmdk-codemod --force should-filter ./src
50
+ ```
51
+
52
+ Run the transforms in this order. `import-rewrite` goes last: `should-filter` adapts a cmdk `filter` function only on elements bound to a `'cmdk'` import, and once the import reads `'modern-cmdk/react'` a cmdk filter can no longer be told from a migrated one.
53
+
54
+ ## Which files are read
55
+
56
+ A directory argument covers every `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.css` and `.scss` file below it. Files and directories whose names start with a dot (`.storybook/`, `src/.palette.tsx`) are not matched by a directory argument or by a wildcard (`*` or `**`). Name them with a pattern whose segment starts with the dot, such as `"./.storybook/*.tsx"` or `"./src/.*.tsx"`, or give the dot directory itself as the directory argument (`./.storybook`). A glob works too; quote it so your shell passes it to the CLI:
57
+
58
+ ```bash
59
+ npx modern-cmdk-codemod --dry-run data-attrs "./src/**/*.{tsx,css}"
60
+ ```
61
+
62
+ The codemod skips `node_modules`, `dist`, `build`, `.next`, `.git`, `coverage`, `.turbo`, `out`, `storybook-static` and `.vercel`, every path your `.gitignore` covers (asked of `git check-ignore`) and every file reached through a symlinked directory, and every file or directory whose name starts with a dot (unless a pattern segment starts with that dot or a directory argument names that directory, such as `./.storybook`).
63
+
64
+ ## The four transforms
65
+
66
+ | Transform | Scope and limits |
67
+ |---|---|
68
+ | `should-filter` | On `Command`, `Command.Dialog`, `CommandRoot` and `CommandDialog` imported from `'cmdk'` or `'modern-cmdk/react'`: `shouldFilter={false}` becomes `filter={false}`, `shouldFilter` and `shouldFilter={true}` are removed, `shouldFilter={expr}` becomes `filter={expr ? undefined : false}`. On an element bound to `'cmdk'` only, a cmdk `filter` function (`(value, search, keywords) => number`) is wrapped in an adapter to `(item, query) => number \| false`. A `filter` on an element bound to `'modern-cmdk/react'` is left alone without a report. Other components named `Command`, and any other element, are not touched. |
69
+ | `forward-ref` | Unwraps `forwardRef(...)` from `react` into a function component that takes `ref` as a prop (React 19), keeping the props and ref types. |
70
+ | `data-attrs` | Renames cmdk's `cmdk-*` attributes and `--cmdk-list-height` in JSX attributes, in selectors inside JS/TS string and template literals, and in `.css` and `.scss` files. Next to a cmdk attribute it rewrites the state attributes (`[data-selected="true"]` becomes `[data-active]`, `[data-selected="false"]` becomes `:not([data-active])`, and the same for `data-disabled`), and the Tailwind value variants on cmdk elements (`data-[selected=true]:` becomes `data-active:`, `data-[disabled=true]:` becomes `data-disabled:`). |
71
+ | `import-rewrite` | Rewrites imports, re-exports, `import()`, `require()` and `vi.mock` or `jest.mock` of `'cmdk'` to `'modern-cmdk/react'`. `CommandRoot` and a default import become `Command`. |
72
+
73
+ ### What each transform reports
74
+
75
+ A report is printed as `<path>: <message>` on stderr, counted in the summary, and never changes the exit code; the site is left as it was for you to migrate by hand. These are the messages as the code writes them (`<...>` is filled in per site):
76
+
77
+ - `should-filter`: `<Command> at line N has both shouldFilter and filter; left unchanged for manual migration (...)`; `... has a filter value the transform cannot adapt; left unchanged for manual migration`; `... has a filter expression that may not be a function; left unchanged for manual migration`; `... has a filter expression that is computed where it stands (a call or other computed value), ...; left unchanged for manual migration: hoist it to a const and pass the name`; `<Command> at line N has a non-expression shouldFilter value; left unchanged for manual migration`; `... has an empty shouldFilter value; left unchanged for manual migration`.
78
+ - `forward-ref`: `forwardRef() without a render function: unwrap by hand`; `forwardRef(<name>): unwrap by hand` and `forwardRef(<expression>): unwrap by hand`; `forwardRef render function with more than two parameters: unwrap by hand`; `forwardRef props parameter is not an identifier or object pattern: unwrap by hand`; `forwardRef props pattern already has a 'ref' key: unwrap by hand`; `forwardRef ref parameter is not a plain identifier: unwrap by hand`; `forwardRef<T>() render function without parameters: unwrap by hand`; `forwardRef props pattern has a computed key that may be 'ref': unwrap by hand`; `forwardRef render function has a JSX spread and no ref type argument: unwrap by hand`; `forwardRef ref is annotated but the props type is unknown: unwrap by hand`.
79
+ - `data-attrs`: `<attr> has no modern-cmdk equivalent: <reason>; left unchanged` (`cmdk-label`, which cmdk renders as a visually hidden `<label>` while modern-cmdk puts the label on the root as `aria-label`, and `cmdk-item-select`, a DOM event that has no counterpart: use the item `onSelect` prop); `<selector> beside a cmdk attribute cannot be migrated automatically: ...; left unchanged` for a state attribute with a value other than `"true"` or `"false"`; `<selector> under a rule whose selectors only partly hold a cmdk attribute cannot be migrated automatically; left unchanged` (SCSS); `<variant>: on a cmdk element cannot be migrated automatically: ...; left unchanged` (Tailwind `data-[selected]:` and `data-[disabled]:` presence forms); `<attr> is a cmdk attribute name outside a selector; modern-cmdk renders <data-command-attr>; left unchanged` (a bare attribute name in a string); `<variant>: has no modern-cmdk equivalent, <presence> is a presence attribute with no false value (use not-<presence>:); left unchanged` (Tailwind `data-[selected=false]:` and `data-[disabled=false]:`).
80
+ - `import-rewrite`: `<name> has no modern-cmdk/react export; left on the cmdk import` (`defaultFilter`, `CommandProps`); `useCommandState was rewritten, but its state is modern-cmdk's CommandState, not cmdk's; review the selector`; `'cmdk/<subpath>' is a cmdk subpath with no modern-cmdk/react counterpart; left unchanged`; `declare module 'cmdk' was left unchanged; move the declaration to 'modern-cmdk/react' by hand`; `the mock factory for 'cmdk' names ..., which modern-cmdk/react does not export under that name; the factory body is not rewritten, check it by hand`; `<name> has no modern-cmdk/react export; left on the cmdk export`; `<name> has no modern-cmdk/react export; used through <namespace or binding>`; `<name> has no modern-cmdk/react export; used through import('cmdk')`; `<name> has no modern-cmdk/react export under that name; destructured from the cmdk module`; `export * from 'cmdk' re-exported names that modern-cmdk/react lacks (CommandRoot, defaultFilter); re-export the modern-cmdk/react names by hand` (and the same for `export * as K from 'cmdk'`).
81
+
82
+ A real run from a committed tree whose `./src` holds only `src/palette.css`, with `[cmdk-root]`, `[cmdk-item][data-selected="true"]` and `[cmdk-label]` rules, printed:
83
+
84
+ ```text
85
+ $ npx modern-cmdk-codemod data-attrs ./src
86
+ src/palette.css: line 3: cmdk-label has no modern-cmdk equivalent: cmdk renders a visually hidden <label>; modern-cmdk puts the label on the root as aria-label and renders no label element; left unchanged
87
+ changed src/palette.css
88
+ 1 changed, 0 unchanged, 0 errors, 1 reported
89
+ ```
90
+
91
+ which left `[data-command-root] { ... }`, `[data-command-item][data-active] { ... }` and the untouched `[cmdk-label] { display: none; }`.
92
+
93
+ ## Options and exit codes
94
+
95
+ Pasted from `node dist/cli.mjs --help` of this package's build:
96
+
97
+ ```text
98
+ Usage: modern-cmdk-codemod <transform> <glob|dir> [options]
99
+
100
+ modern-cmdk-codemod runs with --strict unless --lenient is given. A strict run without --dry-run
101
+ refuses to write into a git tree with uncommitted changes (or outside a git tree) unless --force is given.
102
+
103
+ Transforms:
104
+ import-rewrite Rewrites cmdk imports to modern-cmdk/react
105
+ data-attrs Converts [cmdk-*] selectors to [data-command-*]
106
+ forward-ref Removes React.forwardRef wrappers (React 19)
107
+ should-filter Converts shouldFilter prop to filter
108
+
109
+ Options:
110
+ --dry-run List the files that would change; write nothing
111
+ --strict Exit 2 on usage errors (incl. unknown options), 1 on file errors
112
+ --concurrency=N Files processed at once (default: min(8, available cores))
113
+ -h, --help Show this help
114
+ --lenient Keep the lenient exit codes below (cannot be combined with --strict) and skip the clean-git-tree check (modern-cmdk-codemod only)
115
+ --force Skip the clean-git-tree check (modern-cmdk-codemod only; modern-cmdk ignores it)
116
+ -v, --version Print the package name and version
117
+
118
+ Exit codes:
119
+ strict 0 success (reports included); 1 a file errored, no file matched or the run failed;
120
+ 2 usage error (unknown or malformed option, unknown transform, missing argument,
121
+ --strict with --lenient) or a refused dirty tree (modern-cmdk-codemod only)
122
+ lenient 0 for --help, missing arguments, usage errors and per-file errors; 1 for an unknown
123
+ transform, no matching files and an uncaught error
124
+
125
+ Files run on worker threads when --concurrency is above 1; --concurrency=1 runs them on the main thread.
126
+ A directory argument covers its .ts .tsx .mts .cts .js .jsx .mjs .cjs .css .scss files (dot paths excluded).
127
+ --dry-run also prints a unified diff of every file that would change.
128
+
129
+ Node >= 26.4.0 (the engines.node of modern-cmdk-codemod)
130
+ ```
131
+
132
+ `modern-cmdk-codemod` is a new CLI with no earlier contract, so it is strict by default. `--lenient` (which also skips the git check) restores the exit codes of the deprecated `modern-cmdk` bin.
133
+
134
+ ## Relation to the deprecated `modern-cmdk` bin
135
+
136
+ `modern-cmdk` still ships a `modern-cmdk` bin that runs the same four transforms, bundled. It is deprecated and leaves in modern-cmdk 2.0: it writes one deprecation warning to stderr per run, keeps the 1.x exit codes (lenient unless argv carries `--strict`) and 1.4's excludes (of the ten directories `modern-cmdk-codemod` skips, it skips only `node_modules`, `dist`, `build` and `.next`, and applies no `.gitignore`; like `modern-cmdk-codemod`, it reads only the ten script and stylesheet extensions from a directory, leaves out files and directories whose names start with a dot unless a pattern segment starts with that dot or a directory argument names that directory, descends no symlinked directory under `**` or a directory argument, reads no symlinked file and on Windows matches names case-insensitively; unlike it, it follows a symlinked directory that a pattern names or that a directory argument names, such as `"./src/link/*.tsx"` or `./src/link`), and makes no git check. Use `modern-cmdk-codemod` instead. The bundled codemod dependencies and their licences are listed in the `THIRD-PARTY-NOTICES.md` that `modern-cmdk` ships.
137
+
138
+ ## More
139
+
140
+ The [migration guide](https://abcrimson.github.io/modern-cmdk/guide/migration-from-cmdk.html) has the attribute table, the CSS, SCSS and Tailwind rules and every breaking change.
141
+
142
+ ## License
143
+
144
+ [MIT](https://github.com/ABCrimson/modern-cmdk/blob/main/LICENSE)
@@ -0,0 +1,43 @@
1
+ // src/print-options.ts
2
+ const TOKEN = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|`(?:[^`\\]|\\[\s\S])*`|(?<s>'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*")/g;
3
+ const SPECIFIER_CONTEXT = /(?:\bfrom|\bimport|\brequire\s*\(|\bimport\s*\()\s*$/;
4
+ function quoteOf(raw) {
5
+ if (raw.startsWith("'")) return "single";
6
+ if (raw.startsWith("\"")) return "double";
7
+ }
8
+ /**
9
+ * Print options that reproduce the source file's line terminator and quote style.
10
+ *
11
+ * - lineTerminator: CRLF when the source contains a CRLF, else LF.
12
+ * - quote: the quote of `sampleRaw` when it starts with one; otherwise the quote of the first
13
+ * import, export-from, require or dynamic-import specifier; otherwise the more frequent quote
14
+ * of the file's string literals, a tie giving double.
15
+ */
16
+ function printOptions(source, sampleRaw) {
17
+ const lineTerminator = source.includes("\r\n") ? "\r\n" : "\n";
18
+ const sampled = sampleRaw === void 0 ? void 0 : quoteOf(sampleRaw);
19
+ if (sampled !== void 0) return {
20
+ quote: sampled,
21
+ lineTerminator
22
+ };
23
+ let single = 0;
24
+ let double = 0;
25
+ let specifier;
26
+ for (const match of source.matchAll(TOKEN)) {
27
+ const raw = match.groups?.s;
28
+ if (raw === void 0) continue;
29
+ const quote = raw.startsWith("'") ? "single" : "double";
30
+ if (quote === "single") single++;
31
+ else double++;
32
+ if (specifier === void 0 && SPECIFIER_CONTEXT.test(source.slice(Math.max(0, match.index - 32), match.index))) specifier = quote;
33
+ }
34
+ if (specifier !== void 0) return {
35
+ quote: specifier,
36
+ lineTerminator
37
+ };
38
+ return {
39
+ quote: single > double ? "single" : "double",
40
+ lineTerminator
41
+ };
42
+ }
43
+ export { printOptions as t };