Skip to content

Commit

Permalink
feat(v5): New refactorings: wrap with useMemo, useCallback useEffect,…
Browse files Browse the repository at this point in the history
… rename state veriable (#112)

BREAKING CHANGE: Hooks support will now be activated automatically based on React version (16.8 and up) instead of a flag

* feat(rename-state-variable): New Refactoring! Rename State Variable

* feat(hooks-support): activate hooks support based on react version

* removed excess deps

* refactoring + wrap with effect refactoring

* feat(wrap-with-useCallback): New refactoring - wrap function with useCallback

* ts fix

* feat(wrap-with-usememo): New! Wrap expressions with useMemo

* docs: updated teh docs

* fix(wrap-with-memo): improved case detection

* docs: added new refactorings

* docs

* docs
  • Loading branch information
Boris Litvinsky authored May 15, 2020
1 parent 8d2878c commit eba2954
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 63 deletions.
49 changes: 29 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@

# VSCode Glean

> The extension provides refactoring tools for your React/Javascript/Typescript codebase
> The extension provides refactoring tools for your React codebase
[![Build Status](https://travis-ci.org/wix/vscode-glean.svg?branch=master)](https://travis-ci.org/wix/vscode-glean)
[![](https://vsmarketplacebadge.apphb.com/version/wix.glean.svg)](https://marketplace.visualstudio.com/items?itemName=wix.glean)

The extension provides refactoring tools for your React codebase: extract JSX into a new component, convert Class Components to Functional Components and more! In addition, you can extract regular Javascript/Typescript code between files, while handling exporting the selected code from the old location and importing in the new one!

The extension provides refactoring tools for your React codebase: extract JSX into a new component, convert Class Components to Functional Components, wrapping with Hooks and more!
## Highlights

- Allows extracting JSX into new component
- Allows converting Class Components to Functional Components and vice-verse
- Allows wrapping JSX with conditional
- Allows renaming state variables and their setters simultaneously.
- Allows wrapping code with `useMemo`, `useCallback` or `useEffect`
- Moving code between files
- Typescript support
- ES2015 modules support
Expand All @@ -31,23 +32,41 @@ Go to the link below and click `Install`.

### Extracting JSX into a new Component

Glean allows easy extraction of JSX into new React components. Just select the JSX to extract, and Glean will handle all the rest:
Glean allows easy extraction of JSX into new React components (in the same or other file). Just select the JSX to extract, and Glean will handle all the rest:

- Generate Stateful or Stateless Component, such that the extracted JSX will continue to function.
- Generate Class or Functional Component, such that the extracted JSX will continue to function.
- It will identify all inputs to the newly created component.
- Replace extracted JSX with newly created component, while providing it with all the props.

![Example of JSX extraction](https://github.com/wix/vscode-glean/blob/master/assets/extract-to-comp.gif?raw=true)

### Converting Functional Component to Stateful Component
### Converting Class Component to Functional Component
Glean seamlesly automates convertion of class components to functional component, while take care of all the complexity:

- Converts `setState` calls to `useState`
- Converts `componentDidMount` and `componentWillUnmount` to `useEffect`
- Converts class properties to `useRef`
- Wraps call non-Lifecycle methods with `useCallback`

**WARNING!!! If You are using React version older than 16.8.0, This refactoring will delete all Lifecycle methods and setState calls!**

![Example of Hooks Support](https://github.com/wix/vscode-glean/blob/master/assets/hooks.gif?raw=true)



### Converting Functional Component to Class Component

![Example of Stateless to Stateful Component Conversion](https://github.com/wix/vscode-glean/blob/master/assets/stateless-to-stateful.gif?raw=true)

### Converting Stateful Component to Functional Component
### Rename State Variable

![Example of Stateful to Stateless Component Conversion](https://github.com/wix/vscode-glean/blob/master/assets/stateful-to-stateless.gif?raw=true)
Rename any state variable and let Glean rename its setter accordingly for You!

**WARNING!!! This refactoring will delete all Lifecycle methods and setState calls!**
![Example of Rename State](https://github.com/wix/vscode-glean/blob/master/assets/rename-state.gif?raw=true)

### Wrap with Hook (usMemo, useCallback or useEffect)

![Example of Rename State](https://github.com/wix/vscode-glean/blob/master/assets/use-callback.gif?raw=true)

### Render Conditionally

Expand All @@ -59,14 +78,6 @@ Select text and either VSCode's code suggestion (aka "Lightbulb") or Command Pal

![Example of Javascript Extraction](https://github.com/wix/vscode-glean/blob/master/assets/extract-to-file.gif?raw=true)

## Experiments Features

All the experimental features are opt-in and need to be enabled through the configuration.

### Hooks Support for Class Component to Functional Component Refactoring

![Example of Hooks Support](https://github.com/wix/vscode-glean/blob/master/assets/hooks.gif?raw=true)

## Configuration Options

#### glean.jsModuleSystem (Default: 'esm')
Expand All @@ -85,11 +96,9 @@ Determines whether VSCode should switch to target file after extracting.

A list of enabled experimental features. Available experimental features:

- "hooksForFunctionalComponents" - Hooks Support

#### glean.showConversionWarning (Default: true)

Determines whether VSCode should show conversion warning when converting Stateful Component to Functional Component.
Determines whether VSCode should show conversion warning when converting Class Component to Functional Component.

## Contribute

Expand Down
Binary file added assets/rename-state.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/use-callback.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@
"url": "https://github.com/wix/vscode-glean.git"
},
"icon": "assets/icon.png"
}
}
28 changes: 28 additions & 0 deletions src/ast-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as t from "@babel/types";
import traverse from "@babel/traverse";


export function getReactImportReference(ast): t.ImportDeclaration {
return ast.program.body.find(statement => {
Expand All @@ -11,3 +13,29 @@ export function getReactImportReference(ast): t.ImportDeclaration {
export function isExportedDeclaration(ast) {
return t.isExportNamedDeclaration(ast) || t.isExportDefaultDeclaration(ast);
}

export function findPathInContext(ast, identifierName) {
let foundPath = null;
const visitor = {
Identifier(path) {
if (!foundPath && path.node.name === identifierName) {
foundPath = path;
}
}
};

traverse(ast, visitor);
return foundPath;
}

export function pathContains(path, start, end) {
if (!path.node) return false;
const pathStart = path.node.loc.start;
const pathEnd = path.node.loc.end;
return (
(
(pathStart.line === start.line && pathStart.column >= start.character)) &&
(
(pathEnd.line >= end.line && pathEnd.column >= end.character))
);
}
2 changes: 2 additions & 0 deletions src/editor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import * as path from "path";
import { QuickPickItem } from "vscode";
import * as fs from 'fs';

export const workspaceRoot = () => vscode.workspace.rootPath;

Expand Down Expand Up @@ -98,3 +99,4 @@ export const importMissingDependencies = (targetFile) =>
targetFile,
{ fixId: "fixMissingImport" }
);

42 changes: 42 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { isStatefulComp, statefulToStatelessComponent } from './modules/stateful
import { extractToFile } from './modules/extract-to-file';
import { extractJSXToComponentToFile, extractJSXToComponent } from './modules/extract-to-component';
import { wrapJSXWithCondition } from './modules/wrap-with-conditional';
import { renameState, isStateVariable } from './modules/rename-state';
import { wrapWithUseEffect, isInsideOfFunctionBody } from './modules/wrap-with-useeffect';
import { isFunctionInsideAFunction, wrapWithUseCallback } from './modules/wrap-with-usecallback';
import { isVariableDeclarationWithNonFunctionInit, wrapWithUseMemo } from './modules/wrap-with-usememo';

export class CompleteActionProvider implements vscode.CodeActionProvider {
public provideCodeActions(): ProviderResult<vscode.Command[]> {
Expand All @@ -19,6 +23,34 @@ export class CompleteActionProvider implements vscode.CodeActionProvider {

const text = selectedText()

if (isStateVariable(text)) {
return [{
command: 'extension.glean.react.rename-state-hook',
title: 'Rename State'
}];
}

if (isVariableDeclarationWithNonFunctionInit(text)) {
return [{
command: 'extension.glean.react.wrap-with-usememo',
title: 'Wrap with useMemo'
}];
}

if (isFunctionInsideAFunction()) {
return [{
command: 'extension.glean.react.wrap-with-usecallback',
title: 'Wrap with useCallback'
}];
}

if (isInsideOfFunctionBody(text) && !isJSX(text)) {
return [{
command: 'extension.glean.react.wrap-with-useeffect',
title: 'Wrap with useEffect'
}];
}

if (isJSX(text)) {
return [{
command: 'extension.glean.react.extract-component-to-file',
Expand Down Expand Up @@ -48,6 +80,7 @@ export class CompleteActionProvider implements vscode.CodeActionProvider {
}]
}


return [exportToFileAction];
}
}
Expand All @@ -69,6 +102,15 @@ export function activate(context: vscode.ExtensionContext) {

vscode.commands.registerCommand('extension.glean.react.stateful-to-stateless', statefulToStatelessComponent);

vscode.commands.registerCommand('extension.glean.react.rename-state-hook', renameState);

vscode.commands.registerCommand('extension.glean.react.wrap-with-useeffect', wrapWithUseEffect);

vscode.commands.registerCommand('extension.glean.react.wrap-with-usecallback', wrapWithUseCallback);

vscode.commands.registerCommand('extension.glean.react.wrap-with-usememo', wrapWithUseMemo);


}


Expand Down
56 changes: 56 additions & 0 deletions src/modules/rename-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { codeToAst } from "../parsing";
import { selectedText, allText, selectedTextStart, selectedTextEnd, activeFileName, activeEditor, showInputBox } from "../editor";
import traverse from "@babel/traverse";
import { transformFromAst } from "@babel/core";
import { window, Position, Range } from "vscode";
import { persistFileSystemChanges, replaceTextInFile } from "../file-system";
import { capitalizeFirstLetter } from "../utils";
import * as t from "@babel/types";
import { findPathInContext } from "../ast-helpers";

function isPathOnLines(path, start, end) {
if (!path.node) return false;
const pathStart = path.node.loc.start;
const pathEnd = path.node.loc.end;
return (
(pathStart.line === start.line && pathStart.column === start.character) &&
(pathEnd.line === end.line && pathEnd.column === end.character))

}

export function isStateVariable(text) {
try {
const allAST = codeToAst(allText());
const containerPath = findPathInContext(allAST, text);
const variableDeclarationParent = containerPath.scope.bindings[containerPath.node.name].path.parentPath;
return variableDeclarationParent && variableDeclarationParent.node.declarations[0].init.callee.name === 'useState'
} catch (e) {
return false;
}
}

export async function renameState() {
const selectedStateVariable = selectedText();
const varName = await showInputBox(null, 'Name of the state variable');

const allAST = codeToAst(allText());
const containerPath = findPathInContext(allAST, selectedStateVariable);
const variableDeclarationParent = containerPath.findParent(parent => t.isVariableDeclaration(parent));
if (variableDeclarationParent && variableDeclarationParent.node.declarations[0].init.callee.name === 'useState') {
containerPath.scope.bindings[selectedStateVariable].path.parentPath.get('declarations.0.id.elements.0').scope.rename(selectedStateVariable, varName);
containerPath.scope.bindings[varName].path.parentPath.get('declarations.0.id.elements.1').scope.rename(`set${capitalizeFirstLetter(selectedStateVariable)}`, `set${capitalizeFirstLetter(varName)}`);

const processedJSX = transformFromAst(allAST).code;
const endLine = activeEditor().document.lineAt(activeEditor().document.lineCount - 1).range;
const change = replaceTextInFile(
processedJSX,
new Position(0, 0),
endLine.end,
activeFileName()
);


return persistFileSystemChanges(change)

}
}
Loading

0 comments on commit eba2954

Please sign in to comment.