Skip to content

Commit

Permalink
feat(general): Glean V5 (#61)
Browse files Browse the repository at this point in the history
* Class to Functional - useState hook support

* Class to Functional - improved Typescript support

* Auto-import React if needed

* Extract Component - always generate functional component
  • Loading branch information
borislit authored Feb 5, 2019
1 parent 218878d commit 5ad9a80
Show file tree
Hide file tree
Showing 21 changed files with 1,049 additions and 638 deletions.
68 changes: 45 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
</p>

# VSCode Glean
> The extension provides refactoring tools for your React/Javascript/Typescript codebase

> The extension provides refactoring tools for your React/Javascript/Typescript 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 Functions to Stateful React 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 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!

## Highlights
- Allows extracting JSX into new components as well as other React code refactoring options

- Allows extracting JSX into new component
- Allows converting Class Components to Functional Components and vice-verse
- Allows wrapping JSX with conditional
- Moving code between files
- Typescript support
- ES2015 modules support
- CommonJS modules support
- Plain Text support

## Installation

Expand All @@ -28,16 +29,8 @@ Go to the link below and click `Install`.

## Features

### Extract to file

Select text and use either VSCode's code suggestion (aka "Lightbulb") or Command Pallet ('Extract to File' command) to extract the text.
### Extracting JSX into a new Component


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


### JSX Support
#### 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:

- Generate Stateful or Stateless Component, such that the extracted JSX will continue to function.
Expand All @@ -46,30 +39,53 @@ Glean allows easy extraction of JSX into new React components. Just select the J

![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 Functional Component to Stateful 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
### Converting Stateful Component to Functional Component

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

**WARNING!!! This refactoring will delete all Lifecycle methods and setState calls!**

#### Render Conditionally
### Render Conditionally

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

### Extract to file

Select text and either VSCode's code suggestion (aka "Lightbulb") or Command Pallet ('Extract to File' command) to extract the text.

![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')

Determines how the selected code will be exported/imported. Valid options are 'esm' and 'commonjs'.

#### glean.jsFilesExtensions (Default: [ "js", "jsx", "ts", "tsx" ])

List of extensions of files that should be treated as javascript files. This determines whether or not the snippet will be exported and imported. The snippet will be treated as javascript only if the extension of both origin and target files appears in this list.

#### glean.switchToTarget (Default: false)

Determines whether VSCode should switch to target file after extracting.

#### glean.experiments (Default: [])

A list of enabled experimental features. Available experimental features:

- "hooksForFunctionalComponents" - Hooks Support

## Contribute

Expand All @@ -78,6 +94,7 @@ Feel free to open issues or PRs!
### Getting started

In order to start working all you need to do is:

```sh
$ git clone [email protected]:wix/vscode-glean.git
$ cd vscode-glean
Expand All @@ -86,12 +103,17 @@ $ code .
```

### Running Extension
* Go to VSCode debug sidebar
* Select `Extension` from the dropdown
* Hit `F5`

- Go to VSCode debug sidebar
- Select `Extension` from the dropdown
- Hit `F5`

### Running Tests
* Go to VSCode debug sidebar
* Select `Extension Tests` from the dropdown
* Hit `F5`

- Go to VSCode debug sidebar
- Select `Extension Tests` from the dropdown
- Hit `F5`

### Commit messages

Please refer to to the following [guide](https://marketplace.visualstudio.com/items?itemName=wix.glean).
Binary file added assets/hooks.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
],
"configuration": {
"type": "object",
"title": "Glean Configuration",
"title": "Glean",
"properties": {
"glean.jsFilesExtensions": {
"type": [
Expand All @@ -58,19 +58,27 @@
"type": "boolean",
"default": false,
"description": "Specifies whether to switch to the target file after extracting the content or not"
},
"glean.experiments": {
"type": [
"string"
],
"default": [],
"description": "Active experimental features. Please see https://github.com/wix/vscode-glean/ for list of experimental features"
}
}
}
},
},
"scripts": {
"vscode:prepublish": "npm test",
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "npm run compile && node ./node_modules/vscode/bin/test",
"semantic-release": "semantic-release"
},
"devDependencies": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@semantic-release/changelog": "^2.1.2",
"@semantic-release/git": "^5.0.0",
Expand All @@ -83,10 +91,12 @@
"@types/sinon": "^4.3.0",
"@types/sinon-chai": "^2.7.29",
"chai": "^4.2.0",
"prettier": "1.15.3",
"semantic-release": "^15.10.0",
"semantic-release-vsce": "^2.1.2",
"sinon": "^4.5.0",
"sinon-chai": "^3.0.0",
"tslint-config-prettier": "^1.17.0",
"typescript": "^2.6.1",
"vscode": "^1.1.21"
},
Expand Down
13 changes: 13 additions & 0 deletions src/ast-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as t from "@babel/types";

export function getReactImportReference(ast): t.ImportDeclaration {
return ast.program.body.find(statement => {
return (
t.isImportDeclaration(statement) && statement.source.value === "react"
);
});
}

export function isExportedDeclaration(ast) {
return t.isExportNamedDeclaration(ast) || t.isExportDefaultDeclaration(ast);
}
105 changes: 4 additions & 101 deletions src/code-actions.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,18 @@
import { SnippetString } from 'vscode';
import { showDirectoryPicker } from "./directories-picker";
import { showFilePicker } from "./file-picker";
import { activeEditor, selectedText, activeFileName, openFile, selectedTextStart, selectedTextEnd, showErrorMessage, showInformationMessage, allText } from "./editor";
import { statelessToStateful } from "./modules/statless-to-stateful";
import { statefulToStateless } from './modules/stateful-to-stateless'
import { activeFileName, openFile, selectedTextStart, selectedTextEnd, showErrorMessage } from "./editor";
import { shouldSwitchToTarget, shouldBeConsideredJsFiles } from "./settings";
import { replaceTextInFile, appendTextToFile, prependTextToFile, removeContentFromFileAtLineAndColumn } from "./file-system";
import { getIdentifier, generateImportStatementFromFile, transformJSIntoExportExpressions } from "./parsing";
import { createComponentInstance, wrapWithComponent, isRangeContainedInJSXExpression, isJSXExpression } from "./modules/jsx";
import * as relative from 'relative';
import * as path from 'path';

export async function extractJSXToComponent() {
var editor = activeEditor();
if (!editor) {
return; // No open text editor
}

try {
const folderPath = await showDirectoryPicker()
const filePath = await showFilePicker(folderPath);

const selectionProccessingResult = await wrapWithComponent(filePath, selectedText());
await appendSelectedTextToFile(selectionProccessingResult, filePath);
await prependImportsToFileIfNeeded(selectionProccessingResult, filePath);
const componentInstance = createComponentInstance(selectionProccessingResult.metadata.name, selectionProccessingResult.metadata.componentProperties);
await replaceSelectionWith(componentInstance);
await switchToDestinationFileIfRequired(filePath);
} catch (e) {
handleError(e);
}
}

export async function wrapJSXWithCondition() {
var editor = activeEditor();
if (!editor) {
return; // No open text editor
}

try {
const selText = selectedText();
const isParentJSXExpression = isRangeContainedInJSXExpression(allText(), selectedTextStart(), selectedTextEnd());
const conditionalJSX = isJSXExpression(selText) ? selText : `<>${selText}</>`;
const snippetInnerText = `\n$\{1:true\}\n? ${conditionalJSX}\n: $\{2:null\}\n`;
const snippetText = isParentJSXExpression ? `{${snippetInnerText}}` : `(${snippetInnerText})`;
await editor.insertSnippet(new SnippetString(snippetText));
} catch (e) {
handleError(e);
}
}

export async function extractToFile() {
var editor = activeEditor();
if (!editor) {
return; // No open text editor
}

try {
const folderPath = await showDirectoryPicker()
const filePath = await showFilePicker(folderPath);

const selectionProccessingResult: ProcessedSelection = {
text: selectedText(),
metadata: {}
};
await appendSelectedTextToFile(selectionProccessingResult, filePath);
await removeSelectedTextFromOriginalFile(selectionProccessingResult);
await prependImportsToFileIfNeeded(selectionProccessingResult, filePath);

await switchToDestinationFileIfRequired(filePath);

} catch (e) {
handleError(e);
}
}

export async function statelessToStatefulComponent() {
try {
const selectionProccessingResult = statelessToStateful(selectedText())
await replaceSelectionWith(selectionProccessingResult.text);

} catch (e) {
handleError(e);
}
}

export async function statefulToStatelessComponent() {
try {
await showInformationMessage('WARNING! All lifecycle methods and react instance methods would be removed. Are you sure you want to continue?', ['Yes', 'No']).then(async res => {
if (res === 'Yes') {
const selectionProccessingResult = statefulToStateless(selectedText())
await replaceSelectionWith(selectionProccessingResult.text);
}
});
} catch (e) {
handleError(e);
}
}
import * as path from 'path'

export async function switchToDestinationFileIfRequired(destinationFilePath: any) {
if (shouldSwitchToTarget()) {
await openFile(destinationFilePath);
}
}

export async function replaceSelectionWith(text: string, path = activeFileName()) {
await replaceTextInFile(text, selectedTextStart(), selectedTextEnd(), activeFileName());
export function replaceSelectionWith(text: string) {
return replaceTextInFile(text, selectedTextStart(), selectedTextEnd(), activeFileName());
}

export type ProcessedSelection = {
Expand Down Expand Up @@ -141,11 +48,7 @@ export const prependImportsToFileIfNeeded = ({
const importStatement = generateImportStatementFromFile(identifiers, destinationModule);
return prependTextToFile(importStatement, originFilePath);
};
export const removeSelectedTextFromOriginalFile = selection => {
let content = '';

return removeContentFromFileAtLineAndColumn(selectedTextStart(), selectedTextEnd(), activeFileName(), content);
};
export const isOperationBetweenJSFiles = destinationPath => shouldBeConsideredJsFiles(activeFileName(), destinationPath);

export const handleError = e => {
Expand Down
1 change: 0 additions & 1 deletion src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export const activeEditor = () => vscode.window.activeTextEditor;

export const config = () => vscode.workspace.getConfiguration('glean');


export function currentEditorPath(): string {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) return;
Expand Down
8 changes: 6 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { extractToFile, statelessToStatefulComponent, statefulToStatelessComponent, extractJSXToComponent, wrapJSXWithCondition } from './code-actions';
'use strict';

import * as vscode from 'vscode';
import { selectedText } from './editor';
import { isStatelessComp, isStatefulComp, isJSX } from './modules/jsx';
import { isJSX } from './modules/jsx';
import { ProviderResult } from 'vscode';
import { isStatelessComp, statelessToStatefulComponent } from './modules/statless-to-stateful';
import { isStatefulComp, statefulToStatelessComponent } from './modules/stateful-to-stateless';
import { extractToFile } from './modules/extract-to-file';
import { extractJSXToComponent } from './modules/extract-to-component';
import { wrapJSXWithCondition } from './modules/wrap-with-conditional';

export class CompleteActionProvider implements vscode.CodeActionProvider {
public provideCodeActions(): ProviderResult<vscode.Command[]> {
Expand Down
16 changes: 11 additions & 5 deletions src/file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export function createFileIfDoesntExist(absolutePath: string): string {
return absolutePath;
}

export function readFileContent(absolutePath) {
return fs.readFileSync(absolutePath, 'utf8');
}

export function subfoldersListOf(root: string, ignoreList): string[] {
if (!root) {
return [];
Expand All @@ -38,12 +42,8 @@ export function filesInFolder(folder): string[] {
return results;
}

export function replaceTextInFile(text, start: vscode.Position, end: vscode.Position, path) {
const edit = new vscode.WorkspaceEdit();
edit.replace(Uri.file(path), new vscode.Range(start, end), text);
return vscode.workspace.applyEdit(edit);
export const replaceTextInFile = (text, start: vscode.Position, end: vscode.Position, path) => edit => edit.replace(Uri.file(path), new vscode.Range(start, end), text);

}
export async function appendTextToFile(text, absolutePath) {
const edit = new vscode.WorkspaceEdit();
const linesInFile = await countLineInFile(absolutePath);
Expand All @@ -60,6 +60,12 @@ export async function appendTextToFile(text, absolutePath) {
// });
}

export function persistFileSystemChanges(...changes) {
const accumulatedEdit = new vscode.WorkspaceEdit();
changes.forEach(addChangeTo => addChangeTo(accumulatedEdit))
return vscode.workspace.applyEdit(accumulatedEdit);
}

export function prependTextToFile(text, absolutePath) {
let edit = new vscode.WorkspaceEdit();
edit.insert(Uri.file(absolutePath), new vscode.Position(0, 0), text);
Expand Down
Loading

0 comments on commit 5ad9a80

Please sign in to comment.