[WEB-5459] feat(codemods): add function declaration transformer with tests (#8137)
- Add jscodeshift-based codemod to convert arrow function components to function declarations - Support React.FC, observer-wrapped, and forwardRef components - Include comprehensive test suite covering edge cases - Add npm script to run transformer across codebase - Target only .tsx files in source directories, excluding node_modules and declaration files * [WEB-5459] chore: updates after running codemod --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
parent
90866fb925
commit
83fdebf64d
1771 changed files with 17003 additions and 13856 deletions
5
packages/codemods/.prettierrc
Normal file
5
packages/codemods/.prettierrc
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
483
packages/codemods/function-declaration.ts
Normal file
483
packages/codemods/function-declaration.ts
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
import {
|
||||
API,
|
||||
FileInfo,
|
||||
Options,
|
||||
TSTypeReference,
|
||||
JSCodeshift,
|
||||
Identifier,
|
||||
BlockStatement,
|
||||
VariableDeclarator,
|
||||
Expression,
|
||||
Pattern,
|
||||
SpreadElement,
|
||||
JSXNamespacedName,
|
||||
ASTNode,
|
||||
Node,
|
||||
FunctionDeclaration,
|
||||
} from "jscodeshift";
|
||||
|
||||
const COMPONENT_TYPE_NAMES = new Set([
|
||||
"FC",
|
||||
"FunctionComponent",
|
||||
"VFC",
|
||||
"VoidFunctionComponent",
|
||||
]);
|
||||
|
||||
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
||||
|
||||
function isReactComponentType(typeReference: TSTypeReference, j: JSCodeshift) {
|
||||
const typeName = typeReference.typeName;
|
||||
|
||||
if (!typeName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (j.Identifier.check(typeName)) {
|
||||
return COMPONENT_TYPE_NAMES.has(typeName.name);
|
||||
}
|
||||
|
||||
if (
|
||||
j.TSQualifiedName.check(typeName) &&
|
||||
j.Identifier.check(typeName.left) &&
|
||||
j.Identifier.check(typeName.right)
|
||||
) {
|
||||
return (
|
||||
typeName.left.name === "React" &&
|
||||
COMPONENT_TYPE_NAMES.has(typeName.right.name)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isComponentNameIdentifier(identifier: Identifier | null | undefined) {
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return COMPONENT_NAME_PATTERN.test(identifier.name);
|
||||
}
|
||||
|
||||
function addComments(target: Node, comments: NonNullable<Node["comments"]>) {
|
||||
if (!comments || comments.length === 0) {
|
||||
return;
|
||||
}
|
||||
target.comments ||= [];
|
||||
target.comments.push(...comments);
|
||||
}
|
||||
|
||||
function copyOuterComments(source: Node, target: Node, j: JSCodeshift) {
|
||||
if (!j.Node.check(source) || !j.Node.check(target) || !source.comments) {
|
||||
return;
|
||||
}
|
||||
const outerComments = source.comments.filter((c) => c.leading || c.trailing);
|
||||
addComments(target, outerComments);
|
||||
}
|
||||
|
||||
function ensureParamType(
|
||||
param: Pattern,
|
||||
propsType: ASTNode | null | undefined,
|
||||
j: JSCodeshift
|
||||
) {
|
||||
if (!j.Pattern.check(param)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("typeAnnotation" in param)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!propsType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (j.TSTypeReference.check(propsType) && propsType.typeName) {
|
||||
param.typeAnnotation = j.tsTypeAnnotation(
|
||||
propsType.typeParameters
|
||||
? j.tsTypeReference(propsType.typeName, propsType.typeParameters)
|
||||
: j.tsTypeReference(propsType.typeName)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (j.TSType.check(propsType)) {
|
||||
// @ts-expect-error: jscodeshift types are too strict here
|
||||
param.typeAnnotation = j.tsTypeAnnotation(propsType);
|
||||
}
|
||||
}
|
||||
|
||||
function toBlockBody(j: JSCodeshift, body: BlockStatement | Expression) {
|
||||
if (j.BlockStatement.check(body)) {
|
||||
return body;
|
||||
}
|
||||
|
||||
// @ts-expect-error: jscodeshift types are too strict here
|
||||
const returnStatement = j.returnStatement(body);
|
||||
|
||||
return j.blockStatement([returnStatement]);
|
||||
}
|
||||
|
||||
function isFunction(node: Node, j: JSCodeshift) {
|
||||
return (
|
||||
j.ArrowFunctionExpression.check(node) || j.FunctionExpression.check(node)
|
||||
);
|
||||
}
|
||||
|
||||
function extractArrowFunction(
|
||||
init: Expression | SpreadElement | JSXNamespacedName,
|
||||
j: JSCodeshift
|
||||
) {
|
||||
if (isFunction(init, j)) {
|
||||
return init;
|
||||
}
|
||||
|
||||
// If it's a CallExpression like observer(() => {}), extract the arrow function
|
||||
if (j.CallExpression.check(init)) {
|
||||
const firstArg = init.arguments?.[0];
|
||||
if (firstArg && isFunction(firstArg, j)) {
|
||||
return firstArg;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function extractPropsTypeFromWrapper(
|
||||
init: Expression | SpreadElement | JSXNamespacedName,
|
||||
j: JSCodeshift
|
||||
) {
|
||||
// If it's a CallExpression like observer<React.FC<Props>>((props) => {})
|
||||
// Extract the Props type from React.FC<Props>
|
||||
if (!j.CallExpression.check(init)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("typeParameters" in init)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeParameters = init.typeParameters;
|
||||
if (!j.TSTypeParameterInstantiation.check(typeParameters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeParam = typeParameters.params?.[0];
|
||||
if (
|
||||
!j.TSTypeReference.check(typeParam) ||
|
||||
!isReactComponentType(typeParam, j)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the generic type from React.FC<PropsType>
|
||||
return typeParam.typeParameters?.params?.[0];
|
||||
}
|
||||
|
||||
function isReactForwardRef(
|
||||
init: Expression | SpreadElement | JSXNamespacedName,
|
||||
j: JSCodeshift
|
||||
) {
|
||||
if (!j.CallExpression.check(init)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const callee = init.callee;
|
||||
|
||||
// Check for React.forwardRef
|
||||
if (
|
||||
j.MemberExpression.check(callee) &&
|
||||
j.Identifier.check(callee.object) &&
|
||||
j.Identifier.check(callee.property)
|
||||
) {
|
||||
return (
|
||||
callee.object.name === "React" && callee.property.name === "forwardRef"
|
||||
);
|
||||
}
|
||||
|
||||
// Check for forwardRef (imported directly)
|
||||
if (j.Identifier.check(callee)) {
|
||||
return callee.name === "forwardRef";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractForwardRefTypes(
|
||||
init: Expression | SpreadElement | JSXNamespacedName,
|
||||
j: JSCodeshift
|
||||
) {
|
||||
if (!isReactForwardRef(init, j)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("typeParameters" in init)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeParameters = init.typeParameters;
|
||||
|
||||
// If no type parameters, we still want to apply default empty object for props
|
||||
if (
|
||||
!j.TSTypeParameterInstantiation.check(typeParameters) ||
|
||||
typeParameters.params.length === 0
|
||||
) {
|
||||
return; // Let the default props type handling take care of it
|
||||
}
|
||||
|
||||
const typeParams = typeParameters.params;
|
||||
|
||||
// React.forwardRef<ElementType, PropsType>
|
||||
// If PropsType is not specified, use Record<string, unknown> to avoid ESLint errors
|
||||
const [elementType] = typeParams;
|
||||
|
||||
if (!elementType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propsType =
|
||||
typeParams.length >= 2 && typeParams[1]
|
||||
? typeParams[1]
|
||||
: j.tsTypeReference(
|
||||
j.identifier("Record"),
|
||||
j.tsTypeParameterInstantiation([
|
||||
j.tsStringKeyword(),
|
||||
j.tsUnknownKeyword(),
|
||||
])
|
||||
);
|
||||
|
||||
// Create React.ForwardedRef<ElementType> for the ref parameter
|
||||
const refType = j.tsTypeReference(
|
||||
j.tsQualifiedName(j.identifier("React"), j.identifier("ForwardedRef")),
|
||||
j.tsTypeParameterInstantiation([elementType])
|
||||
);
|
||||
|
||||
return { propsType, refType };
|
||||
}
|
||||
|
||||
function isEmptyObjectType(type: ASTNode, j: JSCodeshift) {
|
||||
return j.TSTypeLiteral.check(type) && type.members.length === 0;
|
||||
}
|
||||
|
||||
function convertToFunction(
|
||||
j: JSCodeshift,
|
||||
declaration: VariableDeclarator,
|
||||
init: Expression | SpreadElement | JSXNamespacedName,
|
||||
propsType: ASTNode | null | undefined
|
||||
) {
|
||||
if (!j.Identifier.check(declaration.id)) {
|
||||
throw new Error("Declaration id must be an identifier");
|
||||
}
|
||||
const componentName = declaration.id.name;
|
||||
const arrowFn = extractArrowFunction(init, j);
|
||||
|
||||
if (!arrowFn) {
|
||||
throw new Error("Expected ArrowFunctionExpression or FunctionExpression");
|
||||
}
|
||||
|
||||
const params = arrowFn.params;
|
||||
const body = toBlockBody(j, arrowFn.body);
|
||||
|
||||
const newFunction = j.functionDeclaration(
|
||||
j.identifier(componentName),
|
||||
params,
|
||||
body
|
||||
);
|
||||
|
||||
// Check if this is React.forwardRef and extract types for props and ref
|
||||
const forwardRefTypes = extractForwardRefTypes(init, j);
|
||||
|
||||
if (forwardRefTypes) {
|
||||
// Apply props type to first parameter
|
||||
const [firstParam, secondParam] = newFunction.params;
|
||||
if (j.Pattern.check(firstParam) && "typeAnnotation" in firstParam) {
|
||||
ensureParamType(firstParam, forwardRefTypes.propsType, j);
|
||||
}
|
||||
// Apply ref type to second parameter
|
||||
if (j.Pattern.check(secondParam) && "typeAnnotation" in secondParam) {
|
||||
ensureParamType(secondParam, forwardRefTypes.refType, j);
|
||||
}
|
||||
} else if (newFunction.params.length > 0) {
|
||||
const [firstParam] = newFunction.params;
|
||||
if (firstParam) {
|
||||
ensureParamType(firstParam, propsType, j);
|
||||
}
|
||||
} else if (propsType && !isEmptyObjectType(propsType, j)) {
|
||||
// If there are no params but a non-empty propsType exists, add _props parameter
|
||||
const propsParam = j.identifier("_props");
|
||||
ensureParamType(propsParam, propsType, j);
|
||||
newFunction.params.push(propsParam);
|
||||
}
|
||||
|
||||
if (arrowFn.returnType) {
|
||||
newFunction.returnType = arrowFn.returnType;
|
||||
}
|
||||
|
||||
// Preserve type parameters (generics) from arrow function
|
||||
if (arrowFn.typeParameters) {
|
||||
newFunction.typeParameters = arrowFn.typeParameters;
|
||||
}
|
||||
|
||||
newFunction.async = arrowFn.async;
|
||||
newFunction.generator = arrowFn.generator;
|
||||
|
||||
return newFunction;
|
||||
}
|
||||
|
||||
function containsJsx(j: JSCodeshift, body: ASTNode) {
|
||||
return (
|
||||
j(body).find(j.JSXElement).paths().length > 0 ||
|
||||
j(body).find(j.JSXFragment).paths().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function toFunctionExpression(
|
||||
j: JSCodeshift,
|
||||
declaration: FunctionDeclaration
|
||||
) {
|
||||
const expression = j.functionExpression(
|
||||
declaration.id,
|
||||
declaration.params,
|
||||
declaration.body,
|
||||
declaration.generator,
|
||||
declaration.async
|
||||
);
|
||||
expression.returnType = declaration.returnType;
|
||||
expression.typeParameters = declaration.typeParameters;
|
||||
return expression;
|
||||
}
|
||||
|
||||
export default function transform(file: FileInfo, api: API, options: Options) {
|
||||
const baseJ = api.jscodeshift;
|
||||
const j =
|
||||
typeof baseJ.withParser === "function" ? baseJ.withParser("tsx") : baseJ;
|
||||
const root = j(file.source);
|
||||
|
||||
root
|
||||
.find(j.VariableDeclaration)
|
||||
.filter((path) => {
|
||||
const [firstDeclaration] = path.node.declarations;
|
||||
|
||||
if (!j.VariableDeclarator.check(firstDeclaration)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!j.Identifier.check(firstDeclaration.id) ||
|
||||
!isComponentNameIdentifier(firstDeclaration.id)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const init = firstDeclaration.init;
|
||||
|
||||
if (!init) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const functionToCheck = extractArrowFunction(init, j);
|
||||
|
||||
if (!functionToCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.path && !file.path.endsWith(".tsx")) {
|
||||
if (!containsJsx(j, functionToCheck)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.forEach((path) => {
|
||||
const [firstDeclaration] = path.node.declarations;
|
||||
if (!j.VariableDeclarator.check(firstDeclaration)) {
|
||||
return;
|
||||
}
|
||||
const init = firstDeclaration.init;
|
||||
|
||||
if (!init) {
|
||||
return;
|
||||
}
|
||||
|
||||
let typeAnnotation: ASTNode | null | undefined;
|
||||
if (j.Identifier.check(firstDeclaration.id)) {
|
||||
typeAnnotation = firstDeclaration.id.typeAnnotation?.typeAnnotation;
|
||||
}
|
||||
|
||||
// Try to get props type from variable type annotation first
|
||||
let propsType: ASTNode | undefined =
|
||||
j.TSTypeReference.check(typeAnnotation) &&
|
||||
isReactComponentType(typeAnnotation, j)
|
||||
? typeAnnotation.typeParameters?.params?.[0]
|
||||
: undefined;
|
||||
|
||||
// If no props type from variable annotation, try to extract from wrapper's type parameters
|
||||
if (!propsType) {
|
||||
propsType = extractPropsTypeFromWrapper(init, j);
|
||||
}
|
||||
|
||||
const newFunction = convertToFunction(
|
||||
j,
|
||||
firstDeclaration,
|
||||
init,
|
||||
propsType
|
||||
);
|
||||
|
||||
const originalNode = path.node;
|
||||
|
||||
// Check if init is wrapped in a call expression (e.g., observer(...))
|
||||
const hasWrapper = j.CallExpression.check(init);
|
||||
|
||||
if (hasWrapper) {
|
||||
// Preserve the wrapper by keeping it as a const assignment
|
||||
// e.g., export const Foo = observer(() => {}) becomes export const Foo = observer(function Foo() {...})
|
||||
|
||||
// Convert function declaration to function expression for wrapping
|
||||
const functionExpression = toFunctionExpression(j, newFunction);
|
||||
|
||||
const wrappedFunction = j.callExpression(init.callee, [
|
||||
functionExpression,
|
||||
]);
|
||||
|
||||
if (!j.Identifier.check(firstDeclaration.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newDeclarator = j.variableDeclarator(
|
||||
j.identifier(firstDeclaration.id.name),
|
||||
wrappedFunction
|
||||
);
|
||||
|
||||
const newVarDecl = j.variableDeclaration("const", [newDeclarator]);
|
||||
|
||||
// Copy comments from original declaration to new function
|
||||
copyOuterComments(originalNode, newVarDecl, j);
|
||||
|
||||
j(path).replaceWith(newVarDecl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy outer comments from original declaration to new function
|
||||
copyOuterComments(originalNode, newFunction, j);
|
||||
|
||||
// Copy comments from VariableDeclarator (e.g. export /* comment */ const Foo)
|
||||
if (firstDeclaration.comments) {
|
||||
addComments(newFunction, firstDeclaration.comments);
|
||||
}
|
||||
|
||||
// Copy comments from arrow function
|
||||
if (init.comments) {
|
||||
addComments(newFunction, init.comments);
|
||||
}
|
||||
|
||||
j(path).replaceWith(newFunction);
|
||||
});
|
||||
|
||||
const quote = options.quote ?? '"';
|
||||
|
||||
const source = root.toSource({
|
||||
quote,
|
||||
});
|
||||
|
||||
return source;
|
||||
}
|
||||
366
packages/codemods/instructions.md
Normal file
366
packages/codemods/instructions.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# jscodeshift Instructions
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core API
|
||||
|
||||
#### `jscodeshift`
|
||||
|
||||
The main function that returns the jscodeshift instance.
|
||||
|
||||
- **Parameters**: `source` (String)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const j = jscodeshift(sourceCode);
|
||||
```
|
||||
|
||||
### Node Traversal APIs
|
||||
|
||||
#### `find`
|
||||
|
||||
Finds nodes that match the provided type.
|
||||
|
||||
- **Parameters**: `type` (String or Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const variableDeclarations = j.find(j.VariableDeclaration);
|
||||
```
|
||||
|
||||
#### `findImportDeclarations`
|
||||
|
||||
Finds all ImportDeclarations optionally filtered by name.
|
||||
|
||||
- **Parameters**: `sourcePath` (String)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const routerImports = j.findImportDeclarations("react-router-dom");
|
||||
```
|
||||
|
||||
#### `closestScope`
|
||||
|
||||
Finds the closest enclosing scope of a node.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const closestScopes = j.find(j.Identifier).closestScope();
|
||||
```
|
||||
|
||||
#### `closest`
|
||||
|
||||
Finds the nearest parent node that matches the specified type.
|
||||
|
||||
- **Parameters**: `type` (String or Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const closestFunction = j.find(j.Identifier).closest(j.FunctionDeclaration);
|
||||
```
|
||||
|
||||
#### `getVariableDeclarators`
|
||||
|
||||
Retrieves variable declarators from the current collection.
|
||||
|
||||
- **Parameters**: `callback` (Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const variableDeclarators = j.find(j.Identifier).getVariableDeclarators((path) => path.value.name);
|
||||
```
|
||||
|
||||
#### `findVariableDeclarators`
|
||||
|
||||
Finds variable declarators by name.
|
||||
|
||||
- **Parameters**: `name` (String)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const variableDeclarators = j.findVariableDeclarators("a");
|
||||
```
|
||||
|
||||
#### `filter`
|
||||
|
||||
Filters nodes based on a predicate function.
|
||||
|
||||
- **Parameters**: `predicate` (Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const constDeclarations = j.find(j.VariableDeclaration).filter((path) => path.node.kind === "const");
|
||||
```
|
||||
|
||||
#### `forEach`
|
||||
|
||||
Iterates over each node in the collection.
|
||||
|
||||
- **Parameters**: `callback` (Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
j.find(j.VariableDeclaration).forEach((path) => {
|
||||
console.log(path.node);
|
||||
});
|
||||
```
|
||||
|
||||
#### `some`
|
||||
|
||||
Checks if at least one element in the collection passes the test.
|
||||
|
||||
- **Parameters**: `callback` (Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const hasVariableA = root.find(j.VariableDeclarator).some((path) => path.node.id.name === "a");
|
||||
```
|
||||
|
||||
#### `every`
|
||||
|
||||
Checks if all elements in the collection pass the test.
|
||||
|
||||
- **Parameters**: `callback` (Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const allAreConst = root.find(j.VariableDeclaration).every((path) => path.node.kind === "const");
|
||||
```
|
||||
|
||||
#### `map`
|
||||
|
||||
Maps each node in the collection to a new value.
|
||||
|
||||
- **Parameters**: `callback` (Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const variableNames = j.find(j.VariableDeclaration).map((path) => path.node.declarations.map((decl) => decl.id.name));
|
||||
```
|
||||
|
||||
#### `size`
|
||||
|
||||
Returns the number of nodes in the collection.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const numberOfNodes = j.find(j.VariableDeclaration).size();
|
||||
```
|
||||
|
||||
#### `length`
|
||||
|
||||
Returns the number of elements in the collection.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const varCount = root.find(j.VariableDeclarator).length;
|
||||
```
|
||||
|
||||
#### `nodes`
|
||||
|
||||
Returns the AST nodes in the collection.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const nodes = j.find(j.VariableDeclaration).nodes();
|
||||
```
|
||||
|
||||
#### `paths`
|
||||
|
||||
Returns the paths of the found nodes.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const paths = j.find(j.VariableDeclaration).paths();
|
||||
```
|
||||
|
||||
#### `getAST`
|
||||
|
||||
Returns the root AST node of the collection.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const ast = root.getAST();
|
||||
```
|
||||
|
||||
#### `get`
|
||||
|
||||
Gets the first node in the collection.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const firstVariableDeclaration = j.find(j.VariableDeclaration).get();
|
||||
```
|
||||
|
||||
#### `at`
|
||||
|
||||
Navigates to a specific path in the AST.
|
||||
|
||||
- **Parameters**: `index` (Number)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const secondVariableDeclaration = j.find(j.VariableDeclaration).at(1);
|
||||
```
|
||||
|
||||
#### `getTypes`
|
||||
|
||||
Returns the set of node types present in the collection.
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
const types = root.find(j.VariableDeclarator).getTypes();
|
||||
```
|
||||
|
||||
#### `isOfType`
|
||||
|
||||
Checks if the node in the collection is of a specific type.
|
||||
|
||||
- **Parameters**: `type` (String)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const isVariableDeclarator = root.find(j.VariableDeclarator).at(0).isOfType("VariableDeclarator");
|
||||
```
|
||||
|
||||
### Node Transformation APIs
|
||||
|
||||
#### `replaceWith`
|
||||
|
||||
Replaces the current node(s) with a new node.
|
||||
|
||||
- **Parameters**: `newNode` (Node or Function)
|
||||
- **Example**:
|
||||
```javascript
|
||||
j.find(j.Identifier).replaceWith((path) => j.identifier(path.node.name.toUpperCase()));
|
||||
```
|
||||
|
||||
#### `insertBefore`
|
||||
|
||||
Inserts a node before the current node.
|
||||
|
||||
- **Parameters**: `newNode` (Node)
|
||||
- **Example**:
|
||||
```javascript
|
||||
j.find(j.FunctionDeclaration).insertBefore(j.expressionStatement(j.stringLiteral("Inserted before")));
|
||||
```
|
||||
|
||||
#### `insertAfter`
|
||||
|
||||
Inserts a node after the current node.
|
||||
|
||||
- **Parameters**: `newNode` (Node)
|
||||
- **Example**:
|
||||
```javascript
|
||||
j.find(j.FunctionDeclaration).insertAfter(j.expressionStatement(j.stringLiteral("Inserted after")));
|
||||
```
|
||||
|
||||
#### `remove`
|
||||
|
||||
Removes the current node(s).
|
||||
|
||||
- **Example**:
|
||||
```javascript
|
||||
j.find(j.VariableDeclaration).remove();
|
||||
```
|
||||
|
||||
#### `renameTo`
|
||||
|
||||
Renames the nodes in the collection to a new name.
|
||||
|
||||
- **Parameters**: `newName` (String)
|
||||
- **Example**:
|
||||
```javascript
|
||||
root.find(j.Identifier, { name: "a" }).renameTo("x");
|
||||
```
|
||||
|
||||
#### `toSource`
|
||||
|
||||
Converts the transformed AST back to source code.
|
||||
|
||||
- **Parameters**: `options` (Object)
|
||||
- **Example**:
|
||||
```javascript
|
||||
const transformedSource = j.toSource({ quote: "single" });
|
||||
```
|
||||
|
||||
## AST Grammar
|
||||
|
||||
jscodeshift provides 278 node types which are mapped to their corresponding node type in `ast-types`.
|
||||
|
||||
### Common Node Types
|
||||
|
||||
- **AnyTypeAnnotation**: A type annotation representing any type.
|
||||
- **ArrayExpression**: Represents an array literal.
|
||||
- **ArrayPattern**: A pattern that matches an array from a destructuring assignment.
|
||||
- **ArrayTypeAnnotation**: A type annotation for arrays.
|
||||
- **ArrowFunctionExpression**: An arrow function expression.
|
||||
- **AssignmentExpression**: Represents an assignment expression.
|
||||
- **AssignmentPattern**: A pattern that matches an assignment from a destructuring assignment.
|
||||
- **AwaitExpression**: Represents an await expression.
|
||||
- **BigIntLiteral**: A literal representing a big integer.
|
||||
- **BinaryExpression**: Represents a binary expression.
|
||||
- **BlockStatement**: Represents a block statement.
|
||||
- **BooleanLiteral**: A literal representing a boolean value.
|
||||
- **BreakStatement**: Represents a break statement.
|
||||
- **CallExpression**: Represents a call expression.
|
||||
- **CatchClause**: Represents a catch clause in a try statement.
|
||||
- **ClassDeclaration**: Represents a class declaration.
|
||||
- **ClassExpression**: Represents a class expression.
|
||||
- **ClassMethod**: Represents a method of a class.
|
||||
- **ClassProperty**: Represents a property of a class.
|
||||
- **Comment**: Represents a comment in the code.
|
||||
- **ConditionalExpression**: Represents a conditional expression (ternary).
|
||||
- **ContinueStatement**: Represents a continue statement.
|
||||
- **DebuggerStatement**: Represents a debugger statement.
|
||||
- **Declaration**: Represents a declaration in the code.
|
||||
- **DoWhileStatement**: Represents a do…while statement.
|
||||
- **ExportAllDeclaration**: Represents an export all declaration.
|
||||
- **ExportDeclaration**: Represents an export declaration.
|
||||
- **ExportDefaultDeclaration**: Represents an export default declaration.
|
||||
- **ExportNamedDeclaration**: Represents a named export declaration.
|
||||
- **ExpressionStatement**: Represents an expression statement.
|
||||
- **File**: Represents a file in the AST.
|
||||
- **ForInStatement**: Represents a for-in statement.
|
||||
- **ForOfStatement**: Represents a for-of statement.
|
||||
- **ForStatement**: Represents a for statement.
|
||||
- **FunctionDeclaration**: Represents a function declaration.
|
||||
- **FunctionExpression**: Represents a function expression.
|
||||
- **Identifier**: Represents an identifier.
|
||||
- **IfStatement**: Represents an if statement.
|
||||
- **ImportDeclaration**: Represents an import declaration.
|
||||
- **ImportDefaultSpecifier**: Represents a default import specifier.
|
||||
- **ImportNamespaceSpecifier**: Represents a namespace import specifier.
|
||||
- **ImportSpecifier**: Represents an import specifier.
|
||||
- **InterfaceDeclaration**: Represents an interface declaration.
|
||||
- **JSXAttribute**: Represents an attribute in a JSX element.
|
||||
- **JSXElement**: Represents a JSX element.
|
||||
- **JSXExpressionContainer**: Represents an expression container in JSX.
|
||||
- **JSXFragment**: Represents a JSX fragment.
|
||||
- **JSXIdentifier**: Represents an identifier in JSX.
|
||||
- **JSXText**: Represents text in JSX.
|
||||
- **Literal**: Represents a literal value.
|
||||
- **LogicalExpression**: Represents a logical expression.
|
||||
- **MemberExpression**: Represents a member expression.
|
||||
- **MethodDefinition**: Represents a method definition.
|
||||
- **NewExpression**: Represents a new expression.
|
||||
- **ObjectExpression**: Represents an object expression.
|
||||
- **ObjectPattern**: Represents an object pattern for destructuring.
|
||||
- **ObjectProperty**: Represents a property in an object.
|
||||
- **Program**: Represents the entire program.
|
||||
- **Property**: Represents a property in an object.
|
||||
- **ReturnStatement**: Represents a return statement.
|
||||
- **SpreadElement**: Represents a spread element in an array or function call.
|
||||
- **StringLiteral**: Represents a string literal.
|
||||
- **SwitchCase**: Represents a case in a switch statement.
|
||||
- **SwitchStatement**: Represents a switch statement.
|
||||
- **TemplateLiteral**: Represents a template literal.
|
||||
- **ThisExpression**: Represents the `this` expression.
|
||||
- **ThrowStatement**: Represents a throw statement.
|
||||
- **TryStatement**: Represents a try statement.
|
||||
- **TSAnyKeyword**: Represents the TypeScript `any` keyword.
|
||||
- **TSArrayType**: Represents a TypeScript array type.
|
||||
- **TSAsExpression**: Represents a TypeScript as-expression.
|
||||
- **TSBooleanKeyword**: Represents the TypeScript `boolean` keyword.
|
||||
- **TSDeclareFunction**: Represents a TypeScript function declaration.
|
||||
- **TSEnumDeclaration**: Represents a TypeScript enum declaration.
|
||||
- **TSInterfaceDeclaration**: Represents a TypeScript interface declaration.
|
||||
- **TSNumberKeyword**: Represents the TypeScript `number` keyword.
|
||||
- **TSStringKeyword**: Represents the TypeScript `string` keyword.
|
||||
- **TSTypeAliasDeclaration**: Represents a TypeScript type alias declaration.
|
||||
- **TSTypeAnnotation**: Represents a TypeScript type annotation.
|
||||
- **TSTypeReference**: Represents a type reference in TypeScript.
|
||||
- **TSUnionType**: Represents a union type in TypeScript.
|
||||
- **UnaryExpression**: Represents a unary expression.
|
||||
- **VariableDeclaration**: Represents a variable declaration.
|
||||
- **VariableDeclarator**: Represents a variable declarator.
|
||||
- **WhileStatement**: Represents a while statement.
|
||||
|
||||
For a complete list and detailed structure of each node, refer to the [AST Grammar documentation](https://jscodeshift.com/build/ast-grammar/).
|
||||
15
packages/codemods/package.json
Normal file
15
packages/codemods/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "@plane/codemods",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"function-declaration": "jscodeshift -t ./function-declaration.ts --extensions=tsx --parser=tsx ../../apps/*/app ../../apps/*/ce ../../apps/*/core ../../apps/*/ee ../../apps/*/helpers ../../packages/*/src --ignore-pattern='**/node_modules/**' --ignore-pattern='**/dist/**' --ignore-pattern='**/build/**' --ignore-pattern='**/*.d.ts'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hypermod/utils": "^0.7.1",
|
||||
"@types/jscodeshift": "^17.3.0",
|
||||
"jscodeshift": "^17.3.0",
|
||||
"vitest": "^4.0.8"
|
||||
}
|
||||
}
|
||||
465
packages/codemods/tests/function-declaration.spec.ts
Normal file
465
packages/codemods/tests/function-declaration.spec.ts
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { applyTransform } from "@hypermod/utils";
|
||||
import * as transformer from "../function-declaration";
|
||||
|
||||
describe("function-declaration", () => {
|
||||
it("should convert arrow function components to function declarations", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import React from "react";
|
||||
|
||||
export const MyComponent: React.FC<{}> = () => {
|
||||
return <div>Hello, world!</div>;
|
||||
};
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import React from "react";
|
||||
|
||||
export function MyComponent() {
|
||||
return <div>Hello, world!</div>;
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should handle components with props", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import React from "react";
|
||||
|
||||
interface IMyComponentProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const MyComponent: React.FC<IMyComponentProps> = ({ name }) => {
|
||||
return <div>Hello, {name}!</div>;
|
||||
};
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import React from "react";
|
||||
|
||||
interface IMyComponentProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function MyComponent(
|
||||
{
|
||||
name
|
||||
}: IMyComponentProps
|
||||
) {
|
||||
return <div>Hello, {name}!</div>;
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should preserve default props", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import React from "react";
|
||||
|
||||
interface IMyComponentProps {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const MyComponent: React.FC<IMyComponentProps> = ({ name = "world" }) => {
|
||||
return <div>Hello, {name}!</div>;
|
||||
};
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import React from "react";
|
||||
|
||||
interface IMyComponentProps {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function MyComponent(
|
||||
{
|
||||
name = "world"
|
||||
}: IMyComponentProps
|
||||
) {
|
||||
return <div>Hello, {name}!</div>;
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not transform non-component arrow functions", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
const myFunction = () => {
|
||||
return "hello";
|
||||
};
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const myFunction = () => {
|
||||
return "hello";
|
||||
};"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should handle observer-wrapped components", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const WorkspaceAnalyticsHeader = observer(() => {
|
||||
return <div>Analytics</div>;
|
||||
});
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import { observer } from "mobx-react";
|
||||
|
||||
export const WorkspaceAnalyticsHeader = observer(function WorkspaceAnalyticsHeader() {
|
||||
return <div>Analytics</div>;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should handle inline arrow function components", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
export const StarUsOnGitHubLink = () => {
|
||||
return <a href="https://github.com">Star us</a>;
|
||||
};
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"export function StarUsOnGitHubLink() {
|
||||
return <a href="https://github.com">Star us</a>;
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should handle React.FC type without generics", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import type { FC } from "react";
|
||||
|
||||
export const ProjectAppSidebar: FC = observer(() => {
|
||||
return <div>Sidebar</div>;
|
||||
});
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import type { FC } from "react";
|
||||
|
||||
export const ProjectAppSidebar = observer(function ProjectAppSidebar() {
|
||||
return <div>Sidebar</div>;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should handle inline JSX arrow function", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
export const DateAlert = (props: TDateAlertProps) => <></>;
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"export function DateAlert(props: TDateAlertProps) {
|
||||
return <></>;
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should handle observer with generic type parameters", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const InstanceProvider = observer<React.FC<React.PropsWithChildren>>((props) => {
|
||||
const { children } = props;
|
||||
return <>{children}</>;
|
||||
});
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import { observer } from "mobx-react";
|
||||
|
||||
export const InstanceProvider = observer(function InstanceProvider(props: React.PropsWithChildren) {
|
||||
const { children } = props;
|
||||
return <>{children}</>;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not add double semicolons after use client directive", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
"use client";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const MyComponent = observer(() => {
|
||||
return <div>Hello</div>;
|
||||
});
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
""use client";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const MyComponent = observer(function MyComponent() {
|
||||
return <div>Hello</div>;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should preserve generic type parameters in wrapper functions", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import React from "react";
|
||||
|
||||
export const ScatterChart = React.memo(<K extends string, T extends string>(props: TScatterChartProps<K, T>) => {
|
||||
return <div>Chart</div>;
|
||||
});
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import React from "react";
|
||||
|
||||
export const ScatterChart = React.memo(
|
||||
function ScatterChart<K extends string, T extends string>(props: TScatterChartProps<K, T>) {
|
||||
return <div>Chart</div>;
|
||||
}
|
||||
);"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should preserve generic type parameters on React.forwardRef", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import React from "react";
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||
return <button ref={ref}>Click me</button>;
|
||||
});
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import React from "react";
|
||||
|
||||
const Button = React.forwardRef(
|
||||
function Button(props: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) {
|
||||
return <button ref={ref}>Click me</button>;
|
||||
}
|
||||
);"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should prefix unused props parameter with underscore", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import type { TCallbackMentionComponentProps } from "@plane/editor";
|
||||
|
||||
export const EditorAdditionalMentionsRoot: React.FC<TCallbackMentionComponentProps> = () => null;
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import type { TCallbackMentionComponentProps } from "@plane/editor";
|
||||
|
||||
export function EditorAdditionalMentionsRoot(_props: TCallbackMentionComponentProps) {
|
||||
return null;
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should add Record<string, unknown> type for React.forwardRef with only element type", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
import { forwardRef } from "react";
|
||||
|
||||
const ListLoaderItemRow = forwardRef<HTMLDivElement>((props, ref) => (
|
||||
<div ref={ref}>Content</div>
|
||||
));
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"import { forwardRef } from "react";
|
||||
|
||||
const ListLoaderItemRow = forwardRef(
|
||||
function ListLoaderItemRow(props: Record<string, unknown>, ref: React.ForwardedRef<HTMLDivElement>) {
|
||||
return (<div ref={ref}>Content</div>);
|
||||
}
|
||||
);"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should preserve comments in function body", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
export const PreloadResources = () => (
|
||||
// usePreloadResources();
|
||||
null
|
||||
);
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"export function PreloadResources() {
|
||||
return (
|
||||
// usePreloadResources();
|
||||
(null)
|
||||
);
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should preserve leading comments before export declaration", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
"use client";
|
||||
|
||||
// TODO: Check if we need this
|
||||
// https://nextjs.org/docs/app/api-reference/functions/generate-metadata#link-relpreload
|
||||
// export const usePreloadResources = () => {
|
||||
// useEffect(() => {
|
||||
// const preloadItem = (url: string) => {
|
||||
// ReactDOM.preload(url, { as: "fetch", crossOrigin: "use-credentials" });
|
||||
// };
|
||||
//
|
||||
// const urls = [
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/instances/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/profile/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/settings/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/workspaces/?v=\${Date.now()}\`,
|
||||
// ];
|
||||
//
|
||||
// urls.forEach((url) => preloadItem(url));
|
||||
// }, []);
|
||||
// };
|
||||
|
||||
export const PreloadResources = () =>
|
||||
// usePreloadResources();
|
||||
null;
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
""use client";
|
||||
|
||||
// TODO: Check if we need this
|
||||
// https://nextjs.org/docs/app/api-reference/functions/generate-metadata#link-relpreload
|
||||
// export const usePreloadResources = () => {
|
||||
// useEffect(() => {
|
||||
// const preloadItem = (url: string) => {
|
||||
// ReactDOM.preload(url, { as: "fetch", crossOrigin: "use-credentials" });
|
||||
// };
|
||||
//
|
||||
// const urls = [
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/instances/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/profile/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/settings/\`,
|
||||
// \`\${process.env.VITE_API_BASE_URL}/api/users/me/workspaces/?v=\${Date.now()}\`,
|
||||
// ];
|
||||
//
|
||||
// urls.forEach((url) => preloadItem(url));
|
||||
// }, []);
|
||||
// };
|
||||
|
||||
export function PreloadResources() {
|
||||
return (
|
||||
// usePreloadResources();
|
||||
null
|
||||
);
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should preserve leading comments before wrapped export declaration", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
// This is a wrapped component
|
||||
// It uses observer for reactivity
|
||||
export const MyObserverComponent = observer(() => {
|
||||
return <div>Observer component</div>;
|
||||
});
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"// This is a wrapped component
|
||||
// It uses observer for reactivity
|
||||
export const MyObserverComponent = observer(function MyObserverComponent() {
|
||||
return <div>Observer component</div>;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should preserve trailing comments on exported variable declaration", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
export const Foo = () => <div />; // trailing comment
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toContain("// trailing comment");
|
||||
});
|
||||
|
||||
it("should preserve leading comments on exported variable declaration inside export", async () => {
|
||||
const result = await applyTransform(
|
||||
transformer,
|
||||
`
|
||||
export /* leading comment */ const Foo = () => <div />;
|
||||
`,
|
||||
{ parser: "tsx" },
|
||||
);
|
||||
|
||||
expect(result).toContain("/* leading comment */");
|
||||
});
|
||||
});
|
||||
|
||||
13
packages/codemods/tsconfig.json
Normal file
13
packages/codemods/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
7
packages/codemods/vitest.config.ts
Normal file
7
packages/codemods/vitest.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue