modern-cmdk 1.4.0 → 1.5.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.
@@ -1,50 +1,215 @@
1
- // src/codemod/transforms/forward-ref.ts
1
+ import { n as __toESM, t as require_jscodeshift } from "../chunk-jscodeshift.mjs";
2
+ import { t as printOptions } from "../chunk-print-options.mjs";
3
+ // ../modern-cmdk-codemod/src/transforms/forward-ref.ts
4
+ var import_jscodeshift = /* @__PURE__ */ __toESM(require_jscodeshift(), 1);
2
5
  /**
3
- * jscodeshift transform — removes forwardRef wrappers and merges the
4
- * ref parameter into the props object as a regular prop.
6
+ * jscodeshift transform — unwraps forwardRef and binds the ref parameter as a `ref` prop.
5
7
  */
6
8
  function transform(fileInfo, api) {
7
9
  if (!fileInfo.source.includes("forwardRef")) return fileInfo.source;
8
10
  const j = api.jscodeshift;
9
11
  const root = j(fileInfo.source);
12
+ const reactImports = root.find(j.ImportDeclaration).filter((path) => {
13
+ return path.node.source.value === "react" && path.node.importKind !== "type";
14
+ });
15
+ const programScope = root.find(j.Program).get().scope;
16
+ const bindings = {
17
+ forwardRefs: /* @__PURE__ */ new Set(),
18
+ namespaces: []
19
+ };
20
+ reactImports.forEach((path) => {
21
+ for (const specifier of path.node.specifiers ?? []) if (specifier.type === "ImportSpecifier") {
22
+ if (specifier.imported.name === "forwardRef") bindings.forwardRefs.add((specifier.local ?? specifier.imported).name);
23
+ } else if (specifier.local) bindings.namespaces.push(specifier.local.name);
24
+ });
25
+ if (bindings.forwardRefs.size === 0 && bindings.namespaces.length === 0) return fileInfo.source;
26
+ /** Whether a declaration other than a react import specifier binds `name` as a type anywhere in the file. */
27
+ const redeclaresType = (name) => root.find(j.TSTypeParameter, { name }).size() > 0 || root.find(j.TSTypeAliasDeclaration, { id: { name } }).size() > 0 || root.find(j.TSInterfaceDeclaration, { id: { name } }).size() > 0 || root.find(j.TSEnumDeclaration, { id: { name } }).size() > 0 || root.find(j.TSModuleDeclaration, { id: { name } }).size() > 0 || root.find(j.ClassDeclaration, { id: { name } }).size() > 0 || root.find(j.ClassExpression, { id: { name } }).size() > 0 || root.find(j.TSImportEqualsDeclaration, { id: { name } }).size() > 0;
28
+ /**
29
+ * `Ref` as a type name: `<Namespace>.Ref` when react has a default or namespace import, else a
30
+ * `Ref` specifier (existing, or a type-only one added once) on the react named import.
31
+ */
32
+ let refResolved = false;
33
+ let refLocal = "Ref";
34
+ const refTypeName = () => {
35
+ const namespace = bindings.namespaces[0];
36
+ if (namespace !== void 0) return j.tsQualifiedName(j.identifier(namespace), j.identifier("Ref"));
37
+ if (!refResolved) {
38
+ refResolved = true;
39
+ const existing = root.find(j.ImportDeclaration, { source: { value: "react" } }).find(j.ImportSpecifier, { imported: { name: "Ref" } }).paths()[0];
40
+ const existingLocal = existing && (existing.node.local ?? existing.node.imported).name;
41
+ if (existingLocal !== void 0 && !redeclaresType(existingLocal)) refLocal = existingLocal;
42
+ else {
43
+ let suffix = 1;
44
+ while (root.find(j.Identifier, { name: refLocal }).size() > 0) {
45
+ suffix += 1;
46
+ refLocal = suffix === 2 ? "ReactRef" : `ReactRef${suffix - 1}`;
47
+ }
48
+ reactImports.filter((path) => (path.node.specifiers ?? []).some((s) => s.type === "ImportSpecifier")).paths()[0]?.node.specifiers?.push(Object.assign(j.importSpecifier(j.identifier("Ref"), refLocal === "Ref" ? null : j.identifier(refLocal)), { importKind: "type" }));
49
+ }
50
+ }
51
+ return j.identifier(refLocal);
52
+ };
10
53
  let hasChanges = false;
11
- root.find(j.CallExpression).filter((path) => {
54
+ const manual = (message) => {
55
+ api.report(message);
56
+ };
57
+ const isForwardRefCall = (node) => {
58
+ const callee = node.callee;
59
+ if (callee.type === "Identifier") return bindings.forwardRefs.has(callee.name);
60
+ return callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && bindings.namespaces.includes(callee.object.name) && callee.property.type === "Identifier" && callee.property.name === "forwardRef";
61
+ };
62
+ const calls = root.find(j.CallExpression).filter((path) => {
63
+ if (!isForwardRefCall(path.node)) return false;
12
64
  const callee = path.node.callee;
13
- if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "React" && callee.property.type === "Identifier" && callee.property.name === "forwardRef") return true;
14
- if (callee.type === "Identifier" && callee.name === "forwardRef") return true;
15
- return false;
16
- }).forEach((path) => {
17
- const args = path.node.arguments;
18
- if (args.length === 0) return;
19
- const renderFn = args[0];
20
- if (!renderFn) return;
21
- if (renderFn.type !== "ArrowFunctionExpression" && renderFn.type !== "FunctionExpression") return;
65
+ const name = callee.type === "Identifier" ? callee.name : callee.object.name;
66
+ return path.scope.lookup(name) === programScope;
67
+ }).paths();
68
+ for (const path of calls) {
69
+ const call = path.node;
70
+ const renderFn = call.arguments[0];
71
+ if (renderFn === void 0) {
72
+ manual("forwardRef() without a render function: unwrap by hand");
73
+ continue;
74
+ }
75
+ if (renderFn.type !== "ArrowFunctionExpression" && renderFn.type !== "FunctionExpression") {
76
+ manual(renderFn.type === "Identifier" ? `forwardRef(${renderFn.name}): unwrap by hand` : "forwardRef(<expression>): unwrap by hand");
77
+ continue;
78
+ }
22
79
  const params = renderFn.params;
23
- if (params.length < 2) return;
80
+ if (params.length > 2) {
81
+ manual("forwardRef render function with more than two parameters: unwrap by hand");
82
+ continue;
83
+ }
84
+ const typeArgs = ((call.typeArguments ?? call.typeParameters)?.params ?? []).filter(isTsType);
85
+ const typeArgRef = typeArgs[0];
86
+ const typeArgProps = typeArgs[1];
24
87
  const propsParam = params[0];
25
88
  const refParam = params[1];
26
- if (!propsParam || !refParam) return;
27
- const refName = refParam.type === "Identifier" ? refParam.name : "ref";
28
- const refProperty = j.objectProperty(j.identifier(refName), j.identifier(refName));
29
- refProperty.shorthand = true;
30
- if (propsParam.type === "ObjectPattern") propsParam.properties.unshift(refProperty);
31
- else if (propsParam.type === "Identifier") {
32
- const restElement = j.restElement(j.identifier(propsParam.name));
33
- const newParam = j.objectPattern([refProperty, restElement]);
34
- if ("typeAnnotation" in propsParam && propsParam.typeAnnotation) newParam.typeAnnotation = propsParam.typeAnnotation;
35
- renderFn.params[0] = newParam;
89
+ if (propsParam === void 0) {
90
+ if (typeArgRef !== void 0) {
91
+ manual("forwardRef<T>() render function without parameters: unwrap by hand");
92
+ continue;
93
+ }
94
+ unwrapCall(path, renderFn);
95
+ hasChanges = true;
96
+ continue;
97
+ }
98
+ const target = propsParam.type === "AssignmentPattern" ? propsParam.left : propsParam;
99
+ if (target.type !== "Identifier" && target.type !== "ObjectPattern") {
100
+ manual("forwardRef props parameter is not an identifier or object pattern: unwrap by hand");
101
+ continue;
102
+ }
103
+ const propsAnnotation = target.typeAnnotation;
104
+ const propsType = tsTypeOf(propsAnnotation) ?? typeArgProps;
105
+ if (target.type === "ObjectPattern" && target.properties.some((p) => (p.type === "ObjectProperty" || p.type === "Property") && p.computed && p.key.type !== "StringLiteral" && p.key.type !== "NumericLiteral" && p.key.type !== "BigIntLiteral" && !(p.key.type === "TemplateLiteral" && p.key.expressions.length === 0))) {
106
+ manual("forwardRef props pattern has a computed key that may be 'ref': unwrap by hand");
107
+ continue;
108
+ }
109
+ if (target.type === "ObjectPattern" && target.properties.some((p) => (p.type === "ObjectProperty" || p.type === "Property") && (!p.computed && p.key.type === "Identifier" && p.key.name === "ref" || p.key.type === "StringLiteral" && p.key.value === "ref" || p.computed && p.key.type === "TemplateLiteral" && p.key.expressions.length === 0 && p.key.quasis[0]?.value.cooked === "ref"))) {
110
+ manual("forwardRef props pattern already has a 'ref' key: unwrap by hand");
111
+ continue;
36
112
  }
113
+ if (refParam === void 0) {
114
+ if (typeArgRef === void 0 && tsTypeOf(propsAnnotation) !== void 0 && j(renderFn).find(j.JSXSpreadAttribute).size() > 0) {
115
+ manual("forwardRef render function has a JSX spread and no ref type argument: unwrap by hand");
116
+ continue;
117
+ }
118
+ const singleRef = typeArgRef ?? (tsTypeOf(propsAnnotation) !== void 0 ? j.tsUnknownKeyword() : void 0);
119
+ if (singleRef !== void 0) {
120
+ const member = j.tsTypeLiteral([Object.assign(j.tsPropertySignature(j.identifier("ref"), j.tsTypeAnnotation(j.tsTypeReference(refTypeName(), j.tsTypeParameterInstantiation([singleRef])))), { optional: true })]);
121
+ target.typeAnnotation = j.tsTypeAnnotation(propsType !== void 0 ? j.tsIntersectionType([intersectable(j, propsType), member]) : member);
122
+ } else if (propsAnnotation == null && typeArgProps !== void 0) target.typeAnnotation = j.tsTypeAnnotation(typeArgProps);
123
+ unwrapCall(path, renderFn);
124
+ hasChanges = true;
125
+ continue;
126
+ }
127
+ if (refParam.type !== "Identifier") {
128
+ manual("forwardRef ref parameter is not a plain identifier: unwrap by hand");
129
+ continue;
130
+ }
131
+ const refAnnotation = refParam.typeAnnotation;
132
+ let refType;
133
+ const annotatedRef = tsTypeOf(refAnnotation);
134
+ if (annotatedRef !== void 0) refType = annotatedRef;
135
+ else if (typeArgRef !== void 0) refType = j.tsTypeReference(refTypeName(), j.tsTypeParameterInstantiation([typeArgRef]));
136
+ if (refType === void 0 && propsType !== void 0) refType = j.tsTypeReference(refTypeName(), j.tsTypeParameterInstantiation([j.tsUnknownKeyword()]));
137
+ let annotation;
138
+ if (refType !== void 0) {
139
+ const refMember = j.tsTypeLiteral([Object.assign(j.tsPropertySignature(j.identifier("ref"), j.tsTypeAnnotation(refType)), { optional: true })]);
140
+ if (propsType !== void 0) annotation = j.tsIntersectionType([intersectable(j, propsType), refMember]);
141
+ else if (typeArgs.length > 0) annotation = refMember;
142
+ else {
143
+ manual("forwardRef ref is annotated but the props type is unknown: unwrap by hand");
144
+ continue;
145
+ }
146
+ } else if (propsType !== void 0) annotation = propsType;
147
+ const refProperty = j.objectProperty(j.identifier("ref"), j.identifier(refParam.name));
148
+ refProperty.shorthand = refParam.name === "ref";
149
+ let pattern;
150
+ if (target.type === "ObjectPattern") {
151
+ target.properties.unshift(refProperty);
152
+ pattern = target;
153
+ } else pattern = j.objectPattern.from({ properties: [refProperty, j.restElement(j.identifier(target.name))] });
154
+ if (annotation !== void 0) pattern.typeAnnotation = j.tsTypeAnnotation(annotation);
155
+ else if (target.type === "Identifier" && target.typeAnnotation) pattern.typeAnnotation = target.typeAnnotation;
156
+ if (propsParam.type === "AssignmentPattern") propsParam.left = pattern;
157
+ else renderFn.params[0] = pattern;
37
158
  renderFn.params = [renderFn.params[0]];
38
- j(path).replaceWith(renderFn);
159
+ unwrapCall(path, renderFn);
39
160
  hasChanges = true;
40
- });
41
- if (hasChanges) {
42
- if (root.find(j.Identifier, { name: "forwardRef" }).filter((path) => path.parent.node.type !== "ImportSpecifier").length === 0) root.find(j.ImportSpecifier, { imported: { name: "forwardRef" } }).forEach((path) => {
43
- const importDecl = path.parent.node;
44
- if (importDecl.specifiers && importDecl.specifiers.length === 1) j(path.parent).remove();
45
- else j(path).remove();
161
+ }
162
+ if (!hasChanges) return fileInfo.source;
163
+ reactImports.forEach((declaration) => {
164
+ const specifiers = declaration.node.specifiers ?? [];
165
+ const before = specifiers.length;
166
+ const kept = specifiers.filter((specifier) => {
167
+ if (specifier.type !== "ImportSpecifier" || specifier.imported.name !== "forwardRef") return true;
168
+ const local = (specifier.local ?? specifier.imported).name;
169
+ return isReferenced(j, root, programScope, local);
46
170
  });
171
+ if (kept.length === before) return;
172
+ if (kept.length === 0) {
173
+ const comments = declaration.node.comments;
174
+ const siblings = declaration.parent.node.body;
175
+ const next = siblings[siblings.indexOf(declaration.node) + 1];
176
+ if (comments && next) next.comments = [...comments, ...next.comments ?? []];
177
+ j(declaration).remove();
178
+ } else declaration.node.specifiers = kept;
179
+ });
180
+ return root.toSource(printOptions(fileInfo.source));
181
+ }
182
+ /** Replace the forwardRef call with its render function, keeping the call's own comments. */
183
+ function unwrapCall(path, renderFn) {
184
+ const comments = path.node.comments;
185
+ if (comments) renderFn.comments = [...comments, ...renderFn.comments ?? []];
186
+ path.replace(renderFn);
187
+ }
188
+ /** Flow type arguments (a .js file with Flow) are not TypeScript types and are never used. */
189
+ const isTsType = (type) => import_jscodeshift.default.TSType.check(type);
190
+ /** The TypeScript type inside a `: T` annotation, if the annotation is one. */
191
+ function tsTypeOf(annotation) {
192
+ return annotation?.type === "TSTypeAnnotation" && isTsType(annotation.typeAnnotation) ? annotation.typeAnnotation : void 0;
193
+ }
194
+ /** A type that binds looser than `&` gets parentheses before it joins an intersection. */
195
+ function intersectable(j, type) {
196
+ switch (type.type) {
197
+ case "TSUnionType":
198
+ case "TSFunctionType":
199
+ case "TSConstructorType":
200
+ case "TSConditionalType": return j.tsParenthesizedType(type);
201
+ default: return type;
47
202
  }
48
- return hasChanges ? root.toSource() : fileInfo.source;
203
+ }
204
+ /** Whether an Identifier named `name` is a reference outside any import specifier. */
205
+ function isReferenced(j, root, programScope, name) {
206
+ return root.find(j.Identifier, { name }).filter((path) => {
207
+ if (path.scope.lookup(name) !== programScope) return false;
208
+ const parent = path.parent.node;
209
+ if (parent.type === "ImportSpecifier" || parent.type === "ImportDefaultSpecifier" || parent.type === "ImportNamespaceSpecifier") return false;
210
+ if ((parent.type === "MemberExpression" || parent.type === "OptionalMemberExpression") && parent.property === path.node && !parent.computed) return false;
211
+ if ((parent.type === "ObjectProperty" || parent.type === "Property") && parent.key === path.node && !parent.computed && !parent.shorthand) return false;
212
+ return true;
213
+ }).size() > 0;
49
214
  }
50
215
  export { transform as default };