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.
@@ -0,0 +1,207 @@
1
+ import { t as printOptions } from "../chunk-print-options.mjs";
2
+ // src/transforms/should-filter.ts
3
+ const SOURCES = /* @__PURE__ */ new Map([["cmdk", "cmdk"], ["modern-cmdk/react", "modern-cmdk/react"]]);
4
+ const COMPONENTS = /* @__PURE__ */ new Set([
5
+ "Command",
6
+ "CommandRoot",
7
+ "CommandDialog"
8
+ ]);
9
+ const adapterSource = (item, query, score, argCount) => {
10
+ return `(${item}, ${query}) => { const ${score} = __fn(${[
11
+ `${item}.value`,
12
+ query,
13
+ `${item}.keywords === undefined ? undefined : [...${item}.keywords]`
14
+ ].slice(0, argCount).join(", ")}); return ${score} > 0 ? ${score} : false; }`;
15
+ };
16
+ function isFunctionLiteral(node) {
17
+ const type = node.type;
18
+ return type === "ArrowFunctionExpression" || type === "FunctionExpression";
19
+ }
20
+ function argCountOf(j, fn) {
21
+ if (!isFunctionLiteral(fn)) return 3;
22
+ if (fn.params.some((param) => param.type === "RestElement")) return 3;
23
+ if (j(fn).find(j.Identifier, { name: "arguments" }).size() > 0) return 3;
24
+ return Math.min(3, fn.params.length);
25
+ }
26
+ function freeName(name, used) {
27
+ let candidate = name;
28
+ while (used.has(candidate)) candidate += "_";
29
+ return candidate;
30
+ }
31
+ /**
32
+ * jscodeshift transform — migrates cmdk's shouldFilter/filter props on cmdk-bound elements.
33
+ */
34
+ function transform(fileInfo, api) {
35
+ const hasShouldFilter = fileInfo.source.includes("shouldFilter");
36
+ const hasFilter = fileInfo.source.includes("filter");
37
+ if (!hasShouldFilter && !hasFilter) return fileInfo.source;
38
+ const j = api.jscodeshift;
39
+ const root = j(fileInfo.source);
40
+ const bindings = /* @__PURE__ */ new Map();
41
+ let firstRaw;
42
+ root.find(j.ImportDeclaration).forEach((path) => {
43
+ const decl = path.node;
44
+ const origin = SOURCES.get(String(decl.source.value));
45
+ if (origin === void 0 || decl.importKind === "type") return;
46
+ firstRaw ??= decl.source.extra?.raw;
47
+ for (const spec of decl.specifiers ?? []) {
48
+ if (spec.type !== "ImportSpecifier") continue;
49
+ const imported = String(spec.imported.name);
50
+ if (!COMPONENTS.has(imported)) continue;
51
+ if (spec.importKind === "type") continue;
52
+ bindings.set(String(spec.local?.name ?? imported), origin);
53
+ }
54
+ });
55
+ if (bindings.size === 0) return fileInfo.source;
56
+ const originOf = (path) => {
57
+ const name = path.node.name;
58
+ let n;
59
+ if (name.type === "JSXIdentifier") n = name.name;
60
+ else if (name.type === "JSXMemberExpression" && name.object.type === "JSXIdentifier" && name.property.name === "Dialog") n = name.object.name;
61
+ else return;
62
+ const declaring = path.scope?.lookup(n);
63
+ if (declaring !== null && declaring !== void 0 && !declaring.isGlobal) return void 0;
64
+ return bindings.get(n);
65
+ };
66
+ let changed = false;
67
+ const typed = /.[cm]?tsx?$/.test(fileInfo.path);
68
+ root.find(j.JSXOpeningElement).forEach((path) => {
69
+ const origin = originOf(path);
70
+ if (origin === void 0) return;
71
+ const attrs = path.node.attributes ?? [];
72
+ const named = (n) => attrs.find((a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === n);
73
+ const shouldFilter = named("shouldFilter");
74
+ const filter = named("filter");
75
+ const line = path.node.loc?.start.line;
76
+ const where = `<${elementName(path.node)}> at line ${line ?? "?"}`;
77
+ if (shouldFilter?.type === "JSXAttribute" && filter !== void 0) {
78
+ api.report(`${where} has both shouldFilter and filter; left unchanged for manual migration (filter={false} replaces shouldFilter={false}; a filter takes (item, query) and returns a number or false)`);
79
+ return;
80
+ }
81
+ if (shouldFilter?.type === "JSXAttribute") {
82
+ changed = rewriteShouldFilter(j, api, path, shouldFilter, where) || changed;
83
+ return;
84
+ }
85
+ if (filter?.type === "JSXAttribute" && origin === "cmdk") changed = adaptFilter(j, api, filter, where, typed) || changed;
86
+ });
87
+ return changed ? root.toSource(printOptions(fileInfo.source, firstRaw)) : fileInfo.source;
88
+ }
89
+ function elementName(node) {
90
+ const name = node.name;
91
+ if (name.type === "JSXIdentifier") return name.name;
92
+ if (name.type === "JSXMemberExpression" && name.object.type === "JSXIdentifier") return `${name.object.name}.${name.property.name}`;
93
+ return "?";
94
+ }
95
+ function rewriteShouldFilter(j, api, path, attr, where) {
96
+ const value = attr.value;
97
+ const manual = (why) => {
98
+ api.report(`${where} has ${why}; left unchanged for manual migration`);
99
+ return false;
100
+ };
101
+ if (value === null || value === void 0) return remove(path, attr);
102
+ if (value.type !== "JSXExpressionContainer") return manual("a non-expression shouldFilter value");
103
+ const expr = value.expression;
104
+ if (expr.type === "JSXEmptyExpression") return manual("an empty shouldFilter value");
105
+ if (isBoolean(expr, true)) return remove(path, attr);
106
+ if (isBoolean(expr, false)) {
107
+ attr.name = j.jsxIdentifier("filter");
108
+ return true;
109
+ }
110
+ attr.name = j.jsxIdentifier("filter");
111
+ attr.value = j.jsxExpressionContainer(j.conditionalExpression(expr, j.identifier("undefined"), j.booleanLiteral(false)));
112
+ return true;
113
+ }
114
+ function isBoolean(expr, want) {
115
+ return (expr.type === "BooleanLiteral" || expr.type === "Literal") && expr.value === want;
116
+ }
117
+ function remove(path, attr) {
118
+ path.node.attributes = (path.node.attributes ?? []).filter((a) => a !== attr);
119
+ return true;
120
+ }
121
+ function isShouldFilterOutput(fn) {
122
+ return fn.type === "ConditionalExpression" && fn.consequent.type === "Identifier" && fn.consequent.name === "undefined" && isBoolean(fn.alternate, false);
123
+ }
124
+ function isAdapterOutput(fn) {
125
+ if (fn.type !== "ArrowFunctionExpression") return false;
126
+ const [first, second, ...rest] = fn.body.type === "BlockStatement" ? fn.body.body : [];
127
+ const [p0, p1, ...moreParams] = fn.params;
128
+ if (p0?.type !== "Identifier" || p1?.type !== "Identifier" || moreParams.length > 0) return false;
129
+ if (first?.type !== "VariableDeclaration" || first.kind !== "const") return false;
130
+ if (second?.type !== "ReturnStatement" || rest.length > 0) return false;
131
+ const [decl, ...moreDecls] = first.declarations;
132
+ if (decl?.type !== "VariableDeclarator" || moreDecls.length > 0) return false;
133
+ if (decl.id.type !== "Identifier" || decl.init?.type !== "CallExpression") return false;
134
+ const arg0 = decl.init.arguments[0];
135
+ if (arg0?.type !== "MemberExpression" || arg0.object.type !== "Identifier" || arg0.object.name !== p0.name || arg0.property.type !== "Identifier" || arg0.property.name !== "value") return false;
136
+ const id = decl.id.name;
137
+ const ret = second.argument;
138
+ return ret?.type === "ConditionalExpression" && ret.test.type === "BinaryExpression" && ret.test.operator === ">" && ret.test.left.type === "Identifier" && ret.test.left.name === id && ret.consequent.type === "Identifier" && ret.consequent.name === id && isBoolean(ret.alternate, false);
139
+ }
140
+ function isGuardedAdapterOutput(fn) {
141
+ return fn.type === "ConditionalExpression" && fn.test.type === "BinaryExpression" && fn.test.operator === "===" && fn.test.right.type === "Identifier" && fn.test.right.name === "undefined" && fn.consequent.type === "Identifier" && fn.consequent.name === "undefined" && isAdapterOutput(fn.alternate);
142
+ }
143
+ function isNamePath(node) {
144
+ if (node.type === "Identifier" || node.type === "ThisExpression") return true;
145
+ if (node.type !== "MemberExpression") return false;
146
+ const key = node.property.type;
147
+ if (node.computed && key !== "StringLiteral" && key !== "NumericLiteral") return false;
148
+ return isNamePath(node.object);
149
+ }
150
+ function cloneNamePath(j, node) {
151
+ if (node.type === "Identifier") return j.identifier(node.name);
152
+ if (node.type !== "MemberExpression") return j.thisExpression();
153
+ const key = node.property;
154
+ const property = key.type === "Identifier" ? j.identifier(key.name) : key.type === "PrivateName" ? j.privateName(j.identifier(String(key.id.name))) : key.type === "StringLiteral" ? j.stringLiteral(key.value) : j.numericLiteral(key.value);
155
+ return j.memberExpression(cloneNamePath(j, node.object), property, node.computed);
156
+ }
157
+ function adaptFilter(j, api, attr, where, typed) {
158
+ const value = attr.value;
159
+ if (value?.type !== "JSXExpressionContainer" || value.expression.type === "JSXEmptyExpression") {
160
+ api.report(`${where} has a filter value the transform cannot adapt; left unchanged for manual migration`);
161
+ return false;
162
+ }
163
+ const fn = value.expression;
164
+ if (isBoolean(fn, false) || isBoolean(fn, true)) return false;
165
+ if (isShouldFilterOutput(fn) || isAdapterOutput(fn) || isGuardedAdapterOutput(fn)) return false;
166
+ if (fn.type === "Identifier" && fn.name === "undefined") return false;
167
+ const type = fn.type;
168
+ if (type === "NullLiteral" || type === "Literal" && fn.value === null || type === "ConditionalExpression" || type === "LogicalExpression") {
169
+ api.report(`${where} has a filter expression that may not be a function; left unchanged for manual migration`);
170
+ return false;
171
+ }
172
+ if (!isFunctionLiteral(fn) && (fn.type === "ThisExpression" || !isNamePath(fn))) {
173
+ api.report(`${where} has a filter expression that is computed where it stands (a call or other computed value), which the adapter would re-evaluate on every item outside render; left unchanged for manual migration: hoist it to a const and pass the name`);
174
+ return false;
175
+ }
176
+ const used = /* @__PURE__ */ new Set();
177
+ if (fn.type === "Identifier") used.add(fn.name);
178
+ j(fn).find(j.Identifier).forEach((id) => {
179
+ used.add(id.node.name);
180
+ });
181
+ const adapter = j(adapterSource(freeName("item", used), freeName("query", used), freeName("score", used), argCountOf(j, fn))).find(j.ArrowFunctionExpression).get().node;
182
+ j(adapter).find(j.Identifier, { name: "__fn" }).replaceWith(() => typed && !isFunctionLiteral(fn) ? castToCmdkFilter(j, fn) : fn);
183
+ if (isFunctionLiteral(fn)) attr.value = j.jsxExpressionContainer(adapter);
184
+ else {
185
+ const copy = cloneNamePath(j, fn);
186
+ attr.value = j.jsxExpressionContainer(j.conditionalExpression(j.binaryExpression("===", copy, j.identifier("undefined")), j.identifier("undefined"), adapter));
187
+ }
188
+ return true;
189
+ }
190
+ function castToCmdkFilter(j, fn) {
191
+ const param = (name, type, optional = false) => {
192
+ const id = j.identifier(name);
193
+ id.typeAnnotation = j.tsTypeAnnotation(type);
194
+ if (optional) id.optional = true;
195
+ return id;
196
+ };
197
+ const signature = j.tsFunctionType([
198
+ param("value", j.tsStringKeyword()),
199
+ param("search", j.tsStringKeyword()),
200
+ param("keywords", j.tsArrayType(j.tsStringKeyword()), true)
201
+ ]);
202
+ signature.typeAnnotation = j.tsTypeAnnotation(j.tsNumberKeyword());
203
+ const cast = j.tsAsExpression(fn, signature);
204
+ cast.extra = { parenthesized: true };
205
+ return cast;
206
+ }
207
+ export { transform as default };
@@ -0,0 +1 @@
1
+ export { t as createApiFactory, n as transformFile } from "./chunk-worker.mjs";
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "modern-cmdk-codemod",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Codemods that migrate cmdk code to modern-cmdk",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/ABCrimson/modern-cmdk/tree/main/packages/modern-cmdk-codemod#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/ABCrimson/modern-cmdk/issues"
10
+ },
11
+ "engines": {
12
+ "node": ">=26.4.0"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "bin": {
18
+ "modern-cmdk-codemod": "./dist/cli.mjs"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "LICENSE"
23
+ ],
24
+ "dependencies": {
25
+ "jscodeshift": "17.4.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/jscodeshift": "17.3.0",
29
+ "@types/node": "26.6.6",
30
+ "tsdown": "0.23.7",
31
+ "typescript": "7.1.0-dev.20261107.1"
32
+ },
33
+ "keywords": [
34
+ "codemod",
35
+ "jscodeshift",
36
+ "cmdk",
37
+ "modern-cmdk",
38
+ "migration",
39
+ "command-palette",
40
+ "react"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/ABCrimson/modern-cmdk.git",
45
+ "directory": "packages/modern-cmdk-codemod"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "typecheck": "tsc --noEmit"
50
+ }
6
51
  }