├── .gitignore
├── assets
├── logo.png
├── logo-square.png
├── extension-demo.gif
├── logo.svg
└── logo-square.svg
├── src
├── test
│ ├── components
│ │ ├── shortHand
│ │ │ ├── export.js
│ │ │ ├── export.ts
│ │ │ ├── shortHand.jsx
│ │ │ ├── shortHand.tsx
│ │ │ ├── shortHandResult.jsx
│ │ │ └── shortHandResult.tsx
│ │ ├── onlyStatic
│ │ │ ├── export.js
│ │ │ ├── export.ts
│ │ │ ├── onlyStatic.jsx
│ │ │ ├── onlyStatic.tsx
│ │ │ ├── onlyStaticResult.jsx
│ │ │ └── onlyStaticResult.tsx
│ │ ├── static
│ │ │ ├── export.js
│ │ │ ├── export.ts
│ │ │ ├── static.jsx
│ │ │ ├── static.tsx
│ │ │ ├── staticResult.jsx
│ │ │ └── staticResult.tsx
│ │ ├── spread
│ │ │ ├── spread.jsx
│ │ │ ├── spreadResult.jsx
│ │ │ ├── spread.tsx
│ │ │ └── spreadResult.tsx
│ │ ├── destructureRename
│ │ │ ├── destructureRename.jsx
│ │ │ ├── destructureRename.tsx
│ │ │ ├── destructureRenameResult.jsx
│ │ │ └── destructureRenameResult.tsx
│ │ ├── spreadAny
│ │ │ ├── spreadAny.tsx
│ │ │ └── spreadAnyResult.tsx
│ │ ├── wrapInFragment
│ │ │ ├── wrapInFragment.jsx
│ │ │ ├── wrapInFragment.tsx
│ │ │ ├── wrapInFragmentResult.jsx
│ │ │ └── wrapInFragmentResult.tsx
│ │ ├── noProps
│ │ │ ├── noProps.jsx
│ │ │ ├── noProps.tsx
│ │ │ ├── noPropsResult.jsx
│ │ │ └── noPropsResult.tsx
│ │ ├── typeTypeDeclarationEmpty
│ │ │ ├── typeTypeDeclarationEmpty.tsx
│ │ │ └── typeTypeDeclarationEmptyResult.tsx
│ │ ├── undestructuredPropsEmpty
│ │ │ ├── undestructuredPropsEmpty.jsx
│ │ │ ├── undestructuredPropsEmpty.tsx
│ │ │ ├── undestructuredPropsEmptyResult.jsx
│ │ │ └── undestructuredPropsEmptyResult.tsx
│ │ ├── arrowFunctionDeclarationEmpty
│ │ │ ├── arrowFunctionDeclarationEmpty.jsx
│ │ │ ├── arrowFunctionDeclarationEmpty.tsx
│ │ │ ├── arrowFunctionDeclarationEmptyResult.jsx
│ │ │ └── arrowFunctionDeclarationEmptyResult.tsx
│ │ ├── typeInlineDeclarationEmpty
│ │ │ ├── typeInlineDeclarationEmpty.tsx
│ │ │ └── typeInlineDeclarationEmptyResult.tsx
│ │ ├── undestructuredPropsSpreadAttribute
│ │ │ ├── undestructuredPropsSpreadAttribute.jsx
│ │ │ ├── undestructuredPropsSpreadAttribute.tsx
│ │ │ ├── undestructuredPropsSpreadAttributeResult.jsx
│ │ │ └── undestructuredPropsSpreadAttributeResult.tsx
│ │ ├── destructureNested
│ │ │ ├── destructureNested.jsx
│ │ │ ├── destructureNested.tsx
│ │ │ ├── destructureNestedResult.jsx
│ │ │ └── destructureNestedResult.tsx
│ │ ├── reactFCTypeEmpty
│ │ │ ├── reactFCTypeEmpty.tsx
│ │ │ └── reactFCTypeEmptyResult.tsx
│ │ ├── fragment
│ │ │ ├── fragment.jsx
│ │ │ ├── fragment.tsx
│ │ │ ├── fragmentResult.jsx
│ │ │ └── fragmentResult.tsx
│ │ ├── arrowFunctionExplicitReturn
│ │ │ ├── arrowFunctionExplicitReturn.jsx
│ │ │ └── arrowFunctionExplicitReturnResult.jsx
│ │ ├── parameter
│ │ │ ├── parameter.jsx
│ │ │ ├── parameterResult.jsx
│ │ │ ├── parameter.tsx
│ │ │ └── parameterResult.tsx
│ │ ├── reactFCType
│ │ │ ├── reactFCType.jsx
│ │ │ ├── reactFCType.tsx
│ │ │ ├── reactFCTypeResult.jsx
│ │ │ └── reactFCTypeResult.tsx
│ │ ├── textChild
│ │ │ ├── textChild.jsx
│ │ │ ├── textChild.tsx
│ │ │ ├── textChildResult.jsx
│ │ │ └── textChildResult.tsx
│ │ ├── undestructuredProps
│ │ │ ├── undestructuredProps.jsx
│ │ │ ├── undestructuredProps.tsx
│ │ │ ├── undestructuredPropsResult.jsx
│ │ │ └── undestructuredPropsResult.tsx
│ │ ├── spreadArray
│ │ │ ├── spreadArray.jsx
│ │ │ ├── spreadArray.tsx
│ │ │ ├── spreadArrayResult.jsx
│ │ │ └── spreadArrayResult.tsx
│ │ ├── typeTypeDeclaration
│ │ │ ├── typeTypeDeclaration.tsx
│ │ │ └── typeTypeDeclarationResult.tsx
│ │ ├── typeInlineDeclaration
│ │ │ ├── typeInlineDeclaration.tsx
│ │ │ └── typeInlineDeclarationResult.tsx
│ │ ├── typeInlineDeclarationReactFC
│ │ │ ├── typeInlineDeclarationReactFC.tsx
│ │ │ └── typeInlineDeclarationReactFCResult.tsx
│ │ ├── arrowFunctionDeclarationSpread
│ │ │ ├── arrowFunctionDeclarationSpread.jsx
│ │ │ ├── arrowFunctionDeclarationSpread.tsx
│ │ │ ├── arrowFunctionDeclarationSpreadResult.jsx
│ │ │ └── arrowFunctionDeclarationSpreadResult.tsx
│ │ ├── parameterTypeReference
│ │ │ ├── parameterTypeReference.tsx
│ │ │ └── parameterTypeReferenceResult.tsx
│ │ ├── componentAsProps
│ │ │ ├── componentAsProps.jsx
│ │ │ ├── componentAsProps.tsx
│ │ │ ├── componentAsPropsResult.jsx
│ │ │ └── componentAsPropsResult.tsx
│ │ ├── spreadNestedTypeReference
│ │ │ ├── spreadNestedTypeReference.tsx
│ │ │ └── spreadNestedTypeReferenceResult.tsx
│ │ ├── typeTypeDeclarationExtended
│ │ │ ├── typeTypeDeclarationExtended.tsx
│ │ │ └── typeTypeDeclarationExtendedResult.tsx
│ │ ├── typeInlineDeclarationExtended
│ │ │ ├── typeInlineDeclarationExtended.tsx
│ │ │ └── typeInlineDeclarationExtendedResult.tsx
│ │ ├── implicit
│ │ │ ├── implicit.jsx
│ │ │ ├── implicit.tsx
│ │ │ ├── implicitResult.jsx
│ │ │ └── implicitResult.tsx
│ │ ├── spreadNested
│ │ │ ├── spreadNested.jsx
│ │ │ ├── spreadNestedResult.jsx
│ │ │ ├── spreadNested.tsx
│ │ │ └── spreadNestedResult.tsx
│ │ ├── componentAsFunction
│ │ │ ├── componentAsFunction.jsx
│ │ │ ├── componentAsFunction.tsx
│ │ │ ├── componentAsFunctionResult.jsx
│ │ │ └── componentAsFunctionResult.tsx
│ │ ├── conditional
│ │ │ ├── conditional.jsx
│ │ │ ├── conditional.tsx
│ │ │ ├── conditionalResult.jsx
│ │ │ └── conditionalResult.tsx
│ │ ├── map
│ │ │ ├── map.jsx
│ │ │ ├── map.tsx
│ │ │ ├── mapResult.jsx
│ │ │ └── mapResult.tsx
│ │ ├── subSelection
│ │ │ ├── subSelection.jsx
│ │ │ ├── subSelection.tsx
│ │ │ ├── subSelectionResult.jsx
│ │ │ └── subSelectionResult.tsx
│ │ ├── propertiesAndMethods
│ │ │ ├── propertiesAndMethods.jsx
│ │ │ ├── propertiesAndMethods.tsx
│ │ │ ├── propertiesAndMethodsResult.jsx
│ │ │ └── propertiesAndMethodsResult.tsx
│ │ ├── longType
│ │ │ ├── longType.tsx
│ │ │ └── longTypeResult.tsx
│ │ └── undestructuredPropsExtended
│ │ │ ├── undestructuredPropsExtended.jsx
│ │ │ └── undestructuredPropsExtendedResult.jsx
│ ├── createTestCase.js
│ ├── checks.test.ts
│ ├── isJsx.test.ts
│ ├── utils.test.ts
│ ├── common.ts
│ └── extractComponent.test.ts
├── checks.ts
├── parsers
│ ├── parsingUtils.ts
│ ├── fragment.ts
│ ├── isJsx.ts
│ ├── replacePropsWithFullPath.ts
│ ├── getNodeType.ts
│ └── extractProps.ts
├── typescriptProgram.ts
├── utils.ts
├── extension.ts
├── types.ts
└── extractComponent.ts
├── .vscode-test.mjs
├── tsconfig.json
├── .prettierrc
├── .vscode
├── extensions.json
├── tasks.json
├── settings.json
└── launch.json
├── .vscodeignore
├── .eslintrc.json
├── LICENSE
├── webpack.config.js
├── CHANGELOG.md
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | .vscode-test/
5 | *.vsix
6 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joao-mbn/react-extract/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/src/test/components/shortHand/export.js:
--------------------------------------------------------------------------------
1 | export const shortHandImport = 'shortHandImport';
2 |
3 |
--------------------------------------------------------------------------------
/src/test/components/shortHand/export.ts:
--------------------------------------------------------------------------------
1 | export const shortHandImport = 'shortHandImport';
2 |
3 |
--------------------------------------------------------------------------------
/assets/logo-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joao-mbn/react-extract/HEAD/assets/logo-square.png
--------------------------------------------------------------------------------
/assets/extension-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joao-mbn/react-extract/HEAD/assets/extension-demo.gif
--------------------------------------------------------------------------------
/src/test/components/onlyStatic/export.js:
--------------------------------------------------------------------------------
1 | export const VALUE = 42;
2 | export const CLASS_NAME = 'class';
3 |
--------------------------------------------------------------------------------
/src/test/components/onlyStatic/export.ts:
--------------------------------------------------------------------------------
1 | export const VALUE = 42;
2 | export const CLASS_NAME = 'class';
3 |
--------------------------------------------------------------------------------
/src/test/components/static/export.js:
--------------------------------------------------------------------------------
1 | export const VALUE = 42;
2 | export const CLASS_NAME = 'class';
3 |
--------------------------------------------------------------------------------
/src/test/components/static/export.ts:
--------------------------------------------------------------------------------
1 | export const VALUE = 42;
2 | export const CLASS_NAME = 'class';
3 |
--------------------------------------------------------------------------------
/src/test/components/spread/spread.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ ...props }) {
4 | return
;
5 | }
6 |
--------------------------------------------------------------------------------
/src/checks.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 |
3 | export function isFileTypescript(document: vscode.TextDocument) {
4 | return document.languageId === 'typescript' || document.languageId === 'typescriptreact';
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/src/test/components/destructureRename/destructureRename.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const { colour: color } = { colour: 'red' };
5 |
6 | return Test
;
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/test/components/destructureRename/destructureRename.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const { colour: color } = { colour: 'red' };
5 |
6 | return Test
;
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/test/components/spreadAny/spreadAny.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ children, ...props }: any) {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/.vscode-test.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@vscode/test-cli';
2 |
3 | export default defineConfig({
4 | files: 'out/test/**/*.test.js',
5 | mocha: {
6 | timeout: 1000000
7 | },
8 | launchArgs: ['--disable-extensions']
9 | });
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "Node16",
5 | "target": "ES2022",
6 | "lib": ["ES2022"],
7 | "sourceMap": true,
8 | "rootDir": "src",
9 | "strict": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/components/spread/spreadResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ ...props }) {
4 | return ;
5 | }
6 |
7 | function Extracted({ ...props }) {
8 | return
;
9 | }
10 |
--------------------------------------------------------------------------------
/src/test/components/wrapInFragment/wrapInFragment.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/components/wrapInFragment/wrapInFragment.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
6 |
7 | {'Hello'}0
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/components/noProps/noProps.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
6 |
Test
7 |
Span 1
8 |
Span 2
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/components/noProps/noProps.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
6 |
Test
7 |
Span 1
8 |
Span 2
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/components/typeTypeDeclarationEmpty/typeTypeDeclarationEmpty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsEmpty/undestructuredPropsEmpty.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsEmpty/undestructuredPropsEmpty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationEmpty/arrowFunctionDeclarationEmpty.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclarationEmpty/typeInlineDeclarationEmpty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationEmpty/arrowFunctionDeclarationEmpty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsSpreadAttribute/undestructuredPropsSpreadAttribute.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ children, ...props }) {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/test/components/destructureNested/destructureNested.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const {
5 | styles: { color },
6 | text
7 | } = { styles: { color: 'red' }, text: 'Test' };
8 |
9 | return {text}
;
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/test/components/destructureNested/destructureNested.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const {
5 | styles: { color },
6 | text
7 | } = { styles: { color: 'red' }, text: 'Test' };
8 |
9 | return {text}
;
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/test/components/reactFCTypeEmpty/reactFCTypeEmpty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
9 | );
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/test/components/fragment/fragment.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 | <>
6 |
7 |
8 | <>
9 | Test
10 | Test
11 | >
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/test/components/fragment/fragment.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 | <>
6 |
7 |
8 | <>
9 | Test
10 | Test
11 | >
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true,
4 | "jsxSingleQuote": true,
5 | "useTabs": false,
6 | "tabWidth": 2,
7 | "semi": true,
8 | "bracketSpacing": true,
9 | "trailingComma": "none",
10 | "bracketSameLine": true,
11 | "arrowParens": "always",
12 | "endOfLine": "crlf"
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationEmpty/arrowFunctionDeclarationEmptyResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | const Extracted = () => (
8 |
11 | );
12 |
13 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationEmpty/arrowFunctionDeclarationEmptyResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | const Extracted = () => (
8 |
11 | );
12 |
13 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionExplicitReturn/arrowFunctionExplicitReturn.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
9 | );
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/test/components/parameter/parameter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component(props) {
4 | return (
5 |
6 |
{props.children}
7 |
8 |
9 | );
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/test/components/destructureRename/destructureRenameResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const { colour: color } = { colour: 'red' };
5 |
6 | return ;
7 | }
8 |
9 | function Extracted({ color }) {
10 | return Test
;
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/test/components/reactFCType/reactFCType.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const myClass = 'my-class';
5 |
6 | return (
7 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint",
6 | "amodio.tsl-problem-matcher",
7 | "ms-vscode.extension-test-runner",
8 | "esbenp.prettier-vscode"
9 | ]
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/test/components/noProps/noPropsResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 |
10 |
Test
11 |
Span 1
12 |
Span 2
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/components/noProps/noPropsResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 |
10 |
Test
11 |
Span 1
12 |
Span 2
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/components/reactFCType/reactFCType.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const myClass: string = 'my-class';
5 |
6 | return (
7 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/typeTypeDeclarationEmpty/typeTypeDeclarationEmptyResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 |
12 | );
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsEmpty/undestructuredPropsEmptyResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 |
12 | );
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsEmpty/undestructuredPropsEmptyResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 |
12 | );
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclarationEmpty/typeInlineDeclarationEmptyResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 |
12 | );
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsSpreadAttribute/undestructuredPropsSpreadAttribute.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component({ children, ...props }: ComponentPropsWithoutRef<'div'>) {
4 | return (
5 |
8 | );
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/test/components/textChild/textChild.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const greeting = 'Ça va? Ça va!';
5 |
6 | return (
7 |
8 |
In French, a conversation can enter an infinite loop with: {greeting}
9 |
Be careful!
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/components/reactFCTypeEmpty/reactFCTypeEmptyResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | const Extracted: React.FC = () => (
8 |
12 | );
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/textChild/textChild.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const greeting: string = 'Ça va? Ça va!';
5 |
6 | return (
7 |
8 |
In French, a conversation can enter an infinite loop with: {greeting}
9 |
Be careful!
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredProps/undestructuredProps.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass = 'my-class';
5 | const baseClass = 'my-class-2';
6 |
7 | return (
8 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/wrapInFragment/wrapInFragmentResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
11 | function Extracted() {
12 | return (
13 | <>
14 |
15 | {'Hello'}0
16 |
17 | >
18 | );
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/test/components/wrapInFragment/wrapInFragmentResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
11 | function Extracted() {
12 | return (
13 | <>
14 |
15 | {'Hello'}0
16 |
17 | >
18 | );
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/test/components/fragment/fragmentResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 | <>
10 |
11 |
12 | <>
13 | Test
14 | Test
15 | >
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/components/fragment/fragmentResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | function Extracted() {
8 | return (
9 | <>
10 |
11 |
12 | <>
13 | Test
14 | Test
15 | >
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/components/spread/spread.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | interface ComponentProps {
4 | style: { color: string; nested1: string; nested2: string };
5 | children: ReactNode;
6 | prop1: string[];
7 | prop2: string;
8 | prop3: string;
9 | }
10 |
11 | function Component({ ...props }: ComponentProps) {
12 | return
;
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/components/spreadArray/spreadArray.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component(props) {
4 | const {
5 | items: [item1, item2, ...otherItems]
6 | } = props;
7 |
8 | return (
9 |
10 |
{item1}
11 |
{item2}
12 |
{otherItems.join(', ')}
13 |
14 | );
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/test/components/typeTypeDeclaration/typeTypeDeclaration.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return (
8 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredProps/undestructuredProps.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return (
8 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclaration/typeInlineDeclaration.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return (
8 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclarationReactFC/typeInlineDeclarationReactFC.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return (
8 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationSpread/arrowFunctionDeclarationSpread.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }) {
4 | return (
5 |
6 |
7 | {children}
8 |
9 |
10 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionExplicitReturn/arrowFunctionExplicitReturnResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | return ;
5 | }
6 |
7 | const Extracted = () => {
8 | return (
9 |
13 | );
14 | };
15 |
16 |
--------------------------------------------------------------------------------
/src/test/components/reactFCType/reactFCTypeResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const myClass = 'my-class';
5 |
6 | return ;
7 | }
8 |
9 | const Extracted = ({ myClass }) => (
10 |
14 | );
15 |
16 |
--------------------------------------------------------------------------------
/src/test/components/destructureNested/destructureNestedResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const {
5 | styles: { color },
6 | text
7 | } = { styles: { color: 'red' }, text: 'Test' };
8 |
9 | return ;
10 | }
11 |
12 | function Extracted({ color, text }) {
13 | return {text}
;
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/src/test/components/destructureRename/destructureRenameResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const { colour: color } = { colour: 'red' };
5 |
6 | return ;
7 | }
8 |
9 | interface ExtractedProps {
10 | color: string;
11 | }
12 |
13 | function Extracted({ color }: ExtractedProps) {
14 | return Test
;
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsSpreadAttribute/undestructuredPropsSpreadAttributeResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ children, ...props }) {
4 | return ;
5 | }
6 |
7 | function Extracted({ children, ...props }) {
8 | return (
9 |
12 | );
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | node_modules/**
4 | src/**
5 | out/**
6 | .gitignore
7 | .prettierrc
8 | CHANGELOG.md
9 | webpack.config.js
10 | vsc-extension-quickstart.md
11 | **/tsconfig.json
12 | **/.eslintrc.json
13 | **/*.map
14 | **/*.ts
15 | **/.vscode-test.*
16 | assets/extension-demo.gif
17 | assets/logo.png
18 | assets/logo.svg
19 | assets/logo-square.svg
20 |
21 | !dist/*.d.ts
22 |
23 |
--------------------------------------------------------------------------------
/src/parsers/parsingUtils.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import * as vscode from 'vscode';
3 |
4 | export function getNodeRange(node: ts.Node, sourceFile: ts.SourceFile) {
5 | const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
6 | const end = sourceFile.getLineAndCharacterOfPosition(node.end);
7 | return new vscode.Range(start.line, start.character, end.line, end.character);
8 | }
9 |
--------------------------------------------------------------------------------
/src/test/components/parameterTypeReference/parameterTypeReference.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component(props: ComponentPropsWithoutRef<'div'>) {
4 | return (
5 |
6 |
{props.children}
7 |
8 |
9 | );
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/test/components/componentAsProps/componentAsProps.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ Header }) {
4 | return (
5 |
6 | {Header}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return (
14 |
15 | Header} />
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/parameter/parameterResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component(props) {
4 | return ;
5 | }
6 |
7 | function Extracted({ props }) {
8 | return (
9 |
10 |
{props.children}
11 |
12 |
13 | );
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/src/test/components/onlyStatic/onlyStatic.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | function Component() {
5 | return (
6 |
7 |
Test
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/components/onlyStatic/onlyStatic.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | function Component() {
5 | return (
6 |
7 |
Test
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/components/spreadNestedTypeReference/spreadNestedTypeReference.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
4 | return (
5 |
6 |
7 | {children}
8 |
9 |
10 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/textChild/textChildResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const greeting = 'Ça va? Ça va!';
5 |
6 | return ;
7 | }
8 |
9 | function Extracted({ greeting }) {
10 | return (
11 |
12 |
In French, a conversation can enter an infinite loop with: {greeting}
13 |
Be careful!
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/components/typeTypeDeclarationExtended/typeTypeDeclarationExtended.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
4 | return (
5 |
6 |
7 | {children}
8 |
9 |
10 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationSpread/arrowFunctionDeclarationSpread.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
4 | return (
5 |
6 |
7 | {children}
8 |
9 |
10 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/componentAsProps/componentAsProps.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | function Child({ Header }: { Header: ReactNode }) {
4 | return (
5 |
6 | {Header}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return (
14 |
15 | Header} />
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/spreadArray/spreadArray.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface ComponentProps {
4 | items: string[];
5 | }
6 |
7 | function Component(props: ComponentProps) {
8 | const {
9 | items: [item1, item2, ...otherItems]
10 | } = props;
11 |
12 | return (
13 |
14 |
{item1}
15 |
{item2}
16 |
{otherItems.join(', ')}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclarationExtended/typeInlineDeclarationExtended.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
4 | return (
5 |
6 |
7 | {children}
8 |
9 |
10 |
11 | );
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/test/components/spreadAny/spreadAnyResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ children, ...props }: any) {
4 | return ;
5 | }
6 |
7 | interface ExtractedProps extends Record {
8 | children: any;
9 | }
10 |
11 | function Extracted({ children, ...props }: ExtractedProps) {
12 | return (
13 |
16 | );
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/src/test/components/implicit/implicit.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const isDisabled = Math.random() > 0.5;
5 |
6 | return (
7 |
8 | Submit
9 | Do this
10 | Do this
11 | Do this
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/test/components/implicit/implicit.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const isDisabled = Math.random() > 0.5;
5 |
6 | return (
7 |
8 | Submit
9 | Do this
10 | Do this
11 | Do this
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/test/components/spreadNested/spreadNested.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child(props) {
4 | return {props}
;
5 | }
6 |
7 | function Component({ children, style: { color, ...nestedProps }, ...props }) {
8 | return (
9 |
10 |
{children}
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/test/components/componentAsFunction/componentAsFunction.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ renderHeader }) {
4 | return (
5 |
6 | {renderHeader('Child' + Math.random().toFixed(2))}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return (
14 |
15 | } />
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/componentAsProps/componentAsPropsResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ Header }) {
4 | return (
5 |
6 | {Header}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return ;
14 | }
15 |
16 | function Extracted() {
17 | return (
18 |
19 | Header} />
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredProps/undestructuredPropsResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass = 'my-class';
5 | const baseClass = 'my-class-2';
6 |
7 | return ;
8 | }
9 |
10 | function Extracted(props) {
11 | return (
12 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/components/reactFCType/reactFCTypeResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const myClass: string = 'my-class';
5 |
6 | return ;
7 | }
8 |
9 | interface ExtractedProps {
10 | myClass: string;
11 | }
12 |
13 | const Extracted: React.FC = ({ myClass }) => (
14 |
18 | );
19 |
20 |
--------------------------------------------------------------------------------
/src/test/components/destructureNested/destructureNestedResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const {
5 | styles: { color },
6 | text
7 | } = { styles: { color: 'red' }, text: 'Test' };
8 |
9 | return ;
10 | }
11 |
12 | interface ExtractedProps {
13 | color: string;
14 | text: string;
15 | }
16 |
17 | function Extracted({ color, text }: ExtractedProps) {
18 | return {text}
;
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/typescriptProgram.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import * as vscode from 'vscode';
3 |
4 | export function getProgramAndSourceFile(document: vscode.TextDocument) {
5 | const program = ts.createProgram([document.uri.fsPath], {
6 | allowJs: true,
7 | strict: true,
8 | lib: ['lib.esnext.full.d.ts'],
9 | jsx: ts.JsxEmit.React,
10 | esModuleInterop: true
11 | });
12 | const sourceFile = program.getSourceFile(document.uri.fsPath);
13 |
14 | return { program, sourceFile };
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/test/components/conditional/conditional.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const variable = Math.random() > 0.5 ? true : null;
5 | return (
6 |
7 | {variable ??
Test }
8 | {variable &&
Test }
9 | {variable ?
Test :
Test }
10 |
Another Test
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/components/conditional/conditional.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const variable = Math.random() > 0.5 ? true : null;
5 | return (
6 |
7 | {variable ??
Test }
8 | {variable &&
Test }
9 | {variable ?
Test :
Test }
10 |
Another Test
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/components/onlyStatic/onlyStaticResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | function Component() {
5 | return ;
6 | }
7 |
8 | function Extracted() {
9 | return (
10 |
11 |
Test
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/components/onlyStatic/onlyStaticResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | function Component() {
5 | return ;
6 | }
7 |
8 | function Extracted() {
9 | return (
10 |
11 |
Test
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/components/componentAsProps/componentAsPropsResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | function Child({ Header }: { Header: ReactNode }) {
4 | return (
5 |
6 | {Header}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return ;
14 | }
15 |
16 | function Extracted() {
17 | return (
18 |
19 | Header} />
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/components/spreadArray/spreadArrayResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component(props) {
4 | const {
5 | items: [item1, item2, ...otherItems]
6 | } = props;
7 |
8 | return ;
9 | }
10 |
11 | function Extracted({ item1, item2, otherItems }) {
12 | return (
13 |
14 |
{item1}
15 |
{item2}
16 |
{otherItems.join(', ')}
17 |
18 | );
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/test/components/textChild/textChildResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const greeting: string = 'Ça va? Ça va!';
5 |
6 | return ;
7 | }
8 |
9 | interface ExtractedProps {
10 | greeting: string;
11 | }
12 |
13 | function Extracted({ greeting }: ExtractedProps) {
14 | return (
15 |
16 |
In French, a conversation can enter an infinite loop with: {greeting}
17 |
Be careful!
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationSpread/arrowFunctionDeclarationSpreadResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }) {
4 | return ;
5 | }
6 |
7 | const Extracted = ({ children, color, nestedProps, ...props }) => (
8 |
9 |
10 | {children}
11 |
12 |
13 |
14 | );
15 |
16 |
--------------------------------------------------------------------------------
/src/test/components/componentAsFunction/componentAsFunction.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | function Child({ renderHeader }: { renderHeader: (title: string) => ReactNode }) {
4 | return (
5 |
6 | {renderHeader('Child' + Math.random().toFixed(2))}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return (
14 |
15 | } />
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/parameter/parameter.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, ReactNode } from 'react';
2 |
3 | interface ComponentProps {
4 | style: CSSProperties;
5 | children: ReactNode;
6 | prop1: string;
7 | prop2: string;
8 | prop3: string;
9 | prop4: string;
10 | }
11 |
12 | function Component(props: ComponentProps) {
13 | return (
14 |
15 |
{props.children}
16 |
17 |
18 | );
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/test/components/spread/spreadResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | interface ComponentProps {
4 | style: { color: string; nested1: string; nested2: string };
5 | children: ReactNode;
6 | prop1: string[];
7 | prop2: string;
8 | prop3: string;
9 | }
10 |
11 | function Component({ ...props }: ComponentProps) {
12 | return ;
13 | }
14 |
15 | interface ExtractedProps extends ComponentProps {}
16 |
17 | function Extracted({ ...props }: ExtractedProps) {
18 | return
;
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/componentAsFunction/componentAsFunctionResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ renderHeader }) {
4 | return (
5 |
6 | {renderHeader('Child' + Math.random().toFixed(2))}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return ;
14 | }
15 |
16 | function Extracted() {
17 | return (
18 |
19 | } />
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclarationReactFC/typeInlineDeclarationReactFCResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return ;
8 | }
9 |
10 | const Extracted: React.FC<{ anotherClass: string; baseClass: string }> = ({ anotherClass, baseClass }) => (
11 |
14 | );
15 |
16 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclaration/typeInlineDeclarationResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return ;
8 | }
9 |
10 | function Extracted({ anotherClass, baseClass }: { anotherClass: string; baseClass: string }) {
11 | return (
12 |
15 | );
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/test/components/implicit/implicitResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const isDisabled = Math.random() > 0.5;
5 |
6 | return ;
7 | }
8 |
9 | function Extracted({ isDisabled }) {
10 | return (
11 |
12 | Submit
13 | Do this
14 | Do this
15 | Do this
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/parameterTypeReference/parameterTypeReferenceResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component(props: ComponentPropsWithoutRef<'div'>) {
4 | return ;
5 | }
6 |
7 | interface ExtractedProps {
8 | props: ComponentPropsWithoutRef<'div'>;
9 | }
10 |
11 | function Extracted({ props }: ExtractedProps) {
12 | return (
13 |
14 |
{props.children}
15 |
16 |
17 | );
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsSpreadAttribute/undestructuredPropsSpreadAttributeResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef } from 'react';
2 |
3 | function Component({ children, ...props }: ComponentPropsWithoutRef<'div'>) {
4 | return ;
5 | }
6 |
7 | interface ExtractedProps extends Omit, 'children'> {
8 | children: React.ReactNode;
9 | }
10 |
11 | function Extracted({ children, ...props }: ExtractedProps) {
12 | return (
13 |
16 | );
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/src/test/components/componentAsFunction/componentAsFunctionResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | function Child({ renderHeader }: { renderHeader: (title: string) => ReactNode }) {
4 | return (
5 |
6 | {renderHeader('Child' + Math.random().toFixed(2))}
7 |
Not a Footer
8 |
9 | );
10 | }
11 |
12 | function Component() {
13 | return ;
14 | }
15 |
16 | function Extracted() {
17 | return (
18 |
19 | } />
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/components/conditional/conditionalResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const variable = Math.random() > 0.5 ? true : null;
5 | return ;
6 | }
7 |
8 | function Extracted({ variable }) {
9 | return (
10 |
11 | {variable ??
Test }
12 | {variable &&
Test }
13 | {variable ?
Test :
Test }
14 |
Another Test
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredProps/undestructuredPropsResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return ;
8 | }
9 |
10 | interface ExtractedProps {
11 | anotherClass: string;
12 | baseClass: string;
13 | }
14 |
15 | function Extracted(props: ExtractedProps) {
16 | return (
17 |
20 | );
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/test/components/typeTypeDeclaration/typeTypeDeclarationResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const anotherClass: string = 'my-class';
5 | const baseClass: string = 'my-class-2';
6 |
7 | return ;
8 | }
9 |
10 | type ExtractedProps = {
11 | anotherClass: string;
12 | baseClass: string;
13 | };
14 |
15 | function Extracted({ anotherClass, baseClass }: ExtractedProps) {
16 | return (
17 |
20 | );
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/test/components/map/map.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ model }) {
4 | return (
5 |
6 | {model.map((item, index) => (
7 |
{item.label}
8 | ))}
9 |
10 | );
11 | }
12 |
13 | function Component() {
14 | const model = [
15 | { label: 'label1', value: 'value1' },
16 | { label: 'label2', value: 'value2' }
17 | ];
18 | return (
19 | <>
20 |
21 | {model.map((item, index) => (
22 |
{item.label}
23 | ))}
24 |
25 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/test/components/spreadNested/spreadNestedResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child(props) {
4 | return {props}
;
5 | }
6 |
7 | function Component({ children, style: { color, ...nestedProps }, ...props }) {
8 | return ;
9 | }
10 |
11 | function Extracted({ children, color, props, nestedProps }) {
12 | return (
13 |
14 |
{children}
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/test/components/subSelection/subSelection.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function ActuallySelectedComponent({ children, className }) {
4 | return {children}
;
5 | }
6 |
7 | function Component() {
8 | const margin = `m-${Math.floor(Math.random() * 100)}`;
9 | return (
10 |
11 |
Child 1
12 |
Child 2
13 |
console.log('Hello')}>
14 | Child 3
15 | Child 4
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/implicit/implicitResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const isDisabled = Math.random() > 0.5;
5 |
6 | return ;
7 | }
8 |
9 | interface ExtractedProps {
10 | isDisabled: boolean;
11 | }
12 |
13 | function Extracted({ isDisabled }: ExtractedProps) {
14 | return (
15 |
16 | Submit
17 | Do this
18 | Do this
19 | Do this
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/components/map/map.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ model }: { model: { label: string; value: string }[] }) {
4 | return (
5 |
6 | {model.map((item, index) => (
7 |
{item.label}
8 | ))}
9 |
10 | );
11 | }
12 |
13 | function Component() {
14 | const model = [
15 | { label: 'label1', value: 'value1' },
16 | { label: 'label2', value: 'value2' }
17 | ];
18 | return (
19 | <>
20 |
21 | {model.map((item, index) => (
22 |
{item.label}
23 | ))}
24 |
25 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/test/components/spreadNested/spreadNested.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child(props: any) {
4 | return {props}
;
5 | }
6 |
7 | interface ComponentProps {
8 | style: { color: string; nested1: string; nested2: string };
9 | children: React.ReactNode;
10 | prop1: string[];
11 | prop2: string;
12 | prop3: string;
13 | }
14 |
15 | function Component({ children, style: { color, ...nestedProps }, ...props }: ComponentProps) {
16 | return (
17 |
18 |
{children}
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/components/conditional/conditionalResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Component() {
4 | const variable = Math.random() > 0.5 ? true : null;
5 | return ;
6 | }
7 |
8 | interface ExtractedProps {
9 | variable: true | null;
10 | }
11 |
12 | function Extracted({ variable }: ExtractedProps) {
13 | return (
14 |
15 | {variable ??
Test }
16 | {variable &&
Test }
17 | {variable ?
Test :
Test }
18 |
Another Test
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/components/parameter/parameterResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, ReactNode } from 'react';
2 |
3 | interface ComponentProps {
4 | style: CSSProperties;
5 | children: ReactNode;
6 | prop1: string;
7 | prop2: string;
8 | prop3: string;
9 | prop4: string;
10 | }
11 |
12 | function Component(props: ComponentProps) {
13 | return ;
14 | }
15 |
16 | interface ExtractedProps {
17 | props: ComponentProps;
18 | }
19 |
20 | function Extracted({ props }: ExtractedProps) {
21 | return (
22 |
23 |
{props.children}
24 |
25 |
26 | );
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/src/test/components/map/mapResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ model }) {
4 | return (
5 |
6 | {model.map((item, index) => (
7 |
{item.label}
8 | ))}
9 |
10 | );
11 | }
12 |
13 | function Component() {
14 | const model = [
15 | { label: 'label1', value: 'value1' },
16 | { label: 'label2', value: 'value2' }
17 | ];
18 | return ;
19 | }
20 |
21 | function Extracted({ model }) {
22 | return (
23 | <>
24 |
25 | {model.map((item, index) => (
26 |
{item.label}
27 | ))}
28 |
29 |
30 | >
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/components/spreadArray/spreadArrayResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface ComponentProps {
4 | items: string[];
5 | }
6 |
7 | function Component(props: ComponentProps) {
8 | const {
9 | items: [item1, item2, ...otherItems]
10 | } = props;
11 |
12 | return ;
13 | }
14 |
15 | interface ExtractedProps {
16 | item1: string;
17 | item2: string;
18 | otherItems: string[];
19 | }
20 |
21 | function Extracted({ item1, item2, otherItems }: ExtractedProps) {
22 | return (
23 |
24 |
{item1}
25 |
{item2}
26 |
{otherItems.join(', ')}
27 |
28 | );
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/test/components/subSelection/subSelection.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithRef, ReactNode } from 'react';
2 |
3 | function ActuallySelectedComponent({ children, className }: { children: ReactNode } & ComponentPropsWithRef<'div'>) {
4 | return {children}
;
5 | }
6 |
7 | function Component() {
8 | const margin = `m-${Math.floor(Math.random() * 100)}`;
9 | return (
10 |
11 |
Child 1
12 |
Child 2
13 |
console.log('Hello')}>
14 | Child 3
15 | Child 4
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/components/static/static.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | const CONSTANT_IN_FILE = 1;
5 |
6 | function Component() {
7 | const baseClass = 'my-class-2';
8 | return (
9 |
10 |
console.log('Clicked!')}>
11 | Test
12 |
13 |
14 |
15 |
16 |
17 |
Test2
18 |
{CONSTANT_IN_FILE}
19 |
20 | );
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/test/components/subSelection/subSelectionResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function ActuallySelectedComponent({ children, className }) {
4 | return {children}
;
5 | }
6 |
7 | function Component() {
8 | const margin = `m-${Math.floor(Math.random() * 100)}`;
9 | return (
10 |
11 | Child 1
12 | Child 2
13 |
14 |
15 | );
16 | }
17 |
18 | function Extracted({ margin }) {
19 | return (
20 | console.log('Hello')}>
21 | Child 3
22 | Child 4
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/components/static/static.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | const CONSTANT_IN_FILE = 1;
5 |
6 | function Component() {
7 | const baseClass: string = 'my-class-2';
8 | return (
9 |
10 |
console.log('Clicked!')}>
11 | Test
12 |
13 |
14 |
15 |
16 |
17 |
Test2
18 |
{CONSTANT_IN_FILE}
19 |
20 | );
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function removeNonWordCharacters(value: string) {
2 | return value.replaceAll(/\W/g, '');
3 | }
4 |
5 | export function capitalizeComponentName(value: string) {
6 | return value.charAt(0).toUpperCase() + value.slice(1);
7 | }
8 |
9 | export function truncateType(type: string) {
10 | return type.length > 500 ? 'any' : type;
11 | }
12 |
13 | export function chooseAdequateType(resolvedType: string, heuristicType: string) {
14 | if (resolvedType !== 'any' && heuristicType === 'any') return resolvedType;
15 | if (resolvedType === 'any' && heuristicType !== 'any') return heuristicType;
16 |
17 | if (resolvedType !== 'any' && heuristicType !== 'any') {
18 | return resolvedType.length <= heuristicType.length ? resolvedType : heuristicType;
19 | }
20 |
21 | return 'any';
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/test/components/static/staticResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | const CONSTANT_IN_FILE = 1;
5 |
6 | function Component() {
7 | const baseClass = 'my-class-2';
8 | return ;
9 | }
10 |
11 | function Extracted({ baseClass }) {
12 | return (
13 |
14 |
console.log('Clicked!')}>
15 | Test
16 |
17 |
18 |
19 |
20 |
21 |
Test2
22 |
{CONSTANT_IN_FILE}
23 |
24 | );
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/src/test/components/map/mapResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child({ model }: { model: { label: string; value: string }[] }) {
4 | return (
5 |
6 | {model.map((item, index) => (
7 |
{item.label}
8 | ))}
9 |
10 | );
11 | }
12 |
13 | function Component() {
14 | const model = [
15 | { label: 'label1', value: 'value1' },
16 | { label: 'label2', value: 'value2' }
17 | ];
18 | return ;
19 | }
20 |
21 | interface ExtractedProps {
22 | model: { label: string; value: string }[];
23 | }
24 |
25 | function Extracted({ model }: ExtractedProps) {
26 | return (
27 | <>
28 |
29 | {model.map((item, index) => (
30 |
{item.label}
31 | ))}
32 |
33 |
34 | >
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/test/components/propertiesAndMethods/propertiesAndMethods.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class MyClass {
4 | myProperty = 'hello';
5 | myMethod() {
6 | return 'world';
7 | }
8 | }
9 |
10 | function Child(props) {
11 | return {props}
;
12 | }
13 |
14 | function Component(props) {
15 | const model1 = { label: 'label1' };
16 | const model2 = { valueFunction: () => 'value1' };
17 | const model3 = { evaluatedFunction: () => 'evaluated' };
18 | const model4 = { nested: { nestedValue: 'nested' } };
19 | const myClass = new MyClass();
20 |
21 | return (
22 |
30 | );
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/test/components/arrowFunctionDeclarationSpread/arrowFunctionDeclarationSpreadResult.tsx:
--------------------------------------------------------------------------------
1 | import { Property } from 'csstype';
2 | import React, { ComponentPropsWithoutRef } from 'react';
3 |
4 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
5 | return ;
6 | }
7 |
8 | interface ExtractedProps extends Omit, 'children' | 'style'> {
9 | children: React.ReactNode;
10 | color: Property.Color | undefined;
11 | nestedProps: Omit;
12 | }
13 |
14 | const Extracted = ({ children, color, nestedProps, ...props }: ExtractedProps) => (
15 |
16 |
17 | {children}
18 |
19 |
20 |
21 | );
22 |
23 |
--------------------------------------------------------------------------------
/src/test/components/propertiesAndMethods/propertiesAndMethods.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class MyClass {
4 | myProperty = 'hello';
5 | myMethod() {
6 | return 'world';
7 | }
8 | }
9 |
10 | function Child(props: any) {
11 | return {props}
;
12 | }
13 |
14 | function Component(props: any) {
15 | const model1 = { label: 'label1' };
16 | const model2 = { valueFunction: () => 'value1' };
17 | const model3 = { evaluatedFunction: () => 'evaluated' };
18 | const model4 = { nested: { nestedValue: 'nested' } };
19 | const myClass = new MyClass();
20 |
21 | return (
22 |
30 | );
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/test/components/typeInlineDeclarationExtended/typeInlineDeclarationExtendedResult.tsx:
--------------------------------------------------------------------------------
1 | import { Property } from 'csstype';
2 | import React, { ComponentPropsWithoutRef } from 'react';
3 |
4 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
5 | return ;
6 | }
7 |
8 | function Extracted({
9 | children,
10 | color,
11 | nestedProps,
12 | ...props
13 | }: Omit, 'children' | 'style'> & {
14 | children: React.ReactNode;
15 | color: Property.Color | undefined;
16 | nestedProps: Omit;
17 | }) {
18 | return (
19 |
20 |
21 | {children}
22 |
23 |
24 |
25 | );
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/src/test/components/spreadNestedTypeReference/spreadNestedTypeReferenceResult.tsx:
--------------------------------------------------------------------------------
1 | import { Property } from 'csstype';
2 | import React, { ComponentPropsWithoutRef } from 'react';
3 |
4 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
5 | return ;
6 | }
7 |
8 | interface ExtractedProps extends Omit, 'children' | 'style'> {
9 | children: React.ReactNode;
10 | color: Property.Color | undefined;
11 | nestedProps: Omit;
12 | }
13 |
14 | function Extracted({ children, color, nestedProps, ...props }: ExtractedProps) {
15 | return (
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/test/components/typeTypeDeclarationExtended/typeTypeDeclarationExtendedResult.tsx:
--------------------------------------------------------------------------------
1 | import { Property } from 'csstype';
2 | import React, { ComponentPropsWithoutRef } from 'react';
3 |
4 | function Component({ children, style: { color, ...nestedProps } = {}, ...props }: ComponentPropsWithoutRef<'div'>) {
5 | return ;
6 | }
7 |
8 | type ExtractedProps = Omit, 'children' | 'style'> & {
9 | children: React.ReactNode;
10 | color: Property.Color | undefined;
11 | nestedProps: Omit;
12 | };
13 |
14 | function Extracted({ children, color, nestedProps, ...props }: ExtractedProps) {
15 | return (
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/test/components/subSelection/subSelectionResult.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithRef, ReactNode } from 'react';
2 |
3 | function ActuallySelectedComponent({ children, className }: { children: ReactNode } & ComponentPropsWithRef<'div'>) {
4 | return {children}
;
5 | }
6 |
7 | function Component() {
8 | const margin = `m-${Math.floor(Math.random() * 100)}`;
9 | return (
10 |
11 | Child 1
12 | Child 2
13 |
14 |
15 | );
16 | }
17 |
18 | interface ExtractedProps {
19 | margin: string;
20 | }
21 |
22 | function Extracted({ margin }: ExtractedProps) {
23 | return (
24 | console.log('Hello')}>
25 | Child 3
26 | Child 4
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/components/longType/longType.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type ComponentProps = {
4 | translation: {
5 | TEXT1: string;
6 | TEXT2: string;
7 | TEXT3: string;
8 | TEXT4: string;
9 | TEXT5: string;
10 | TEXT6: string;
11 | TEXT7: string;
12 | TEXT8: string;
13 | TEXT9: string;
14 | TEXT10: string;
15 | TEXT11: string;
16 | TEXT12: string;
17 | TEXT13: string;
18 | TEXT14: string;
19 | TEXT15: string;
20 | TEXT16: string;
21 | TEXT17: string;
22 | TEXT18: string;
23 | TEXT19: string;
24 | TEXT20: string;
25 | TEXT21: string;
26 | TEXT22: string;
27 | TEXT23: string;
28 | TEXT24: string;
29 | TEXT25: string;
30 | TEXT26: string;
31 | TEXT27: string;
32 | TEXT28: string;
33 | TEXT29: string;
34 | TEXT30: string;
35 | };
36 | };
37 |
38 | function Component({ translation }: ComponentProps) {
39 | return {translation.TEXT1}
;
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/components/static/staticResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CLASS_NAME, VALUE } from './export';
3 |
4 | const CONSTANT_IN_FILE = 1;
5 |
6 | function Component() {
7 | const baseClass: string = 'my-class-2';
8 | return ;
9 | }
10 |
11 | interface ExtractedProps {
12 | baseClass: string;
13 | }
14 |
15 | function Extracted({ baseClass }: ExtractedProps) {
16 | return (
17 |
18 |
console.log('Clicked!')}>
19 | Test
20 |
21 |
22 |
23 |
24 |
25 |
Test2
26 |
{CONSTANT_IN_FILE}
27 |
28 | );
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$ts-webpack-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never",
13 | "group": "watchers"
14 | },
15 | "group": {
16 | "kind": "build",
17 | "isDefault": true
18 | }
19 | },
20 | {
21 | "type": "npm",
22 | "script": "watch-tests",
23 | "problemMatcher": "$tsc-watch",
24 | "isBackground": true,
25 | "presentation": {
26 | "reveal": "never",
27 | "group": "watchers"
28 | },
29 | "group": "build"
30 | },
31 | {
32 | "label": "tasks: watch-tests",
33 | "dependsOn": ["npm: watch", "npm: watch-tests"],
34 | "problemMatcher": []
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/components/shortHand/shortHand.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shortHandImport } from './export';
3 |
4 | const shortHandConstantInFile = 'shortHandConstantInFile';
5 |
6 | function Child(props) {
7 | return {props}
;
8 | }
9 |
10 | function Component(props) {
11 | const shortHand = 'shortHand';
12 | function shortHandFunction() {
13 | return 'test';
14 | }
15 | const shortHandAnonymousFunction = () => 'test';
16 | const objectWithShortHand = { shortHand, shortHandFunction, shortHandAnonymousFunction };
17 |
18 | return (
19 | {
23 | const shortHandToIgnore = 'propShortHand';
24 | const shortHandObject = { shortHandToIgnore };
25 | console.log(JSON.stringify(shortHandObject));
26 | }}
27 | />
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "plugin:react/recommended",
7 | "plugin:react/jsx-runtime",
8 | "prettier"
9 | ],
10 | "parser": "@typescript-eslint/parser",
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": ["@typescript-eslint", "react"],
16 | "rules": {
17 | "@typescript-eslint/naming-convention": [
18 | "warn",
19 | {
20 | "selector": "import",
21 | "format": ["camelCase", "PascalCase"]
22 | }
23 | ],
24 | "react/no-unstable-nested-components": ["warn", { "allowAsProps": true }],
25 | "no-unused-vars": "off",
26 | "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" }],
27 | "no-throw-literal": "warn"
28 | },
29 | "ignorePatterns": ["out", "dist", "**/*.d.ts", "node_modules", "src/test/components/", "src/test/createTestCase.js"]
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/test/components/shortHand/shortHand.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shortHandImport } from './export';
3 |
4 | const shortHandConstantInFile = 'shortHandConstantInFile';
5 |
6 | function Child(props: any) {
7 | return {props}
;
8 | }
9 |
10 | function Component(props: any) {
11 | const shortHand = 'shortHand';
12 | function shortHandFunction() {
13 | return 'test';
14 | }
15 | const shortHandAnonymousFunction = () => 'test';
16 | const objectWithShortHand = { shortHand, shortHandFunction, shortHandAnonymousFunction };
17 |
18 | return (
19 | {
23 | const shortHandToIgnore = 'propShortHand';
24 | const shortHandObject = { shortHandToIgnore };
25 | console.log(JSON.stringify(shortHandObject));
26 | }}
27 | />
28 | );
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files
5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files
6 | },
7 | "search.exclude": {
8 | "out": true, // set this to false to include "out" folder in search results
9 | "dist": true // set this to false to include "dist" folder in search results
10 | },
11 | "typescript.tsc.autoDetect": "off", // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
12 | "typescript.format.semicolons": "insert",
13 | "editor.tabSize": 2,
14 | "[typescript]": {
15 | "editor.defaultFormatter": "esbenp.prettier-vscode"
16 | },
17 | "editor.codeActionsOnSave": {
18 | "source.organizeImports": "explicit",
19 | "source.fixAll": "explicit"
20 | },
21 | "editor.formatOnSave": true,
22 | "prettier.ignorePath": ".prettierignore"
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/components/propertiesAndMethods/propertiesAndMethodsResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class MyClass {
4 | myProperty = 'hello';
5 | myMethod() {
6 | return 'world';
7 | }
8 | }
9 |
10 | function Child(props) {
11 | return {props}
;
12 | }
13 |
14 | function Component(props) {
15 | const model1 = { label: 'label1' };
16 | const model2 = { valueFunction: () => 'value1' };
17 | const model3 = { evaluatedFunction: () => 'evaluated' };
18 | const model4 = { nested: { nestedValue: 'nested' } };
19 | const myClass = new MyClass();
20 |
21 | return ;
22 | }
23 |
24 | function Extracted({ model1, model2, model3, model4, myClass }) {
25 | return (
26 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/components/spreadNested/spreadNestedResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Child(props: any) {
4 | return {props}
;
5 | }
6 |
7 | interface ComponentProps {
8 | style: { color: string; nested1: string; nested2: string };
9 | children: React.ReactNode;
10 | prop1: string[];
11 | prop2: string;
12 | prop3: string;
13 | }
14 |
15 | function Component({ children, style: { color, ...nestedProps }, ...props }: ComponentProps) {
16 | return ;
17 | }
18 |
19 | interface ExtractedProps {
20 | children: React.ReactNode;
21 | color: string;
22 | props: Omit;
23 | nestedProps: { nested1: string; nested2: string };
24 | }
25 |
26 | function Extracted({ children, color, props, nestedProps }: ExtractedProps) {
27 | return (
28 |
29 |
{children}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 João Nascimento
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/test/components/longType/longTypeResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type ComponentProps = {
4 | translation: {
5 | TEXT1: string;
6 | TEXT2: string;
7 | TEXT3: string;
8 | TEXT4: string;
9 | TEXT5: string;
10 | TEXT6: string;
11 | TEXT7: string;
12 | TEXT8: string;
13 | TEXT9: string;
14 | TEXT10: string;
15 | TEXT11: string;
16 | TEXT12: string;
17 | TEXT13: string;
18 | TEXT14: string;
19 | TEXT15: string;
20 | TEXT16: string;
21 | TEXT17: string;
22 | TEXT18: string;
23 | TEXT19: string;
24 | TEXT20: string;
25 | TEXT21: string;
26 | TEXT22: string;
27 | TEXT23: string;
28 | TEXT24: string;
29 | TEXT25: string;
30 | TEXT26: string;
31 | TEXT27: string;
32 | TEXT28: string;
33 | TEXT29: string;
34 | TEXT30: string;
35 | };
36 | };
37 |
38 | function Component({ translation }: ComponentProps) {
39 | return ;
40 | }
41 |
42 | interface ExtractedProps {
43 | translation: ComponentProps['translation'];
44 | }
45 |
46 | function Extracted({ translation }: ExtractedProps) {
47 | return {translation.TEXT1}
;
48 | }
49 |
--------------------------------------------------------------------------------
/src/test/createTestCase.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const testName = process.argv[2];
5 |
6 | const testPath = path.join(__dirname, './components/', testName);
7 |
8 | if (!fs.existsSync(testPath)) {
9 | fs.mkdirSync(testPath, { recursive: true });
10 | }
11 |
12 | const testFiles = [`${testName}.jsx`, `${testName}.tsx`];
13 | testFiles.forEach((file) => {
14 | fs.writeFileSync(
15 | path.join(testPath, file),
16 | `
17 | import React from 'react';
18 |
19 | function Component() {
20 | return (
21 |
25 | );
26 | }
27 | `,
28 | 'utf8'
29 | );
30 | });
31 |
32 | const resultFiles = [`${testName}Result.jsx`, `${testName}Result.tsx`];
33 | resultFiles.forEach((file) => {
34 | fs.writeFileSync(
35 | path.join(testPath, file),
36 | `
37 | import React from 'react';
38 |
39 | function Component() {
40 | return ;
41 | }
42 |
43 | function Extracted() {
44 | return (
45 |
49 | );
50 | }
51 | `,
52 | 'utf8'
53 | );
54 | });
55 |
--------------------------------------------------------------------------------
/src/test/checks.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 | import * as vscode from 'vscode';
3 | import { isFileTypescript } from '../checks';
4 |
5 | suite('isFileTypescript', () => {
6 | test('should return true for TypeScript file', async () => {
7 | const document = await vscode.workspace.openTextDocument(vscode.Uri.parse('untitled:/test.ts'));
8 | const result = isFileTypescript(document);
9 | assert.strictEqual(true, result);
10 | });
11 |
12 | test('should return true for TypeScript React file', async () => {
13 | const document = await vscode.workspace.openTextDocument(vscode.Uri.parse('untitled:/test.tsx'));
14 | const result = isFileTypescript(document);
15 | assert.strictEqual(true, result);
16 | });
17 |
18 | test('should return false for other extensions file', async () => {
19 | const document = await vscode.workspace.openTextDocument(vscode.Uri.parse('untitled:/test.js'));
20 | const result = isFileTypescript(document);
21 | assert.strictEqual(false, result);
22 | });
23 |
24 | test('should return false for files without extension', async () => {
25 | const document = await vscode.workspace.openTextDocument(vscode.Uri.parse('untitled:/test'));
26 | const result = isFileTypescript(document);
27 | assert.strictEqual(false, result);
28 | });
29 | });
30 |
31 |
--------------------------------------------------------------------------------
/src/test/components/propertiesAndMethods/propertiesAndMethodsResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class MyClass {
4 | myProperty = 'hello';
5 | myMethod() {
6 | return 'world';
7 | }
8 | }
9 |
10 | function Child(props: any) {
11 | return {props}
;
12 | }
13 |
14 | function Component(props: any) {
15 | const model1 = { label: 'label1' };
16 | const model2 = { valueFunction: () => 'value1' };
17 | const model3 = { evaluatedFunction: () => 'evaluated' };
18 | const model4 = { nested: { nestedValue: 'nested' } };
19 | const myClass = new MyClass();
20 |
21 | return ;
22 | }
23 |
24 | interface ExtractedProps {
25 | model1: { label: string };
26 | model2: { valueFunction: () => string };
27 | model3: { evaluatedFunction: () => string };
28 | model4: { nested: { nestedValue: string } };
29 | myClass: MyClass;
30 | }
31 |
32 | function Extracted({ model1, model2, model3, model4, myClass }: ExtractedProps) {
33 | return (
34 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"],
14 | "preLaunchTask": "${defaultBuildTask}"
15 | },
16 | {
17 | "name": "Extension Tests",
18 | "type": "extensionHost",
19 | "request": "launch",
20 | "runtimeExecutable": "${execPath}",
21 | "args": [
22 | "--disable-extensions",
23 | "--extensionDevelopmentPath=${workspaceFolder}",
24 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
25 | ],
26 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"],
27 | "preLaunchTask": "tasks: watch-tests"
28 | },
29 | {
30 | "name": "Create Test Case",
31 | "type": "node",
32 | "request": "launch",
33 | "program": "${workspaceFolder}/src/test/createTestCase.js",
34 | "args": ["testCase"]
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/components/shortHand/shortHandResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shortHandImport } from './export';
3 |
4 | const shortHandConstantInFile = 'shortHandConstantInFile';
5 |
6 | function Child(props) {
7 | return {props}
;
8 | }
9 |
10 | function Component(props) {
11 | const shortHand = 'shortHand';
12 | function shortHandFunction() {
13 | return 'test';
14 | }
15 | const shortHandAnonymousFunction = () => 'test';
16 | const objectWithShortHand = { shortHand, shortHandFunction, shortHandAnonymousFunction };
17 |
18 | return (
19 |
25 | );
26 | }
27 |
28 | function Extracted({ objectWithShortHand, shortHand, shortHandAnonymousFunction, shortHandFunction }) {
29 | return (
30 | {
34 | const shortHandToIgnore = 'propShortHand';
35 | const shortHandObject = { shortHandToIgnore };
36 | console.log(JSON.stringify(shortHandObject));
37 | }}
38 | />
39 | );
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { extractComponent } from './extractComponent';
3 | import { isJsx } from './parsers/isJsx';
4 |
5 | class ExtractOnRefactorProvider implements vscode.CodeActionProvider {
6 | provideCodeActions(
7 | document: vscode.TextDocument,
8 | range: vscode.Range | vscode.Selection,
9 | context: vscode.CodeActionContext
10 | ) {
11 | if (context.only && !context.only.contains(vscode.CodeActionKind.Refactor)) return [];
12 |
13 | if (!isJsx(document, range)) return [];
14 |
15 | const refactor = new vscode.CodeAction('React Extract: Extract Component', vscode.CodeActionKind.Refactor);
16 |
17 | refactor.command = {
18 | command: 'reactExtract.extractComponent',
19 | title: 'React Extract: Extract Component',
20 | arguments: [document, range]
21 | };
22 |
23 | return [refactor];
24 | }
25 | }
26 |
27 | export function activate(context: vscode.ExtensionContext) {
28 | context.subscriptions.push(
29 | vscode.languages.registerCodeActionsProvider(
30 | [{ pattern: '{**/*.js,**/*.ts,**/*.jsx,**/*.tsx}' }],
31 | new ExtractOnRefactorProvider(),
32 | { providedCodeActionKinds: [vscode.CodeActionKind.Refactor] }
33 | )
34 | );
35 |
36 | context.subscriptions.push(vscode.commands.registerCommand('reactExtract.extractComponent', extractComponent));
37 | }
38 |
39 | export function deactivate() {}
40 |
41 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import * as vscode from 'vscode';
3 |
4 | export type ExtractedProp = {
5 | name: string;
6 | isSpread: boolean;
7 | type: string;
8 | };
9 |
10 | export type ExternalArgs = {
11 | document: vscode.TextDocument;
12 | range: vscode.Range | vscode.Selection;
13 | componentName: string;
14 | functionDeclaration: 'function' | 'arrow';
15 | typeDeclaration: 'interface' | 'type' | 'inline';
16 | declareWithReactFC: boolean;
17 | explicitReturnStatement: boolean;
18 | destructureProps: boolean;
19 | };
20 |
21 | export type ArgsDerivedFromExternalArgs = {
22 | isTypescript: boolean;
23 | program: ts.Program;
24 | sourceFile: ts.SourceFile;
25 | typeDeclarationName: string;
26 | };
27 |
28 | export type ExtractionArgs = ExternalArgs & ArgsDerivedFromExternalArgs;
29 |
30 | export type PropsAndDerivedData = SingleSpread & {
31 | props: ExtractedProp[];
32 | shouldDisplayTypeDeclaration: boolean;
33 | shouldDestructureProps: boolean;
34 | };
35 |
36 | type SingleSpread =
37 | | {
38 | hasSingleSpread: true;
39 | singleSpreadType: string;
40 | }
41 | | {
42 | hasSingleSpread: false;
43 | singleSpreadType: undefined;
44 | };
45 |
46 | export type TypeDeclarationInfo = {
47 | typeDeclarationBody: string;
48 | typeDeclarationReference: string;
49 | };
50 |
51 | export type BuildArgs = ExtractionArgs & PropsAndDerivedData & TypeDeclarationInfo & { shouldWrapInFragments: boolean };
52 |
53 |
--------------------------------------------------------------------------------
/src/test/components/shortHand/shortHandResult.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shortHandImport } from './export';
3 |
4 | const shortHandConstantInFile = 'shortHandConstantInFile';
5 |
6 | function Child(props: any) {
7 | return {props}
;
8 | }
9 |
10 | function Component(props: any) {
11 | const shortHand = 'shortHand';
12 | function shortHandFunction() {
13 | return 'test';
14 | }
15 | const shortHandAnonymousFunction = () => 'test';
16 | const objectWithShortHand = { shortHand, shortHandFunction, shortHandAnonymousFunction };
17 |
18 | return (
19 |
25 | );
26 | }
27 |
28 | interface ExtractedProps {
29 | objectWithShortHand: { shortHand: string; shortHandFunction: () => string; shortHandAnonymousFunction: () => string };
30 | shortHand: string;
31 | shortHandAnonymousFunction: () => string;
32 | shortHandFunction: () => string;
33 | }
34 |
35 | function Extracted({ objectWithShortHand, shortHand, shortHandAnonymousFunction, shortHandFunction }: ExtractedProps) {
36 | return (
37 | {
41 | const shortHandToIgnore = 'propShortHand';
42 | const shortHandObject = { shortHandToIgnore };
43 | console.log(JSON.stringify(shortHandObject));
44 | }}
45 | />
46 | );
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/src/parsers/fragment.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import { ExtractionArgs } from '../types';
3 | import { getNodeRange } from './parsingUtils';
4 |
5 | export function determineIfShouldWrapInFragments(args: ExtractionArgs) {
6 | const { sourceFile } = args;
7 |
8 | const outermostSelectedNodes: Set = new Set();
9 | ts.forEachChild(sourceFile, (node) => visit({ node, outermostSelectedNodes, ...args }));
10 |
11 | return outermostSelectedNodes.size > 1;
12 | }
13 |
14 | interface VisitorArguments extends ExtractionArgs {
15 | parent?: ts.Node;
16 | node: ts.Node;
17 | outermostSelectedNodes: Set;
18 | }
19 |
20 | function visit(args: VisitorArguments) {
21 | const { node, parent, range, outermostSelectedNodes, sourceFile, document } = args;
22 |
23 | // visiting node outside selection
24 | const nodeRange = getNodeRange(node, sourceFile);
25 | if (!range.intersection(nodeRange)) return;
26 |
27 | // looks for nested nodes. Parents must be passed down as they are undefined in the JSX Children for some reason.
28 | ts.forEachChild(node, (child) => visit({ ...args, node: child, parent: node }));
29 |
30 | if (!parent) return;
31 |
32 | // element is bigger than the selection
33 | if (!range.contains(nodeRange)) return;
34 |
35 | // there is no way in which a valid selection candidate for being wrapped in fragments is not a JSX child
36 | if (!ts.isJsxChild(node)) return;
37 |
38 | // empty JSX text nodes appear when breaking lines with nesting and should be ignored
39 | if (document.getText(nodeRange) === '') return;
40 |
41 | const isOutermostSelectedNode = !range.contains(getNodeRange(parent, sourceFile));
42 | isOutermostSelectedNode && outermostSelectedNodes.add(node);
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/src/parsers/isJsx.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import * as vscode from 'vscode';
3 | import { getProgramAndSourceFile } from '../typescriptProgram';
4 | import { getNodeRange } from './parsingUtils';
5 |
6 | export function isJsx(document: vscode.TextDocument, range: vscode.Range) {
7 | const selectedText = document.getText(range).trim();
8 | if (!selectedText) return false;
9 |
10 | const { sourceFile } = getProgramAndSourceFile(document);
11 | if (!sourceFile) return false;
12 |
13 | const outermostJsx: boolean[] = [];
14 | ts.forEachChild(sourceFile, (node) => visit({ node, range, sourceFile, document, outermostJsx }));
15 |
16 | return !!outermostJsx.length;
17 | }
18 |
19 | interface VisitorArguments {
20 | node: ts.Node;
21 | parent?: ts.Node;
22 | range: vscode.Range;
23 | sourceFile: ts.SourceFile;
24 | document: vscode.TextDocument;
25 | outermostJsx: boolean[];
26 | }
27 |
28 | function visit(args: VisitorArguments) {
29 | const { node, parent, range, sourceFile, document, outermostJsx } = args;
30 |
31 | if (outermostJsx.length) return;
32 |
33 | // visiting node outside selection
34 | const nodeRange = getNodeRange(node, sourceFile);
35 | if (!range.intersection(nodeRange)) return;
36 |
37 | // looks for nested nodes. Parents must be passed down as they are undefined in the JSX Children for some reason.
38 | ts.forEachChild(node, (child) => visit({ ...args, node: child, parent: node }));
39 |
40 | // element is bigger than the selection
41 | if (!range.contains(nodeRange)) return;
42 |
43 | // empty JSX text nodes appear when breaking lines with nesting and should be ignored
44 | if (document.getText(nodeRange) === '') return;
45 |
46 | const isOutermostSelectedNode = !parent || !range.contains(getNodeRange(parent, sourceFile));
47 | if (!isOutermostSelectedNode) return;
48 |
49 | const isJsx = ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
50 | if (isJsx) outermostJsx.push(true);
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | //@ts-check
2 |
3 | 'use strict';
4 |
5 | const path = require('path');
6 | const CopyPlugin = require('copy-webpack-plugin');
7 |
8 | //@ts-check
9 | /** @typedef {import('webpack').Configuration} WebpackConfig **/
10 |
11 | /** @type WebpackConfig */
12 | const extensionConfig = {
13 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
14 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
15 |
16 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
17 | output: {
18 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
19 | path: path.resolve(__dirname, 'dist'),
20 | filename: 'extension.js',
21 | libraryTarget: 'commonjs2'
22 | },
23 | externals: {
24 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
25 | // modules added here also need to be added in the .vscodeignore file
26 | },
27 | resolve: {
28 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
29 | extensions: ['.ts', '.js']
30 | },
31 | module: {
32 | rules: [
33 | {
34 | test: /\.ts$/,
35 | exclude: /node_modules/,
36 | use: [
37 | {
38 | loader: 'ts-loader'
39 | }
40 | ]
41 | }
42 | ]
43 | },
44 | devtool: 'nosources-source-map',
45 | infrastructureLogging: {
46 | level: 'log' // enables logging required for problem matchers
47 | },
48 | plugins: [
49 | new CopyPlugin({
50 | patterns: [{ from: 'node_modules/typescript/lib/*.d.ts', to: '[name][ext]' }]
51 | })
52 | ]
53 | };
54 | module.exports = [extensionConfig];
55 |
56 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsExtended/undestructuredPropsExtended.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const shortHandConstantInFile = 'shortHandConstantInFile';
4 |
5 | function Child(props) {
6 | return {props}
;
7 | }
8 |
9 | function Component(props) {
10 | const shortHand = 'shortHand';
11 | function shortHandFunction() {
12 | return 'test';
13 | }
14 | const shortHandAnonymousFunction = () => 'test';
15 | const objectWithShortHand = { shortHand, shortHandFunction, shortHandAnonymousFunction };
16 |
17 | const myClass = 'myClass';
18 | const mmyClass = 'mmyClass';
19 | const myClass1 = 'myClass1';
20 | const myClass2 = 'myClass2';
21 | const nestedProp = { value: { default: 'default' } };
22 | const condition = true;
23 |
24 | return (
25 | <>
26 | {
30 | const shortHandToIgnore = 'propShortHand';
31 | const shortHandObject = { shortHandToIgnore };
32 | console.log(JSON.stringify(shortHandObject));
33 | }}
34 | />
35 |
36 |
37 |
38 |
39 | {myClass}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
57 |
58 |
59 | >
60 | );
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/assets/logo-square.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/test/isJsx.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 | import * as vscode from 'vscode';
5 | import { isJsx } from '../parsers/isJsx';
6 | import { allRanges, getDocumentsAndRange } from './common';
7 |
8 | suite('isJsx basic scenarios', () => {
9 | async function createDocument(text: string) {
10 | const tempFilePath = path.join(__dirname, 'tempFile.jsx');
11 | if (fs.existsSync(tempFilePath)) {
12 | fs.unlinkSync(tempFilePath);
13 | }
14 |
15 | const declaration = 'const test = ';
16 | fs.writeFileSync(tempFilePath, declaration + text);
17 |
18 | const document = await vscode.workspace.openTextDocument(tempFilePath);
19 |
20 | // Use the document's content to determine the end position
21 | const lastLine = document.lineAt(document.lineCount - 1);
22 | const endPosition = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
23 |
24 | // Create a range that spans the entire text
25 | const range = new vscode.Range(new vscode.Position(0, declaration.length), endPosition);
26 |
27 | return { document, range };
28 | }
29 |
30 | test('should return true for simple JSX', async function () {
31 | const { document, range } = await createDocument('
');
32 | const result = isJsx(document, range);
33 | assert.ok(result);
34 | });
35 |
36 | test('should return true for self enclosed JSX', async function () {
37 | const { document, range } = await createDocument('
');
38 | const result = isJsx(document, range);
39 | assert.ok(result);
40 | });
41 |
42 | test('should return true for fragment', async function () {
43 | const { document, range } = await createDocument('<>>');
44 | const result = isJsx(document, range);
45 | assert.ok(result);
46 | });
47 |
48 | test('should return false for empty selection', async function () {
49 | const { document, range } = await createDocument('');
50 | const result = isJsx(document, range);
51 | assert.ok(!result);
52 | });
53 |
54 | test('should return false for non JSX', async function () {
55 | const { document, range } = await createDocument('const a = 1;');
56 | const result = isJsx(document, range);
57 | assert.ok(!result);
58 | });
59 |
60 | test('should return false for JSX enclosed in non-jsx', async function () {
61 | const { document, range } = await createDocument('[].map(() =>
)');
62 | const result = isJsx(document, range);
63 | assert.ok(!result);
64 | });
65 | });
66 |
67 | suite('isJsx component selections', () => {
68 | Object.keys(allRanges).forEach((key) => {
69 | test(`should return true for ${key}`, async function () {
70 | const { jsTest, tsTest, ranges } = await getDocumentsAndRange(key);
71 | if (jsTest) {
72 | assert.ok(isJsx(jsTest, ranges.javascript));
73 | }
74 |
75 | if (tsTest) {
76 | assert.ok(isJsx(tsTest, ranges.typescript));
77 | }
78 | });
79 | });
80 | });
81 |
82 |
--------------------------------------------------------------------------------
/src/parsers/replacePropsWithFullPath.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import * as vscode from 'vscode';
3 | import { BuildArgs } from '../types';
4 | import { getNodeRange } from './parsingUtils';
5 |
6 | interface ReplacementProps {
7 | position: vscode.Range;
8 | replacement: string;
9 | }
10 |
11 | export function replacePropsWithFullPath(args: BuildArgs) {
12 | const replacements = getReplacements(args);
13 |
14 | const selection = applyReplacementsToSelection({ replacements, ...args });
15 | return selection;
16 | }
17 |
18 | function getReplacements(args: BuildArgs) {
19 | const { sourceFile } = args;
20 |
21 | const replacements: ReplacementProps[] = [];
22 | ts.forEachChild(sourceFile, (node) => visit({ node, replacements, ...args }));
23 |
24 | return replacements;
25 | }
26 |
27 | type VisitorArguments = BuildArgs & {
28 | node: ts.Node;
29 | replacements: ReplacementProps[];
30 | };
31 |
32 | function visit(args: VisitorArguments) {
33 | const { node, sourceFile, range, replacements, props } = args;
34 |
35 | // visiting node outside selection
36 | const nodeRange = getNodeRange(node, sourceFile);
37 | if (!range.intersection(nodeRange)) return;
38 |
39 | // looks for nested nodes
40 | ts.forEachChild(node, (node) => visit({ ...args, node }));
41 |
42 | // escapes from non-identifiers
43 | if (!ts.isIdentifier(node)) return;
44 |
45 | // escapes from tag names and attributes
46 | const invalidIdentifierParents = [
47 | ts.SyntaxKind.JsxSelfClosingElement,
48 | ts.SyntaxKind.JsxOpeningElement,
49 | ts.SyntaxKind.JsxClosingElement,
50 | ts.SyntaxKind.JsxFragment,
51 | ts.SyntaxKind.JsxOpeningFragment,
52 | ts.SyntaxKind.JsxClosingFragment,
53 | ts.SyntaxKind.JsxAttribute,
54 | ts.SyntaxKind.JsxSpreadAttribute
55 | ];
56 | if (invalidIdentifierParents.includes(node.parent?.kind)) return;
57 |
58 | // escapes from identifiers that are not the props to be passed
59 | if (!props.some((prop) => prop.name === node.getText())) return;
60 |
61 | if (node.parent?.kind === ts.SyntaxKind.ShorthandPropertyAssignment) {
62 | replacements.push({ position: nodeRange, replacement: `${node.getText()}: props.${node.getText()}` });
63 | } else {
64 | replacements.push({ position: nodeRange, replacement: `props.${node.getText()}` });
65 | }
66 | }
67 |
68 | type ApplyReplacementsArguments = BuildArgs & {
69 | replacements: ReplacementProps[];
70 | };
71 |
72 | function applyReplacementsToSelection(args: ApplyReplacementsArguments) {
73 | const { document, replacements, range: originalRange } = args;
74 | let modifiedText = document.getText();
75 | let offset = 0;
76 |
77 | for (const { position, replacement } of replacements) {
78 | const start = document.offsetAt(position.start) + offset;
79 | const end = document.offsetAt(position.end) + offset;
80 |
81 | modifiedText = modifiedText.slice(0, start) + replacement + modifiedText.slice(end);
82 |
83 | offset += replacement.length - (end - start);
84 | }
85 |
86 | // Extract the modified text using the adjusted range
87 | const startOffset = document.offsetAt(originalRange.start);
88 | const endOffset = document.offsetAt(originalRange.end) + offset;
89 | const extractedText = modifiedText.substring(startOffset, endOffset);
90 |
91 | return extractedText;
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/src/test/components/undestructuredPropsExtended/undestructuredPropsExtendedResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const shortHandConstantInFile = 'shortHandConstantInFile';
4 |
5 | function Child(props) {
6 | return {props}
;
7 | }
8 |
9 | function Component(props) {
10 | const shortHand = 'shortHand';
11 | function shortHandFunction() {
12 | return 'test';
13 | }
14 | const shortHandAnonymousFunction = () => 'test';
15 | const objectWithShortHand = { shortHand, shortHandFunction, shortHandAnonymousFunction };
16 |
17 | const myClass = 'myClass';
18 | const mmyClass = 'mmyClass';
19 | const myClass1 = 'myClass1';
20 | const myClass2 = 'myClass2';
21 | const nestedProp = { value: { default: 'default' } };
22 | const condition = true;
23 |
24 | return (
25 |
37 | );
38 | }
39 |
40 | function Extracted(props) {
41 | return (
42 | <>
43 | {
52 | const shortHandToIgnore = 'propShortHand';
53 | const shortHandObject = { shortHandToIgnore };
54 | console.log(JSON.stringify(shortHandObject));
55 | }}
56 | />
57 |
58 |
59 |
60 |
65 | {props.myClass}
66 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
91 |
92 |
93 | >
94 | );
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
6 |
7 | ## [Unreleased]
8 |
9 | ## [0.10.0] - 2024-07-21
10 |
11 | ### Changed
12 |
13 | - Extension no longer looks for diagnostics results to decide whether the extract option will show.
14 | - Verification for whether selection is JSX deterministic as opposed to a good guess.
15 |
16 | ## [0.9.0] - 2024-06-28
17 |
18 | ### Added
19 |
20 | - Configuration option for inline props type declaration.
21 | - Configuration option for keeping props undestructured.
22 |
23 | ### Fixed
24 |
25 | - Description for "Explicit Return Statement" configuration.
26 |
27 | ## [0.8.1] - 2024-05-18
28 |
29 | ### Fixed
30 |
31 | - Mismatch between explicitReturnStatement config property name and searched key.
32 |
33 | ## [0.8.0] - 2024-05-18
34 |
35 | ### Added
36 |
37 | - Configuration option for declaring component using React.FC
38 | - Configuration option for declaring arrow functions with explicit return statement.
39 |
40 | ## [0.7.4] - 2024-04-23
41 |
42 | ### Fixed
43 |
44 | - Variables at file scope being passed as props.
45 | - Imported variables and variables at file scope being passed as shorthand props.
46 |
47 | ## [0.7.3] - 2024-03-31
48 |
49 | ### Fixed
50 |
51 | - Shorthand variables declared within selection being passed as props.
52 |
53 | ## [0.7.2] - 2024-03-26
54 |
55 | ### Fixed
56 |
57 | - d.ts files not being shipped in the VSIX
58 |
59 | ## [0.7.1] - 2024-03-24
60 |
61 | ### Fixed
62 |
63 | - Typescript program not able to find global and derived types when bundled.
64 |
65 | ## [0.7.0] - 2024-03-16
66 |
67 | ### Added
68 |
69 | - Wraps selection in React Fragments if a zero or more than one parent element is present.
70 |
71 | ## [0.6.0] - 2024-03-09
72 |
73 | ### Added
74 |
75 | - Configuration option for declaring type as either interface or type.
76 | - Configuration option for declaring function as either named function or arrow function.
77 |
78 | ### Fixed
79 |
80 | - Default types from destructured and spread props from object binding typed as any
81 | - Interface extending single spread props but with no other props adding a semi-colon inside the curly brackets.
82 |
83 | ## [0.5.0] - 2024-03-03
84 |
85 | ### Changed
86 |
87 | - Category in Extension Manifest
88 | - Extension colors to match those of the latest react version
89 | - Demo gif
90 |
91 | ## [0.4.0] - 2024-03-02
92 |
93 | ### Added
94 |
95 | - Some support to prop types for props whose type is long and gets truncated
96 |
97 | ### Fixed
98 |
99 | - Prop types for props passed from function parameters
100 | - Prop types for props passed from array destructuring
101 | - Prop types for props passed from nested object destructuring
102 |
103 | ## [0.3.1] - 2024-02-28
104 |
105 | ### Fixed
106 |
107 | - Passing methods and properties of class instances
108 |
109 | ## [0.3.0] - 2024-02-27
110 |
111 | ### Added
112 |
113 | - Input treatment to the name given to the component
114 |
115 | ## [0.2.1] - 2024-02-26
116 |
117 | ### Fixed
118 |
119 | - Props passed as short-hand assignments not being extracted into new component
120 |
121 | ## [0.2.0] - 2024-02-25
122 |
123 | ### Added
124 |
125 | - Extension Icon
126 |
127 | ## [0.1.0] - 2024-02-25
128 |
129 | ### Added
130 |
131 | - Keywords at package.json
132 | - Link to extension at VS Code marketplace at README
133 | - This CHANGELOG
134 |
135 | ## [0.0.1] - 2024-02-25
136 |
137 | ### Added
138 |
139 | - Initial release with the project's MVP
140 |
141 |
--------------------------------------------------------------------------------
/src/parsers/getNodeType.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import { chooseAdequateType, truncateType } from '../utils';
3 |
4 | interface GetNodeTypeArguments {
5 | valueDeclaration: ts.Declaration;
6 | isSpread: boolean;
7 | node: ts.Node;
8 | checker: ts.TypeChecker;
9 | typeFormatFlag?: ts.TypeFormatFlags;
10 | }
11 |
12 | export function getNodeType(args: GetNodeTypeArguments) {
13 | const resolvedType = getNodeTypeString(args);
14 | const heuristicType = truncateType(getTypeHeuristically(args));
15 |
16 | return chooseAdequateType(resolvedType, heuristicType);
17 | }
18 |
19 | function getNodeTypeString({ node, checker, typeFormatFlag }: GetNodeTypeArguments) {
20 | const type = checker.getTypeAtLocation(node);
21 | const typeAsString = checker.typeToString(type, node, typeFormatFlag);
22 |
23 | const isPropTypeTruncated = /... \d+ more .../.test(typeAsString);
24 |
25 | return isPropTypeTruncated ? 'any' : typeAsString;
26 | }
27 |
28 | function getTypeHeuristically(args: GetNodeTypeArguments) {
29 | const { valueDeclaration } = args;
30 |
31 | const shouldGetHeuristicType = [ts.SyntaxKind.BindingElement, ts.SyntaxKind.Parameter].includes(
32 | valueDeclaration.kind
33 | );
34 | if (!shouldGetHeuristicType) return 'any';
35 |
36 | if (valueDeclaration.kind === ts.SyntaxKind.Parameter) {
37 | return getParameterType({ valueDeclaration });
38 | }
39 |
40 | const isSpreadDeclaration = valueDeclaration.getFirstToken()?.kind === ts.SyntaxKind.DotDotDotToken ?? false;
41 | const parent = valueDeclaration.parent;
42 | const parentType = getValueDeclarationParentType({
43 | ...args,
44 | node: parent,
45 | typeFormatFlag: ts.TypeFormatFlags.NodeBuilderFlagsMask
46 | });
47 |
48 | if (parentType === 'any') return isSpreadDeclaration ? 'Record' : 'any';
49 |
50 | if (parent.kind === ts.SyntaxKind.ArrayBindingPattern) {
51 | return getArrayBoundValueType({ isSpreadDeclaration, parentType });
52 | }
53 |
54 | if (parent.kind === ts.SyntaxKind.ObjectBindingPattern) {
55 | return getObjectBoundValueType({ isSpreadDeclaration, parentType, parent, valueDeclaration });
56 | }
57 |
58 | // Unforeseen case
59 | return 'any';
60 | }
61 |
62 | interface GetArrayBoundValueTypeArgs {
63 | isSpreadDeclaration: boolean;
64 | parentType: string;
65 | }
66 |
67 | function getArrayBoundValueType({ isSpreadDeclaration, parentType }: GetArrayBoundValueTypeArgs) {
68 | if (isSpreadDeclaration) {
69 | return parentType;
70 | } else {
71 | const arrayArgumentAngleBracketsSyntax = [...(parentType.match(/Array<([\s\S]+)>/) ?? [])][1];
72 | const arrayArgumentSquareBracketSyntax = [...(parentType.match(/([\s\S]+)\[]/) ?? [])][1];
73 | return arrayArgumentAngleBracketsSyntax || arrayArgumentSquareBracketSyntax || `${parentType}[number]`;
74 | }
75 | }
76 |
77 | interface GetObjectBoundValueTypeArgs {
78 | parent: ts.Node;
79 | valueDeclaration: ts.Node;
80 | isSpreadDeclaration: boolean;
81 | parentType: string;
82 | }
83 |
84 | function getObjectBoundValueType({
85 | parent,
86 | valueDeclaration,
87 | isSpreadDeclaration,
88 | parentType
89 | }: GetObjectBoundValueTypeArgs) {
90 | if (isSpreadDeclaration) {
91 | const siblings = new Set();
92 |
93 | parent.forEachChild((child) => {
94 | if (child !== valueDeclaration) {
95 | child.forEachChild((child) => {
96 | if (child.kind === ts.SyntaxKind.Identifier) {
97 | siblings.add(child.getText());
98 | }
99 | });
100 | }
101 | });
102 |
103 | if (siblings.size === 0) return parentType;
104 |
105 | return `Omit<${parentType}, ${[...siblings.values()].map((v) => `'${v}'`).join(' | ')}>`;
106 | } else {
107 | return `${parentType}['${valueDeclaration.getText()}']`;
108 | }
109 | }
110 |
111 | interface GetTypeByTypeReferenceArgs {
112 | valueDeclaration: ts.Node;
113 | }
114 |
115 | function getParameterType({ valueDeclaration }: GetTypeByTypeReferenceArgs) {
116 | let type = 'any';
117 | valueDeclaration.forEachChild((child) => {
118 | if (child.kind === ts.SyntaxKind.TypeReference) {
119 | type = child.getText();
120 | }
121 | });
122 | return type;
123 | }
124 |
125 | function getValueDeclarationParentType(args: GetNodeTypeArguments) {
126 | const { node: parent } = args;
127 | const parentResolvedType = getNodeTypeString(args);
128 | let parentHeuristicType = 'any';
129 |
130 | if (parent.kind === ts.SyntaxKind.ObjectBindingPattern && parent.parent.kind === ts.SyntaxKind.Parameter) {
131 | parentHeuristicType = getParameterType({ valueDeclaration: parent.parent });
132 | }
133 |
134 | const parentType = chooseAdequateType(parentResolvedType, parentHeuristicType);
135 | return parentType;
136 | }
137 |
--------------------------------------------------------------------------------
/src/test/utils.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 | import { capitalizeComponentName, chooseAdequateType, removeNonWordCharacters, truncateType } from '../utils';
3 |
4 | suite('removeNonWordCharacters', function () {
5 | test('should remove non-word characters from the string', function () {
6 | const input = 'Hello, World!';
7 | const expected = 'HelloWorld';
8 | const result = removeNonWordCharacters(input);
9 | assert.strictEqual(result, expected);
10 | });
11 |
12 | test('should return an empty string if the input is empty', function () {
13 | const input = '';
14 | const expected = '';
15 | const result = removeNonWordCharacters(input);
16 | assert.strictEqual(result, expected);
17 | });
18 |
19 | test('should return an empty string if the input has only non word characters', function () {
20 | const input = ' ';
21 | const expected = '';
22 | const result = removeNonWordCharacters(input);
23 | assert.strictEqual(result, expected);
24 | });
25 |
26 | test('should return the same string if it contains only word characters', function () {
27 | const input = 'HelloWorld';
28 | const expected = 'HelloWorld';
29 | const result = removeNonWordCharacters(input);
30 | assert.strictEqual(result, expected);
31 | });
32 | });
33 |
34 | suite('capitalizeComponentName', function () {
35 | test('should capitalize the component name', function () {
36 | const input = 'button';
37 | const expected = 'Button';
38 | const result = capitalizeComponentName(input);
39 | assert.strictEqual(result, expected);
40 | });
41 |
42 | test("shouldn't do anything if name is already capitalized", function () {
43 | const input = 'Button';
44 | const expected = 'Button';
45 | const result = capitalizeComponentName(input);
46 | assert.strictEqual(result, expected);
47 | });
48 | });
49 |
50 | suite('truncateType', function () {
51 | test('should return the same string if its length is less than or equal to 500', function () {
52 | const type = 'string';
53 | const result = truncateType(type);
54 | const expected = type;
55 | assert.strictEqual(result, expected);
56 | });
57 |
58 | test('should return "any" if the string length is more than 500', function () {
59 | const type = 'a'.repeat(501);
60 | const result = truncateType(type);
61 | const expected = 'any';
62 | assert.strictEqual(result, expected);
63 | });
64 |
65 | test('should return the same string if its length is exactly 500', function () {
66 | const type = 'a'.repeat(500);
67 | const result = truncateType(type);
68 | const expected = type;
69 | assert.strictEqual(result, expected);
70 | });
71 |
72 | test('should return an empty string if the string has length 0', function () {
73 | const type = '';
74 | const result = truncateType(type);
75 | const expected = type;
76 | assert.strictEqual(result, expected);
77 | });
78 | });
79 |
80 | suite('chooseAdequateType', function () {
81 | test('should return resolvedType if resolvedType is not "any" and heuristicType is "any"', function () {
82 | const resolvedType = 'string';
83 | const heuristicType = 'any';
84 | const expected = resolvedType;
85 | const result = chooseAdequateType(resolvedType, heuristicType);
86 | assert.strictEqual(result, expected);
87 | });
88 |
89 | test('should return heuristicType if resolvedType is "any" and heuristicType is not "any"', function () {
90 | const resolvedType = 'any';
91 | const heuristicType = 'string';
92 | const expected = heuristicType;
93 | const result = chooseAdequateType(resolvedType, heuristicType);
94 | assert.strictEqual(result, expected);
95 | });
96 |
97 | test('should return resolvedType if resolvedType and heuristicType are not "any" and resolvedType length is less than or equal to heuristicType length', function () {
98 | const resolvedType = 'string';
99 | const heuristicType = 'number';
100 | const expected = resolvedType;
101 | const result = chooseAdequateType(resolvedType, heuristicType);
102 | assert.strictEqual(result, expected);
103 | });
104 |
105 | test('should return heuristicType if resolvedType and heuristicType are not "any" and heuristicType length is less than resolvedType length', function () {
106 | const resolvedType = `{
107 | title?: string | undefined;
108 | prefix?: string | undefined;
109 | property?: string | undefined;
110 | slot?: string | undefined;
111 | key?: React.Key | null | undefined;
112 | defaultChecked?: boolean | undefined;
113 | ... 257 more ...;
114 | onTransitionEndCapture?: React.TransitionEventHandler<...> | undefined;`;
115 | const heuristicType = "ComponentPropsWithoutRef<'div'>";
116 | const expected = heuristicType;
117 | const result = chooseAdequateType(resolvedType, heuristicType);
118 | assert.strictEqual(result, expected);
119 | });
120 |
121 | test('should return "any" if resolvedType and heuristicType are "any"', function () {
122 | const resolvedType = 'any';
123 | const heuristicType = 'any';
124 | const expected = 'any';
125 | const result = chooseAdequateType(resolvedType, heuristicType);
126 | assert.strictEqual(result, expected);
127 | });
128 | });
129 |
130 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-extract",
3 | "displayName": "React Extract",
4 | "description": "Extract a valid piece of JSX code into a new function, passing props and creating its interface.",
5 | "publisher": "joao-mbn",
6 | "version": "0.10.0",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/joao-mbn/react-extract"
10 | },
11 | "icon": "./assets/logo-square.png",
12 | "engines": {
13 | "vscode": "^1.86.0"
14 | },
15 | "categories": [
16 | "Formatters",
17 | "Programming Languages"
18 | ],
19 | "galleryBanner": {
20 | "color": "#23272f",
21 | "theme": "dark"
22 | },
23 | "keywords": [
24 | "react",
25 | "refactor",
26 | "typescript",
27 | "javascript",
28 | "jsx",
29 | "tsx"
30 | ],
31 | "activationEvents": [
32 | "onLanguage:javascript",
33 | "onLanguage:typescript",
34 | "onLanguage:javascriptreact",
35 | "onLanguage:typescriptreact"
36 | ],
37 | "main": "./dist/extension.js",
38 | "contributes": {
39 | "commands": [
40 | {
41 | "command": "reactExtract.extractComponent",
42 | "title": "React Extract: Extract Component"
43 | }
44 | ],
45 | "menus": {
46 | "commandPalette": [
47 | {
48 | "command": "reactExtract.extractComponent",
49 | "when": "false"
50 | }
51 | ]
52 | },
53 | "configuration": {
54 | "title": "React Extract",
55 | "properties": {
56 | "reactExtract.typeDeclaration": {
57 | "title": "Type Declaration",
58 | "enum": [
59 | "interface",
60 | "type",
61 | "inline"
62 | ],
63 | "enumItemLabels": [
64 | "Interface",
65 | "Type",
66 | "Inline"
67 | ],
68 | "enumDescriptions": [
69 | "interface ComponentProps {\n //...\n}",
70 | "type ComponentProps = {\n //...\n}",
71 | "function Component(props: { onClick: () => void; value: number; options: number[] }) {\n //...\n}"
72 | ],
73 | "type": "string",
74 | "default": "interface",
75 | "description": "The type of type declaration to be used when extracting the component.",
76 | "order": 1
77 | },
78 | "reactExtract.functionDeclaration": {
79 | "title": "Function Declaration",
80 | "enum": [
81 | "arrow",
82 | "function"
83 | ],
84 | "enumItemLabels": [
85 | "Arrow Function",
86 | "Named Function"
87 | ],
88 | "enumDescriptions": [
89 | "const Component () => (\n //...\n)",
90 | "function Component() {\n //...\n}"
91 | ],
92 | "type": "string",
93 | "default": "function",
94 | "description": "The type of function declaration to be used when extracting the component.",
95 | "order": 2
96 | },
97 | "reactExtract.declareWithReactFC": {
98 | "title": "Declare component using React.FC",
99 | "enum": [
100 | "true",
101 | "false"
102 | ],
103 | "enumItemLabels": [
104 | "true",
105 | "false"
106 | ],
107 | "enumDescriptions": [
108 | "const Component: React.FC = ({...props}) => (\n //...\n)",
109 | "const Component = ({...props}: ComponentProps) => (\n //...\n)"
110 | ],
111 | "type": "string",
112 | "default": "false",
113 | "description": "Whether to declare the component using React.FC or not. \n[Only takes effect if \"Function Declaration\" is set to \"Arrow Function\"].",
114 | "order": 3
115 | },
116 | "reactExtract.destructureProps": {
117 | "title": "Create extracted component destructuring props parameter",
118 | "enum": [
119 | "true",
120 | "false"
121 | ],
122 | "enumItemLabels": [
123 | "true",
124 | "false"
125 | ],
126 | "enumDescriptions": [
127 | "function Component ({ onClick, className }: ComponentProps) {\n return () \n}",
128 | "function Component (props: ComponentProps) {\n return () \n}"
129 | ],
130 | "type": "string",
131 | "default": "true",
132 | "description": "Whether to do object destructure in the extracted component props parameter or not. If set to \"false\", the parameter will be named \"props\".",
133 | "order": 4
134 | },
135 | "reactExtract.explicitReturnStatement": {
136 | "title": "Create extracted component with explicit return statement",
137 | "enum": [
138 | "true",
139 | "false"
140 | ],
141 | "enumItemLabels": [
142 | "true",
143 | "false"
144 | ],
145 | "enumDescriptions": [
146 | "const Component = () => { return ( ) }",
147 | "const Component = () => ( )"
148 | ],
149 | "type": "string",
150 | "default": "false",
151 | "description": "Whether to create the extracted component with explicit return statement or not. \n[Only takes effect if \"Function Declaration\" is set to \"Arrow Function\"].",
152 | "order": 5
153 | }
154 | }
155 | }
156 | },
157 | "scripts": {
158 | "vscode:prepublish": "npm run package",
159 | "compile": "webpack",
160 | "watch": "webpack --watch",
161 | "package": "webpack --mode production --devtool hidden-source-map",
162 | "pretest": "npm run compile-tests && npm run compile && npm run lint",
163 | "test": "vscode-test",
164 | "compile-tests": "tsc -p . --outDir out",
165 | "watch-tests": "tsc -p . -w --outDir out",
166 | "create-test": "node ./src/test/createTestCase.js",
167 | "lint": "eslint src --ext ts",
168 | "lint:fix": "eslint src --fix",
169 | "prettier:write": "prettier src --write"
170 | },
171 | "devDependencies": {
172 | "@types/mocha": "^10.0.6",
173 | "@types/node": "18.x",
174 | "@types/react": "^18.2.58",
175 | "@types/react-dom": "^18.2.19",
176 | "@types/vscode": "^1.86.0",
177 | "@typescript-eslint/eslint-plugin": "^6.19.1",
178 | "@typescript-eslint/parser": "^6.19.1",
179 | "@vscode/test-cli": "^0.0.4",
180 | "@vscode/test-electron": "^2.3.9",
181 | "copy-webpack-plugin": "^12.0.2",
182 | "eslint": "^8.56.0",
183 | "eslint-config-prettier": "^9.1.0",
184 | "eslint-plugin-react": "^7.35.0",
185 | "prettier": "^3.2.5",
186 | "react": "^18.2.0",
187 | "react-dom": "^18.2.0",
188 | "ts-loader": "^9.5.1",
189 | "webpack": "^5.90.0",
190 | "webpack-cli": "^5.1.4"
191 | },
192 | "dependencies": {
193 | "typescript": "^5.3.3"
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VS Code React Extract
2 |
3 |
4 |
5 |
6 |
7 | This extension for Visual Studio Code provides a quick way to refactor your React code. It allows you to extract a valid piece of component code into a new function, automatically passing the props and building the extracted component interface, if using Typescript.
8 |
9 | ## Installation
10 |
11 | [Get it at Visual Studio Code Marketplace: React Extract](https://marketplace.visualstudio.com/items?itemName=joao-mbn.react-extract)
12 |
13 | ## Features
14 |
15 | 
16 |
17 | - **Code Extraction**: Select a valid piece of React component code that you want to refactor.
18 |
19 | - **Quick Refactor Action**: Use the Code Actions feature (`Ctrl + .` or `Cmd + .`, by default) to initiate the refactoring process.
20 |
21 | - **Component Naming**: Pass the component name at the input prompt.
22 |
23 | - **Automatic Prop Passing**: The extension will automatically identify and pass the necessary props to the new function.
24 |
25 | - **TypeScript Support**: If you're using TypeScript, the extension will also build the interface for the new function.
26 |
27 | - **Code Placement**: The new function will be placed at the bottom of the current file.
28 |
29 | ### Configurations
30 |
31 | You may customize the way that the extracted component is built, with the following options on your Settings (`Ctrl + ,` or `Cmd + ,`, by default):
32 |
33 | #### "Type Declaration" | "reactExtract.typeDeclaration"
34 |
35 | - **Description**: The type of type declaration to be used when extracting the component.
36 |
37 | - **Accepts**: `"interface" | "type" | "inline"`
38 |
39 | - **Default**: `"interface"`
40 |
41 | ```typescript
42 | // interface
43 | interface ComponentProps {
44 | // ...
45 | }
46 |
47 | // type
48 | type ComponentProps = {
49 | // ...
50 | };
51 |
52 | // inline
53 | function Component(props: { onClick: () => void; value: number; options: number[] }) {
54 | // ...
55 | }
56 | ```
57 |
58 | #### "Function Declaration" | "reactExtract.functionDeclaration"
59 |
60 | - **Description**: The type of function declaration to be used when extracting the component.
61 |
62 | - **Accepts**: `"arrow" | "function"`
63 |
64 | - **Default**: `"function"`
65 |
66 | ```javascript
67 | // arrow
68 | const Component = () => (
69 | //...
70 | )
71 |
72 | // function
73 | function Component() {
74 | //...
75 | }
76 | ```
77 |
78 | #### "Declare With React FC" | "reactExtract.declareWithReactFC"
79 |
80 | - **Description**: Whether to declare the component using React.FC or not. [Only takes effect if "Function Declaration" is set to "Arrow Function"].
81 |
82 | - **Accepts**: `"true" | "false"`
83 |
84 | - **Default**: `"false"`
85 |
86 | ```typescript
87 | // true
88 | const Component: React.FC = ({...props}) => (
89 | //...
90 | )
91 |
92 | // false
93 | const Component = ({...props}: ComponentProps) => (
94 | //...
95 | )
96 | ```
97 |
98 | #### "Destructure Props" | "reactExtract.destructureProps"
99 |
100 | - **Description**: Whether to do object destructure in the extracted component props parameter or not. If set to "false", the parameter will be named "props".
101 |
102 | - **Accepts**: `"true" | "false"`
103 |
104 | - **Default**: `"true"`
105 |
106 | ```tsx
107 | // true
108 | function Component({ onClick, className }: ComponentProps) {
109 | return ;
110 | }
111 |
112 | // false
113 | function Component(props: ComponentProps) {
114 | return ;
115 | }
116 | ```
117 |
118 | _Note: If Destructure Props is set to false, but one of the props is a spread, props will be passed as destructured. Example:_
119 |
120 | ```jsx
121 | // Before extraction
122 | function Parent() {
123 | const childProps = {
124 | className: 'myClass',
125 | onClick: () => console.log('Clicked!')
126 | };
127 |
128 | return Child Component
;
129 | }
130 |
131 | // After extraction
132 | function Parent() {
133 | const childProps = {
134 | className: 'myClass',
135 | onClick: () => console.log('Clicked!')
136 | };
137 |
138 | return ;
139 | }
140 |
141 | function Child({ ...childProps }) {
142 | return Child Component
;
143 | }
144 | ```
145 |
146 | #### "Explicit Return Statement" | "reactExtract.explicitReturnStatement"
147 |
148 | - **Description**: Whether to create the extracted component with explicit return statement or not. [Only takes effect if "Function Declaration" is set to "Arrow Function"].
149 |
150 | - **Accepts**: `"true" | "false"`
151 |
152 | - **Default**: `"false"`
153 |
154 | ```tsx
155 | // true
156 | const Component: React.FC = ({ ...props }) => {
157 | return ;
158 | };
159 |
160 | // false
161 | const Component = ({ ...props }: ComponentProps) => ;
162 | ```
163 |
164 | ## Contributions
165 |
166 | If you encounter any problems or have suggestions for improvements, please open an issue. Your feedback and contribution is appreciated. If you have the agreed solution as well, please open a pull request.
167 |
168 | ### Application Setup
169 |
170 | Clone the repo, install dependencies and enter VS Code.
171 |
172 | ```sh
173 | $ https://github.com/joao-mbn/react-extract.git
174 | $ cd react-extract
175 | $ npm i
176 | $ code .
177 | ```
178 |
179 | ### Running and Debugging the Application
180 |
181 | - Comment the following block of code in `webpack.config.js` to avoid conflicts with the ts-lib files in the `node_modules`:
182 |
183 | ```javascript
184 | plugins: [
185 | new CopyPlugin({
186 | patterns: [{ from: 'node_modules/typescript/lib/*.d.ts', to: '[name][ext]' }]
187 | })
188 | ];
189 | ```
190 |
191 | - Go to **Run and Debug** and select **Run Extension** from the menu. Hit the play button or F5. For more information go to the [Official VS Code Extension Development Docs](https://code.visualstudio.com/api/get-started/your-first-extension).
192 |
193 | ### Running Tests
194 |
195 | 1. Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) and [TypeScript + Webpack Problem Matchers](https://marketplace.visualstudio.com/items?itemName=amodio.tsl-problem-matcher) extensions.
196 | 2. Open the Command Palette `Ctrl + Shift + P` or `Cmd + Shift + P`.
197 | 3. Select **Task: Run Task**.
198 | 4. Select **tasks: watch tests**.
199 | 5. Run the tests from the test explorer.
200 |
201 | #### Creating a new Integration test for Extract Component
202 |
203 | You can add a new folder under the `src/test/components` folder with the .jsx and .tsx files and their results using the project's naming convention via a script, as follows:
204 |
205 | ```sh
206 | $ npm run create-test myNewTestCase
207 | ```
208 |
209 | _Replace **myNewTestCase** with the appropriate test case name._
210 |
211 | ## Release Notes
212 |
213 | [Checkout the Changelog](./CHANGELOG.md)
214 |
215 | ## Limitations
216 |
217 | This extension is currently not fully supportive of Class Components. You can use it just fine on them, but the passed props may be wrongy extracted. Let it be known if you wish full support on them.
218 |
219 | Even with this disclaimer, be welcome to open issues related to its use with Class Components, as to improve the implementation when support is given.
220 |
221 |
--------------------------------------------------------------------------------
/src/parsers/extractProps.ts:
--------------------------------------------------------------------------------
1 | import ts from 'typescript';
2 | import { ExtractedProp, ExtractionArgs } from '../types';
3 | import { getNodeType } from './getNodeType';
4 | import { getNodeRange } from './parsingUtils';
5 |
6 | export function extractProps(args: ExtractionArgs) {
7 | const { program, sourceFile } = args;
8 |
9 | const checker = program.getTypeChecker();
10 |
11 | const props: Map = new Map();
12 | ts.forEachChild(sourceFile, (node) => visit({ node, checker, props, ...args }));
13 |
14 | return [...props.values()];
15 | }
16 |
17 | interface VisitorArguments extends ExtractionArgs {
18 | node: ts.Node;
19 | checker: ts.TypeChecker;
20 | props: Map;
21 | }
22 |
23 | function visit(args: VisitorArguments) {
24 | const { node, sourceFile, range, checker, props, isTypescript } = args;
25 |
26 | // visiting node outside selection
27 | const nodeRange = getNodeRange(node, sourceFile);
28 | if (!range.intersection(nodeRange)) return;
29 |
30 | // looks for nested nodes
31 | ts.forEachChild(node, (node) => visit({ ...args, node }));
32 |
33 | if (!ts.isIdentifier(node)) return;
34 |
35 | // escapes from tag names
36 | const invalidIdentifierParents = [
37 | ts.SyntaxKind.JsxSelfClosingElement,
38 | ts.SyntaxKind.JsxOpeningElement,
39 | ts.SyntaxKind.JsxClosingElement,
40 | ts.SyntaxKind.JsxFragment,
41 | ts.SyntaxKind.JsxOpeningFragment,
42 | ts.SyntaxKind.JsxClosingFragment
43 | ];
44 | if (invalidIdentifierParents.includes(node.parent?.kind)) return;
45 |
46 | // value is literal undefined
47 | if (node.getText() === 'undefined') return;
48 |
49 | // node does not reference a named entity
50 | const symbol = checker.getSymbolAtLocation(node);
51 | if (!symbol) return;
52 |
53 | const valueDeclaration = symbol?.valueDeclaration;
54 | if (!valueDeclaration) return;
55 |
56 | if (!shouldSymbolValueDeclarationBePassedAsProps({ ...args, valueDeclaration })) return;
57 |
58 | const isSpread = node.parent?.kind === ts.SyntaxKind.JsxSpreadAttribute;
59 | const type = isTypescript ? getNodeType({ node, checker, valueDeclaration, isSpread }) : 'any';
60 |
61 | const newProp = { name: node.getText(), type, isSpread };
62 | props.set(newProp.name, newProp);
63 | }
64 |
65 | function shouldSymbolValueDeclarationBePassedAsProps(args: VisitorArguments & { valueDeclaration: ts.Node }) {
66 | const { sourceFile, range, valueDeclaration } = args;
67 |
68 | // value is imported or is a global variable, and thus not bound to the function scope in which the selection is made
69 | if (valueDeclaration.getSourceFile().fileName !== sourceFile.fileName) return false;
70 |
71 | // types of declarations that should be passed as props, by trial and error with TS AST.
72 | const allowedDeclarationKinds = [
73 | ts.SyntaxKind.BindingElement,
74 | ts.SyntaxKind.Parameter,
75 | ts.SyntaxKind.ShorthandPropertyAssignment,
76 | ts.SyntaxKind.FunctionDeclaration,
77 | ts.SyntaxKind.VariableDeclaration
78 | ];
79 | if (!allowedDeclarationKinds.includes(valueDeclaration.kind)) return false;
80 |
81 | // A value declaration that is at the current file scope if it does not have any block statement containing it
82 | // or it's not passed as function parameter.
83 | // They shouldn't be passed as props, as they are available to the extracted component as well.
84 | if (isValueDeclarationAtFileScope({ ...args, valueDeclaration })) return false;
85 |
86 | // value is declared or is a parameter in the selection itself,
87 | // e.g. { const target = e.target; doStuff(target); }} />
88 | // "onClick" is a JSX Attribute, "e" is a parameter and "target" is a declaration in the selection, but not "doStuff".
89 | // shorthand assignments are exceptions to be checked apart, as declaration and reference occupy the same range.
90 | const declarationRange = getNodeRange(valueDeclaration, sourceFile);
91 | if (range.intersection(declarationRange)) {
92 | if (valueDeclaration.kind !== ts.SyntaxKind.ShorthandPropertyAssignment) return false;
93 |
94 | if (shouldShorthandBeIgnored({ ...args })) return false;
95 | }
96 |
97 | return true;
98 | }
99 |
100 | /**
101 | * Checks if the value declaration is a constant declared in the current file scope.
102 | * A value declaration that is within the current file and is available at the component
103 | * if it does not have any block statement containing it or it's not passed as function parameter.
104 | * @param args - The visitor arguments.
105 | * @param args.valueDeclaration - The value declaration node.
106 | * @returns A boolean indicating whether the value declaration is a constant declared in the current file scope.
107 | */
108 |
109 | function isValueDeclarationAtFileScope({ valueDeclaration }: VisitorArguments & { valueDeclaration: ts.Node }) {
110 | let valueDeclarationContainer: ts.Node = valueDeclaration;
111 | let isAtFileScope = true;
112 |
113 | while (valueDeclarationContainer && isAtFileScope) {
114 | if ([ts.SyntaxKind.Parameter, ts.SyntaxKind.Block].includes(valueDeclarationContainer.kind)) {
115 | isAtFileScope = false;
116 | }
117 | valueDeclarationContainer = valueDeclarationContainer.parent;
118 | }
119 |
120 | return isAtFileScope;
121 | }
122 |
123 | /**
124 | * Determines whether the shorthand should be ignored based on the provided visitor arguments.
125 | * It will be ignored if the shorthand is declared within the selection or is at global/import/file scope.
126 | * @param arguments - The visitor arguments containing the node, source file, and range.
127 | * @returns - True if the shorthand should be ignored, false otherwise.
128 | */
129 | function shouldShorthandBeIgnored({ node, sourceFile, range }: VisitorArguments) {
130 | /**
131 | * There's room for optimizations in this checker:
132 | * The first found reference may be within the JSX Expression but out of scope, meaning that a wrong variable has been found.
133 | * Example: In the else statement, the variable shortHand referenced is not the one in the if statement, but that's the one first found.
134 | *
135 | * if (shortHand) {
136 | * const shortHand = 'propShortHand';
137 | * const shortHandObject = { shortHand };
138 | * } else {
139 | * const shortHandObject = { shortHand };
140 | * }
141 | */
142 |
143 | let hasFoundReference = false;
144 |
145 | function shortHandContainerVisitor(currentNode: ts.Node) {
146 | if (hasFoundReference) return;
147 |
148 | if (ts.isIdentifier(currentNode) && currentNode.getText() === node.getText() && node !== currentNode) {
149 | hasFoundReference = true;
150 | } else {
151 | ts.forEachChild(currentNode, shortHandContainerVisitor);
152 | }
153 | }
154 |
155 | // Reference is within selection
156 | let shorthandContainer = node.parent;
157 | while (range.contains(getNodeRange(shorthandContainer, sourceFile)) && shorthandContainer.parent) {
158 | shorthandContainer = shorthandContainer.parent;
159 | }
160 |
161 | shorthandContainer.forEachChild(shortHandContainerVisitor);
162 |
163 | if (hasFoundReference) return true;
164 |
165 | // Reference is outside selection, but within some component scope containing the shorthand
166 | let outermostParentComponentScope: ts.Node | null = null;
167 | let scopePointer = shorthandContainer;
168 | while (scopePointer) {
169 | if (ts.isFunctionDeclaration(scopePointer) || ts.isArrowFunction(scopePointer)) {
170 | outermostParentComponentScope = scopePointer;
171 | }
172 | scopePointer = scopePointer.parent;
173 | }
174 |
175 | outermostParentComponentScope?.forEachChild(shortHandContainerVisitor);
176 |
177 | // if hasFoundReference is true, the reference is within the component scope, else it's at global/import/file scope.
178 | return !hasFoundReference;
179 | }
180 |
181 |
--------------------------------------------------------------------------------
/src/test/common.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import * as vscode from 'vscode';
4 |
5 | async function getDocuments(folder: string) {
6 | const filePath = path.join(__dirname, '../../src/test/components/', folder);
7 | const fileNames = fs.readdirSync(filePath);
8 |
9 | const tsResultFilePath = fileNames.find((fileName) => fileName.endsWith('Result.tsx'));
10 | const tsTestFilePath = fileNames.find((fileName) => !fileName.endsWith('Result.tsx') && fileName.endsWith('.tsx'));
11 | const jsResultFilePath = fileNames.find((fileName) => fileName.endsWith('Result.jsx'));
12 | const jsTestFilePath = fileNames.find((fileName) => !fileName.endsWith('Result.jsx') && fileName.endsWith('.jsx'));
13 |
14 | const [tsResult, tsTest, jsResult, jsTest] = await Promise.all([
15 | tsResultFilePath && vscode.workspace.openTextDocument(path.join(filePath, tsResultFilePath)),
16 | tsTestFilePath && vscode.workspace.openTextDocument(path.join(filePath, tsTestFilePath)),
17 | jsResultFilePath && vscode.workspace.openTextDocument(path.join(filePath, jsResultFilePath)),
18 | jsTestFilePath && vscode.workspace.openTextDocument(path.join(filePath, jsTestFilePath))
19 | ]);
20 |
21 | return { tsResult, tsTest, jsResult, jsTest } as Record<
22 | 'tsResult' | 'tsTest' | 'jsResult' | 'jsTest',
23 | vscode.TextDocument
24 | >;
25 | }
26 |
27 | export const allRanges: Record = {
28 | noProps: {
29 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10)),
30 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10))
31 | },
32 | fragment: {
33 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(11, 7)),
34 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(11, 7))
35 | },
36 | onlyStatic: {
37 | typescript: new vscode.Range(new vscode.Position(5, 4), new vscode.Position(10, 10)),
38 | javascript: new vscode.Range(new vscode.Position(5, 4), new vscode.Position(10, 10))
39 | },
40 | static: {
41 | typescript: new vscode.Range(new vscode.Position(8, 4), new vscode.Position(18, 10)),
42 | javascript: new vscode.Range(new vscode.Position(8, 4), new vscode.Position(18, 10))
43 | },
44 | implicit: {
45 | typescript: new vscode.Range(new vscode.Position(6, 4), new vscode.Position(12, 11)),
46 | javascript: new vscode.Range(new vscode.Position(6, 4), new vscode.Position(12, 11))
47 | },
48 | conditional: {
49 | typescript: new vscode.Range(new vscode.Position(5, 4), new vscode.Position(10, 10)),
50 | javascript: new vscode.Range(new vscode.Position(5, 4), new vscode.Position(10, 10))
51 | },
52 | componentAsFunction: {
53 | typescript: new vscode.Range(new vscode.Position(13, 4), new vscode.Position(16, 10)),
54 | javascript: new vscode.Range(new vscode.Position(13, 4), new vscode.Position(16, 10))
55 | },
56 | componentAsProps: {
57 | typescript: new vscode.Range(new vscode.Position(13, 4), new vscode.Position(16, 10)),
58 | javascript: new vscode.Range(new vscode.Position(13, 4), new vscode.Position(16, 10))
59 | },
60 | map: {
61 | typescript: new vscode.Range(new vscode.Position(18, 4), new vscode.Position(25, 7)),
62 | javascript: new vscode.Range(new vscode.Position(18, 4), new vscode.Position(25, 7))
63 | },
64 | subSelection: {
65 | typescript: new vscode.Range(new vscode.Position(12, 6), new vscode.Position(15, 34)),
66 | javascript: new vscode.Range(new vscode.Position(12, 6), new vscode.Position(15, 34))
67 | },
68 | textChild: {
69 | typescript: new vscode.Range(new vscode.Position(6, 4), new vscode.Position(9, 10)),
70 | javascript: new vscode.Range(new vscode.Position(6, 4), new vscode.Position(9, 10))
71 | },
72 | shortHand: {
73 | typescript: new vscode.Range(new vscode.Position(18, 4), new vscode.Position(26, 6)),
74 | javascript: new vscode.Range(new vscode.Position(18, 4), new vscode.Position(26, 6))
75 | },
76 | longType: {
77 | typescript: new vscode.Range(new vscode.Position(38, 9), new vscode.Position(38, 39)),
78 | javascript: new vscode.Range(new vscode.Position(38, 9), new vscode.Position(38, 39))
79 | },
80 | propertiesAndMethods: {
81 | typescript: new vscode.Range(new vscode.Position(21, 4), new vscode.Position(28, 6)),
82 | javascript: new vscode.Range(new vscode.Position(21, 4), new vscode.Position(28, 6))
83 | },
84 | destructureRename: {
85 | typescript: new vscode.Range(new vscode.Position(5, 9), new vscode.Position(5, 42)),
86 | javascript: new vscode.Range(new vscode.Position(5, 9), new vscode.Position(5, 42))
87 | },
88 | destructureNested: {
89 | typescript: new vscode.Range(new vscode.Position(8, 9), new vscode.Position(8, 44)),
90 | javascript: new vscode.Range(new vscode.Position(8, 9), new vscode.Position(8, 44))
91 | },
92 | parameter: {
93 | typescript: new vscode.Range(new vscode.Position(13, 4), new vscode.Position(16, 10)),
94 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(7, 10))
95 | },
96 | parameterTypeReference: {
97 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(7, 10)),
98 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(7, 10))
99 | },
100 | spread: {
101 | typescript: new vscode.Range(new vscode.Position(11, 9), new vscode.Position(11, 31)),
102 | javascript: new vscode.Range(new vscode.Position(3, 9), new vscode.Position(3, 31))
103 | },
104 | spreadArray: {
105 | typescript: new vscode.Range(new vscode.Position(12, 4), new vscode.Position(16, 10)),
106 | javascript: new vscode.Range(new vscode.Position(8, 4), new vscode.Position(12, 10))
107 | },
108 | spreadNested: {
109 | typescript: new vscode.Range(new vscode.Position(16, 4), new vscode.Position(21, 10)),
110 | javascript: new vscode.Range(new vscode.Position(8, 4), new vscode.Position(13, 10))
111 | },
112 | spreadNestedTypeReference: {
113 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10)),
114 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10))
115 | },
116 | spreadAny: {
117 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10)),
118 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10))
119 | },
120 | typeInlineDeclaration: {
121 | typescript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10)),
122 | javascript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10))
123 | },
124 | typeInlineDeclarationEmpty: {
125 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10)),
126 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10))
127 | },
128 | typeInlineDeclarationExtended: {
129 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10)),
130 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10))
131 | },
132 | typeInlineDeclarationReactFC: {
133 | typescript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10)),
134 | javascript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10))
135 | },
136 | typeTypeDeclaration: {
137 | typescript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10)),
138 | javascript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10))
139 | },
140 | typeTypeDeclarationEmpty: {
141 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10)),
142 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10))
143 | },
144 | typeTypeDeclarationExtended: {
145 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10)),
146 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10))
147 | },
148 | arrowFunctionDeclarationEmpty: {
149 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10)),
150 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10))
151 | },
152 | arrowFunctionDeclarationSpread: {
153 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10)),
154 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(9, 10))
155 | },
156 | arrowFunctionExplicitReturn: {
157 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(7, 10)),
158 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(7, 10))
159 | },
160 | reactFCType: {
161 | typescript: new vscode.Range(new vscode.Position(6, 4), new vscode.Position(9, 10)),
162 | javascript: new vscode.Range(new vscode.Position(6, 4), new vscode.Position(9, 10))
163 | },
164 | reactFCTypeEmpty: {
165 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(7, 10)),
166 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(7, 10))
167 | },
168 | wrapInFragment: {
169 | typescript: new vscode.Range(new vscode.Position(5, 6), new vscode.Position(7, 21)),
170 | javascript: new vscode.Range(new vscode.Position(5, 6), new vscode.Position(7, 21))
171 | },
172 | undestructuredProps: {
173 | typescript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10)),
174 | javascript: new vscode.Range(new vscode.Position(7, 4), new vscode.Position(9, 10))
175 | },
176 | undestructuredPropsEmpty: {
177 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10)),
178 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10))
179 | },
180 | undestructuredPropsSpreadAttribute: {
181 | typescript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10)),
182 | javascript: new vscode.Range(new vscode.Position(4, 4), new vscode.Position(6, 10))
183 | },
184 | undestructuredPropsExtended: {
185 | typescript: new vscode.Range(new vscode.Position(24, 4), new vscode.Position(58, 7)),
186 | javascript: new vscode.Range(new vscode.Position(24, 4), new vscode.Position(58, 7))
187 | }
188 | };
189 |
190 | export async function getDocumentsAndRange(key: string) {
191 | const ranges = allRanges[key];
192 |
193 | if (!ranges) {
194 | throw new Error(`Range not found for key: ${key}`);
195 | }
196 |
197 | return {
198 | ranges,
199 | ...(await getDocuments(key))
200 | };
201 | }
202 |
203 |
--------------------------------------------------------------------------------
/src/extractComponent.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { isFileTypescript } from './checks';
3 | import { extractProps } from './parsers/extractProps';
4 | import { determineIfShouldWrapInFragments } from './parsers/fragment';
5 | import { replacePropsWithFullPath } from './parsers/replacePropsWithFullPath';
6 | import { ArgsDerivedFromExternalArgs, BuildArgs, ExternalArgs, ExtractionArgs, PropsAndDerivedData } from './types';
7 | import { getProgramAndSourceFile } from './typescriptProgram';
8 | import { capitalizeComponentName, removeNonWordCharacters } from './utils';
9 |
10 | export async function extractComponent(document: vscode.TextDocument, range: vscode.Range | vscode.Selection) {
11 | const componentName = await getComponentName();
12 | if (!componentName) return;
13 |
14 | const configs = getFileConfigs();
15 |
16 | const args: ExternalArgs = { document, range, componentName, ...configs };
17 |
18 | await buildExtractedComponent(args);
19 | }
20 |
21 | export async function buildExtractedComponent(externalArgs: ExternalArgs) {
22 | const { document, range } = externalArgs;
23 |
24 | const argsDerivedFromExternalArgs = getArgsDerivedFromExternalArgs(externalArgs);
25 |
26 | const extractionArgs = { ...externalArgs, ...argsDerivedFromExternalArgs };
27 |
28 | const shouldWrapInFragments = determineIfShouldWrapInFragments(extractionArgs);
29 | const propsAndDerivedData = getPropsAndPropsDerivedData(extractionArgs);
30 | const typeDeclarationInfo = getTypeDeclarationBodyAndReference({ ...extractionArgs, ...propsAndDerivedData });
31 |
32 | const buildArgs = { ...extractionArgs, ...propsAndDerivedData, ...typeDeclarationInfo, shouldWrapInFragments };
33 |
34 | const editor = await vscode.window.showTextDocument(document);
35 | await editor.edit((editBuilder) => {
36 | const extractedComponent = buildFunctionDeclarationWithType(buildArgs);
37 |
38 | const lastLine = document.lineAt(document.lineCount - 1);
39 | const endOfDocument = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
40 | editBuilder.insert(endOfDocument, '\n' + extractedComponent);
41 |
42 | const componentInstance = buildComponentInstance(buildArgs);
43 | editBuilder.replace(range, componentInstance);
44 | });
45 | }
46 |
47 | async function getComponentName() {
48 | const componentGivenName = await vscode.window.showInputBox({
49 | value: 'Component',
50 | title: 'Give the extracted component a name'
51 | });
52 | // If the user clears the input or cancels the input, it's implied that the user doesn't want to proceed.
53 | if (!componentGivenName) return;
54 |
55 | const componentNameWithoutNonWordChars = removeNonWordCharacters(componentGivenName);
56 | if (!componentNameWithoutNonWordChars) return;
57 |
58 | const componentName = capitalizeComponentName(componentNameWithoutNonWordChars);
59 | return componentName;
60 | }
61 |
62 | function getFileConfigs() {
63 | const config = vscode.workspace.getConfiguration('reactExtract');
64 |
65 | const _functionDeclaration = config.get('functionDeclaration');
66 | const functionDeclaration: 'arrow' | 'function' = _functionDeclaration === 'arrow' ? 'arrow' : 'function';
67 |
68 | const _typeDeclaration = config.get('typeDeclaration');
69 | const typeDeclaration =
70 | typeof _typeDeclaration === 'string' && ['interface', 'type', 'inline'].includes(_typeDeclaration)
71 | ? (_typeDeclaration as 'type' | 'inline' | 'interface')
72 | : 'interface';
73 |
74 | const _declareWithReactFC = config.get('declareWithReactFC');
75 | const declareWithReactFC = _declareWithReactFC === 'true' && functionDeclaration === 'arrow';
76 |
77 | const _destructureProps = config.get('destructureProps');
78 | const destructureProps = _destructureProps !== 'false';
79 |
80 | const _explicitReturnStatement = config.get('explicitReturnStatement');
81 | const explicitReturnStatement = _explicitReturnStatement === 'true' && functionDeclaration === 'arrow';
82 |
83 | return { functionDeclaration, typeDeclaration, declareWithReactFC, explicitReturnStatement, destructureProps };
84 | }
85 |
86 | function getArgsDerivedFromExternalArgs(args: ExternalArgs): ArgsDerivedFromExternalArgs {
87 | const { componentName, document } = args;
88 |
89 | const isTypescript = isFileTypescript(document);
90 | const typeDeclarationName = `${componentName}Props`;
91 | const { program, sourceFile } = getProgramAndSourceFile(document);
92 |
93 | if (!sourceFile) {
94 | vscode.window.showErrorMessage('Could not get source file');
95 | throw new Error('Could not get source file');
96 | }
97 |
98 | return { isTypescript, typeDeclarationName, program, sourceFile };
99 | }
100 |
101 | function getPropsAndPropsDerivedData(args: ExtractionArgs): PropsAndDerivedData {
102 | const { isTypescript, destructureProps } = args;
103 |
104 | const props = extractProps(args).sort((a, b) => {
105 | if (a.isSpread) return 1;
106 | if (b.isSpread) return -1;
107 |
108 | return a.name.localeCompare(b.name);
109 | });
110 |
111 | const shouldDisplayTypeDeclaration = isTypescript && props.length > 0;
112 |
113 | const spreadProps = props.filter((prop) => prop.isSpread);
114 | const hasSingleSpread = spreadProps.length === 1;
115 | const shouldDestructureProps = destructureProps || spreadProps.length > 0;
116 |
117 | return {
118 | props,
119 | shouldDisplayTypeDeclaration,
120 | shouldDestructureProps,
121 | ...(hasSingleSpread
122 | ? { hasSingleSpread, singleSpreadType: spreadProps[0].type }
123 | : { hasSingleSpread, singleSpreadType: undefined })
124 | };
125 | }
126 |
127 | function getTypeDeclarationBodyAndReference(args: ExtractionArgs & PropsAndDerivedData) {
128 | const { typeDeclaration: typeDeclarationType, typeDeclarationName } = args;
129 | const typeDeclaration = buildTypeDeclaration(args);
130 | const typeDeclarationBody = typeDeclarationType === 'inline' ? '' : typeDeclaration;
131 | const typeDeclarationReference = typeDeclarationType === 'inline' ? typeDeclaration : typeDeclarationName;
132 |
133 | return { typeDeclarationBody, typeDeclarationReference };
134 | }
135 |
136 | function buildTypeDeclaration(args: ExtractionArgs & PropsAndDerivedData) {
137 | const {
138 | hasSingleSpread,
139 | props,
140 | singleSpreadType,
141 | typeDeclaration: typeDeclarationType,
142 | typeDeclarationName,
143 | shouldDisplayTypeDeclaration
144 | } = args;
145 |
146 | if (!shouldDisplayTypeDeclaration) return '';
147 |
148 | const propsToDeclare = props.filter(({ isSpread }) => !hasSingleSpread || (hasSingleSpread && !isSpread));
149 | const declaredProps =
150 | propsToDeclare.map(({ name, type }) => `${name}: ${type}`).join(';\n') + (propsToDeclare.length > 0 ? ';' : '');
151 |
152 | switch (typeDeclarationType) {
153 | case 'type':
154 | return `\n
155 | type ${typeDeclarationName} = ${singleSpreadType ? `${singleSpreadType} & ` : ''} {
156 | ${declaredProps}
157 | };\n
158 | `;
159 | case 'inline':
160 | return `${singleSpreadType ? `${singleSpreadType} & ` : ''} { ${declaredProps} }`;
161 | case 'interface':
162 | return `\n
163 | interface ${typeDeclarationName} ${singleSpreadType ? `extends ${singleSpreadType}` : ''} {
164 | ${declaredProps}
165 | }\n
166 | `;
167 | }
168 | }
169 |
170 | function buildFunctionDeclarationWithType(args: BuildArgs) {
171 | const { componentName, functionDeclaration: functionDeclarationType, typeDeclarationBody } = args;
172 |
173 | const functionArguments = buildFunctionArguments(args);
174 | const functionArgumentsType = buildFunctionArgumentsType(args);
175 | const body = buildFunctionBody(args);
176 |
177 | if (functionDeclarationType === 'arrow') {
178 | const explicitType = buildFunctionExplicitType(args);
179 | return `
180 | ${typeDeclarationBody}
181 | const ${componentName}${explicitType} = (${functionArguments}${functionArgumentsType}) => ${body}
182 | `;
183 | } else {
184 | return `
185 | ${typeDeclarationBody}
186 | function ${componentName}(${functionArguments}${functionArgumentsType}) ${body}
187 | `;
188 | }
189 | }
190 |
191 | function buildFunctionArgumentsType(args: BuildArgs) {
192 | const {
193 | functionDeclaration: functionDeclarationType,
194 | declareWithReactFC,
195 | shouldDisplayTypeDeclaration,
196 | typeDeclarationReference
197 | } = args;
198 |
199 | if (!shouldDisplayTypeDeclaration) return '';
200 |
201 | if (functionDeclarationType === 'arrow' && declareWithReactFC) return '';
202 |
203 | return `: ${typeDeclarationReference}`;
204 | }
205 |
206 | function buildFunctionArguments(args: BuildArgs) {
207 | const { props, hasSingleSpread, shouldDestructureProps } = args;
208 |
209 | if (props.length === 0) return '';
210 |
211 | if (!shouldDestructureProps) return 'props';
212 |
213 | const boundProps = props.map(({ name, isSpread }) => (isSpread && hasSingleSpread ? `...${name}` : name)).join(',\n');
214 | return `{ ${boundProps} }`;
215 | }
216 |
217 | function buildFunctionExplicitType(args: BuildArgs) {
218 | const {
219 | functionDeclaration,
220 | declareWithReactFC,
221 | shouldDisplayTypeDeclaration,
222 | typeDeclarationReference,
223 | isTypescript
224 | } = args;
225 |
226 | if (isTypescript && functionDeclaration === 'arrow' && declareWithReactFC) {
227 | return `: React.FC${shouldDisplayTypeDeclaration ? `<${typeDeclarationReference}>` : ''}`;
228 | } else {
229 | return '';
230 | }
231 | }
232 |
233 | function buildFunctionBody(args: BuildArgs) {
234 | const {
235 | document,
236 | explicitReturnStatement,
237 | functionDeclaration: functionDeclarationType,
238 | range,
239 | shouldWrapInFragments,
240 | shouldDestructureProps,
241 | props
242 | } = args;
243 |
244 | const selection =
245 | shouldDestructureProps || props.length === 0 ? document.getText(range) : replacePropsWithFullPath(args);
246 |
247 | const functionReturn = shouldWrapInFragments ? `<>\n${selection}\n>` : selection;
248 |
249 | if (functionDeclarationType === 'arrow' && !explicitReturnStatement) {
250 | return `(
251 | ${functionReturn}
252 | );`;
253 | } else {
254 | return `{
255 | return (
256 | ${functionReturn}
257 | );
258 | }`;
259 | }
260 | }
261 |
262 | function buildComponentInstance(args: BuildArgs) {
263 | const { componentName, props, hasSingleSpread } = args;
264 |
265 | const passedProps = props
266 | .map(({ name, isSpread }) => (isSpread && hasSingleSpread ? `{...${name}}` : `${name}={${name}}`))
267 | .join('\n');
268 |
269 | return `
270 | <${componentName}
271 | ${passedProps}
272 | />
273 | `;
274 | }
275 |
276 |
--------------------------------------------------------------------------------
/src/test/extractComponent.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 | import { buildExtractedComponent } from '../extractComponent';
3 | import { ExtractionArgs } from '../types';
4 | import { getDocumentsAndRange } from './common';
5 |
6 | interface AssertExtractionFlags {
7 | keepSemiColons?: boolean;
8 | }
9 |
10 | function assertExtraction(expected: string, result: string, { keepSemiColons = false }: AssertExtractionFlags = {}) {
11 | const parser = (text: string) => {
12 | let next = text;
13 | next = next.replaceAll(/import[\s\S]*?;/g, '');
14 | next = next.replaceAll(/\s+/g, '');
15 | next = keepSemiColons ? next : next.replaceAll(/;/g, '');
16 | next = next.replaceAll(/"/g, "'");
17 | next = next.replaceAll(/[()]/g, '');
18 |
19 | return next;
20 | };
21 |
22 | return assert.strictEqual(parser(result), parser(expected));
23 | }
24 |
25 | suite('buildExtractedComponent', function () {
26 | const defaultArgs: Pick<
27 | ExtractionArgs,
28 | | 'componentName'
29 | | 'functionDeclaration'
30 | | 'typeDeclaration'
31 | | 'declareWithReactFC'
32 | | 'explicitReturnStatement'
33 | | 'destructureProps'
34 | > = {
35 | componentName: 'Extracted',
36 | functionDeclaration: 'function',
37 | typeDeclaration: 'interface',
38 | explicitReturnStatement: false,
39 | declareWithReactFC: false,
40 | destructureProps: true
41 | };
42 |
43 | suite('extracts a nested component without any props', function () {
44 | test('with typescript', async function () {
45 | const {
46 | ranges: { typescript: range },
47 | tsTest,
48 | tsResult
49 | } = await getDocumentsAndRange('noProps');
50 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
51 | assertExtraction(tsResult.getText(), tsTest.getText());
52 | });
53 |
54 | test('with javascript', async function () {
55 | const {
56 | ranges: { javascript: range },
57 | jsResult,
58 | jsTest
59 | } = await getDocumentsAndRange('noProps');
60 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
61 | assertExtraction(jsResult.getText(), jsTest.getText());
62 | });
63 | });
64 |
65 | suite('extracts a nested component using fragments', function () {
66 | test('with typescript', async function () {
67 | const {
68 | ranges: { typescript: range },
69 | tsResult,
70 | tsTest
71 | } = await getDocumentsAndRange('fragment');
72 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
73 | assertExtraction(tsResult.getText(), tsTest.getText());
74 | });
75 |
76 | test('with javascript', async function () {
77 | const {
78 | ranges: { javascript: range },
79 | jsResult,
80 | jsTest
81 | } = await getDocumentsAndRange('fragment');
82 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
83 | assertExtraction(jsResult.getText(), jsTest.getText());
84 | });
85 | });
86 |
87 | suite('extracts a simple nested component with only static props', function () {
88 | test('with typescript', async function () {
89 | const {
90 | ranges: { typescript: range },
91 | tsResult,
92 | tsTest
93 | } = await getDocumentsAndRange('onlyStatic');
94 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
95 | assertExtraction(tsResult.getText(), tsTest.getText());
96 | });
97 |
98 | test('with javascript', async function () {
99 | const {
100 | ranges: { javascript: range },
101 | jsResult,
102 | jsTest
103 | } = await getDocumentsAndRange('onlyStatic');
104 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
105 | assertExtraction(jsResult.getText(), jsTest.getText());
106 | });
107 | });
108 |
109 | suite('extracts a component with a mix of static and non-static props', function () {
110 | test('with typescript', async function () {
111 | const {
112 | ranges: { typescript: range },
113 | tsResult,
114 | tsTest
115 | } = await getDocumentsAndRange('static');
116 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
117 | assertExtraction(tsResult.getText(), tsTest.getText());
118 | });
119 |
120 | test('with javascript', async function () {
121 | const {
122 | ranges: { javascript: range },
123 | jsResult,
124 | jsTest
125 | } = await getDocumentsAndRange('static');
126 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
127 | assertExtraction(jsResult.getText(), jsTest.getText());
128 | });
129 | });
130 |
131 | suite('extracts a component with implicitly true variables', function () {
132 | test('with typescript', async function () {
133 | const {
134 | ranges: { typescript: range },
135 | tsResult,
136 | tsTest
137 | } = await getDocumentsAndRange('implicit');
138 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
139 | assertExtraction(tsResult.getText(), tsTest.getText());
140 | });
141 |
142 | test('with javascript', async function () {
143 | const {
144 | ranges: { javascript: range },
145 | jsResult,
146 | jsTest
147 | } = await getDocumentsAndRange('implicit');
148 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
149 | assertExtraction(jsResult.getText(), jsTest.getText());
150 | });
151 | });
152 |
153 | suite('extracts a component having a conditional rendering patterns', function () {
154 | test('with typescript', async function () {
155 | const {
156 | ranges: { typescript: range },
157 | tsResult,
158 | tsTest
159 | } = await getDocumentsAndRange('conditional');
160 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
161 | assertExtraction(tsResult.getText(), tsTest.getText());
162 | });
163 |
164 | test('with javascript', async function () {
165 | const {
166 | ranges: { javascript: range },
167 | jsResult,
168 | jsTest
169 | } = await getDocumentsAndRange('conditional');
170 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
171 | assertExtraction(jsResult.getText(), jsTest.getText());
172 | });
173 | });
174 |
175 | suite('extracts a component having a function as props, the function itself return a component', function () {
176 | test('with typescript', async function () {
177 | const {
178 | ranges: { typescript: range },
179 | tsResult,
180 | tsTest
181 | } = await getDocumentsAndRange('componentAsFunction');
182 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
183 | assertExtraction(tsResult.getText(), tsTest.getText());
184 | });
185 |
186 | test('with javascript', async function () {
187 | const {
188 | ranges: { javascript: range },
189 | jsResult,
190 | jsTest
191 | } = await getDocumentsAndRange('componentAsFunction');
192 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
193 | assertExtraction(jsResult.getText(), jsTest.getText());
194 | });
195 | });
196 |
197 | suite('extracts a component passing another component as props', function () {
198 | test('with typescript', async function () {
199 | const {
200 | ranges: { typescript: range },
201 | tsResult,
202 | tsTest
203 | } = await getDocumentsAndRange('componentAsProps');
204 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
205 | assertExtraction(tsResult.getText(), tsTest.getText());
206 | });
207 |
208 | test('with javascript', async function () {
209 | const {
210 | ranges: { javascript: range },
211 | jsResult,
212 | jsTest
213 | } = await getDocumentsAndRange('componentAsProps');
214 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
215 | assertExtraction(jsResult.getText(), jsTest.getText());
216 | });
217 | });
218 |
219 | suite('extracts a component containing a dynamic rendering with map', function () {
220 | test('with typescript', async function () {
221 | const {
222 | ranges: { typescript: range },
223 | tsResult,
224 | tsTest
225 | } = await getDocumentsAndRange('map');
226 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
227 | assertExtraction(tsResult.getText(), tsTest.getText());
228 | });
229 |
230 | test('with javascript', async function () {
231 | const {
232 | ranges: { javascript: range },
233 | jsResult,
234 | jsTest
235 | } = await getDocumentsAndRange('map');
236 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
237 | assertExtraction(jsResult.getText(), jsTest.getText());
238 | });
239 | });
240 |
241 | suite('extracts a component that only a part of the component tree of the place it got extracted from', function () {
242 | test('with typescript', async function () {
243 | const {
244 | ranges: { typescript: range },
245 | tsResult,
246 | tsTest
247 | } = await getDocumentsAndRange('subSelection');
248 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
249 | assertExtraction(tsResult.getText(), tsTest.getText());
250 | });
251 |
252 | test('with javascript', async function () {
253 | const {
254 | ranges: { javascript: range },
255 | jsResult,
256 | jsTest
257 | } = await getDocumentsAndRange('subSelection');
258 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
259 | assertExtraction(jsResult.getText(), jsTest.getText());
260 | });
261 | });
262 |
263 | suite('extracts a component where children is a text', function () {
264 | test('with typescript', async function () {
265 | const {
266 | ranges: { typescript: range },
267 | tsResult,
268 | tsTest
269 | } = await getDocumentsAndRange('textChild');
270 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
271 | assertExtraction(tsResult.getText(), tsTest.getText());
272 | });
273 |
274 | test('with javascript', async function () {
275 | const {
276 | ranges: { javascript: range },
277 | jsResult,
278 | jsTest
279 | } = await getDocumentsAndRange('textChild');
280 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
281 | assertExtraction(jsResult.getText(), jsTest.getText());
282 | });
283 | });
284 |
285 | suite('extracts a component with short-hand properties', function () {
286 | test('with typescript', async function () {
287 | const {
288 | ranges: { typescript: range },
289 | tsResult,
290 | tsTest
291 | } = await getDocumentsAndRange('shortHand');
292 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
293 | assertExtraction(tsResult.getText(), tsTest.getText());
294 | });
295 |
296 | test('with javascript', async function () {
297 | const {
298 | ranges: { javascript: range },
299 | jsResult,
300 | jsTest
301 | } = await getDocumentsAndRange('shortHand');
302 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
303 | assertExtraction(jsResult.getText(), jsTest.getText());
304 | });
305 | });
306 |
307 | suite('extracts a component where the prop type is too big to be fully displayed', function () {
308 | test('with typescript', async function () {
309 | const {
310 | ranges: { typescript: range },
311 | tsResult,
312 | tsTest
313 | } = await getDocumentsAndRange('longType');
314 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
315 | assertExtraction(tsResult.getText(), tsTest.getText());
316 | });
317 | });
318 |
319 | suite('extracts a component with complex pattern of properties and methods passed as props', function () {
320 | test('with typescript', async function () {
321 | const {
322 | ranges: { typescript: range },
323 | tsResult,
324 | tsTest
325 | } = await getDocumentsAndRange('propertiesAndMethods');
326 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
327 | assertExtraction(tsResult.getText(), tsTest.getText());
328 | });
329 |
330 | test('with javascript', async function () {
331 | const {
332 | ranges: { javascript: range },
333 | jsResult,
334 | jsTest
335 | } = await getDocumentsAndRange('propertiesAndMethods');
336 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
337 | assertExtraction(jsResult.getText(), jsTest.getText());
338 | });
339 | });
340 |
341 | suite('extracts a component with prop declaration being a renamed destructured prop', function () {
342 | test('with typescript', async function () {
343 | const {
344 | ranges: { typescript: range },
345 | tsResult,
346 | tsTest
347 | } = await getDocumentsAndRange('destructureRename');
348 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
349 | assertExtraction(tsResult.getText(), tsTest.getText());
350 | });
351 |
352 | test('with javascript', async function () {
353 | const {
354 | ranges: { javascript: range },
355 | jsResult,
356 | jsTest
357 | } = await getDocumentsAndRange('destructureRename');
358 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
359 | assertExtraction(jsResult.getText(), jsTest.getText());
360 | });
361 | });
362 |
363 | suite('extracts a component with props coming from a nested destructure statement', function () {
364 | test('with typescript', async function () {
365 | const {
366 | ranges: { typescript: range },
367 | tsResult,
368 | tsTest
369 | } = await getDocumentsAndRange('destructureNested');
370 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
371 | assertExtraction(tsResult.getText(), tsTest.getText());
372 | });
373 |
374 | test('with javascript', async function () {
375 | const {
376 | ranges: { javascript: range },
377 | jsResult,
378 | jsTest
379 | } = await getDocumentsAndRange('destructureNested');
380 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
381 | assertExtraction(jsResult.getText(), jsTest.getText());
382 | });
383 | });
384 |
385 | suite('extracts a component having props passed from function parameter', function () {
386 | test('with typescript', async function () {
387 | const {
388 | ranges: { typescript: range },
389 | tsResult,
390 | tsTest
391 | } = await getDocumentsAndRange('parameter');
392 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
393 | assertExtraction(tsResult.getText(), tsTest.getText());
394 | });
395 |
396 | test('with javascript', async function () {
397 | const {
398 | ranges: { javascript: range },
399 | jsResult,
400 | jsTest
401 | } = await getDocumentsAndRange('parameter');
402 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
403 | assertExtraction(jsResult.getText(), jsTest.getText());
404 | });
405 | });
406 |
407 | suite('infers parameter type from type reference', function () {
408 | test('with typescript', async function () {
409 | const {
410 | ranges: { typescript: range },
411 | tsResult,
412 | tsTest
413 | } = await getDocumentsAndRange('parameterTypeReference');
414 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
415 | assertExtraction(tsResult.getText(), tsTest.getText());
416 | });
417 | });
418 |
419 | suite('extracts a component using spread syntax', function () {
420 | test('with typescript', async function () {
421 | const {
422 | ranges: { typescript: range },
423 | tsResult,
424 | tsTest
425 | } = await getDocumentsAndRange('spread');
426 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
427 | assertExtraction(tsResult.getText(), tsTest.getText(), { keepSemiColons: true });
428 | });
429 |
430 | test('with javascript', async function () {
431 | const {
432 | ranges: { javascript: range },
433 | jsResult,
434 | jsTest
435 | } = await getDocumentsAndRange('spread');
436 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
437 | assertExtraction(jsResult.getText(), jsTest.getText());
438 | });
439 | });
440 |
441 | suite('extracts a component using array spread syntax', function () {
442 | test('with typescript', async function () {
443 | const {
444 | ranges: { typescript: range },
445 | tsResult,
446 | tsTest
447 | } = await getDocumentsAndRange('spreadArray');
448 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
449 | assertExtraction(tsResult.getText(), tsTest.getText());
450 | });
451 |
452 | test('with javascript', async function () {
453 | const {
454 | ranges: { javascript: range },
455 | jsResult,
456 | jsTest
457 | } = await getDocumentsAndRange('spreadArray');
458 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
459 | assertExtraction(jsResult.getText(), jsTest.getText());
460 | });
461 | });
462 |
463 | suite('extracts a component using having nested spread syntax', function () {
464 | test('with typescript', async function () {
465 | const {
466 | ranges: { typescript: range },
467 | tsResult,
468 | tsTest
469 | } = await getDocumentsAndRange('spreadNested');
470 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
471 | assertExtraction(tsResult.getText(), tsTest.getText());
472 | });
473 |
474 | test('with javascript', async function () {
475 | const {
476 | ranges: { javascript: range },
477 | jsResult,
478 | jsTest
479 | } = await getDocumentsAndRange('spreadNested');
480 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
481 | assertExtraction(jsResult.getText(), jsTest.getText());
482 | });
483 | });
484 |
485 | suite('infers types from type reference of a nested spread syntax', function () {
486 | test('with typescript', async function () {
487 | const {
488 | ranges: { typescript: range },
489 | tsResult,
490 | tsTest
491 | } = await getDocumentsAndRange('spreadNestedTypeReference');
492 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
493 | assertExtraction(tsResult.getText(), tsTest.getText());
494 | });
495 | });
496 |
497 | suite('infers default types from destructured and spread props from object binding typed as any', function () {
498 | test('with typescript', async function () {
499 | const {
500 | ranges: { typescript: range },
501 | tsResult,
502 | tsTest
503 | } = await getDocumentsAndRange('spreadAny');
504 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
505 | assertExtraction(tsResult.getText(), tsTest.getText());
506 | });
507 | });
508 |
509 | suite('builds type as inline declaration if so configured', function () {
510 | test('with typescript', async function () {
511 | const {
512 | ranges: { typescript: range },
513 | tsResult,
514 | tsTest
515 | } = await getDocumentsAndRange('typeInlineDeclaration');
516 | await buildExtractedComponent({ ...defaultArgs, typeDeclaration: 'inline', document: tsTest, range });
517 | assertExtraction(tsResult.getText(), tsTest.getText());
518 | });
519 | });
520 |
521 | suite('builds no type as inline declaration if there are no props', function () {
522 | test('with typescript', async function () {
523 | const {
524 | ranges: { typescript: range },
525 | tsResult,
526 | tsTest
527 | } = await getDocumentsAndRange('typeInlineDeclarationEmpty');
528 | await buildExtractedComponent({ ...defaultArgs, typeDeclaration: 'inline', document: tsTest, range });
529 | assertExtraction(tsResult.getText(), tsTest.getText());
530 | });
531 | });
532 |
533 | suite('builds type as inline declaration if so configured, with type extension', function () {
534 | test('with typescript', async function () {
535 | const {
536 | ranges: { typescript: range },
537 | tsResult,
538 | tsTest
539 | } = await getDocumentsAndRange('typeInlineDeclarationExtended');
540 | await buildExtractedComponent({ ...defaultArgs, typeDeclaration: 'inline', document: tsTest, range });
541 | assertExtraction(tsResult.getText(), tsTest.getText());
542 | });
543 | });
544 |
545 | suite('builds type as inline declaration if so configured, with ReactFC type declaration', function () {
546 | test('with typescript', async function () {
547 | const {
548 | ranges: { typescript: range },
549 | tsResult,
550 | tsTest
551 | } = await getDocumentsAndRange('typeInlineDeclarationReactFC');
552 | await buildExtractedComponent({
553 | ...defaultArgs,
554 | functionDeclaration: 'arrow',
555 | declareWithReactFC: true,
556 | typeDeclaration: 'inline',
557 | document: tsTest,
558 | range
559 | });
560 | assertExtraction(tsResult.getText(), tsTest.getText());
561 | });
562 | });
563 |
564 | suite('builds type as type declaration if so configured', function () {
565 | test('with typescript', async function () {
566 | const {
567 | ranges: { typescript: range },
568 | tsResult,
569 | tsTest
570 | } = await getDocumentsAndRange('typeTypeDeclaration');
571 | await buildExtractedComponent({ ...defaultArgs, typeDeclaration: 'type', document: tsTest, range });
572 | assertExtraction(tsResult.getText(), tsTest.getText());
573 | });
574 | });
575 |
576 | suite('builds no type as type declaration if there are no props', function () {
577 | test('with typescript', async function () {
578 | const {
579 | ranges: { typescript: range },
580 | tsResult,
581 | tsTest
582 | } = await getDocumentsAndRange('typeTypeDeclarationEmpty');
583 | await buildExtractedComponent({ ...defaultArgs, typeDeclaration: 'type', document: tsTest, range });
584 | assertExtraction(tsResult.getText(), tsTest.getText());
585 | });
586 | });
587 |
588 | suite('builds type as type declaration if so configured, with type extension', function () {
589 | test('with typescript', async function () {
590 | const {
591 | ranges: { typescript: range },
592 | tsResult,
593 | tsTest
594 | } = await getDocumentsAndRange('typeTypeDeclarationExtended');
595 | await buildExtractedComponent({ ...defaultArgs, typeDeclaration: 'type', document: tsTest, range });
596 | assertExtraction(tsResult.getText(), tsTest.getText());
597 | });
598 | });
599 |
600 | suite('builds the component as an arrow function if so configured, if there are no props', function () {
601 | test('with typescript', async function () {
602 | const {
603 | ranges: { typescript: range },
604 | tsResult,
605 | tsTest
606 | } = await getDocumentsAndRange('arrowFunctionDeclarationEmpty');
607 | await buildExtractedComponent({ ...defaultArgs, functionDeclaration: 'arrow', document: tsTest, range });
608 | assertExtraction(tsResult.getText(), tsTest.getText());
609 | });
610 |
611 | test('with javascript', async function () {
612 | const {
613 | ranges: { javascript: range },
614 | jsResult,
615 | jsTest
616 | } = await getDocumentsAndRange('arrowFunctionDeclarationEmpty');
617 | await buildExtractedComponent({ ...defaultArgs, functionDeclaration: 'arrow', document: jsTest, range });
618 | assertExtraction(jsResult.getText(), jsTest.getText());
619 | });
620 | });
621 |
622 | suite(
623 | 'builds the component as an arrow function if so configured, if there are props including spread props',
624 | function () {
625 | test('with typescript', async function () {
626 | const {
627 | ranges: { typescript: range },
628 | tsResult,
629 | tsTest
630 | } = await getDocumentsAndRange('arrowFunctionDeclarationSpread');
631 | await buildExtractedComponent({ ...defaultArgs, functionDeclaration: 'arrow', document: tsTest, range });
632 | assertExtraction(tsResult.getText(), tsTest.getText());
633 | });
634 |
635 | test('with javascript', async function () {
636 | const {
637 | ranges: { javascript: range },
638 | jsResult,
639 | jsTest
640 | } = await getDocumentsAndRange('arrowFunctionDeclarationSpread');
641 | await buildExtractedComponent({ ...defaultArgs, functionDeclaration: 'arrow', document: jsTest, range });
642 | assertExtraction(jsResult.getText(), jsTest.getText());
643 | });
644 | }
645 | );
646 |
647 | suite('builds the component as an arrow function with explicit return statement, if so configured', function () {
648 | test('with javascript', async function () {
649 | const {
650 | ranges: { javascript: range },
651 | jsResult,
652 | jsTest
653 | } = await getDocumentsAndRange('arrowFunctionExplicitReturn');
654 | await buildExtractedComponent({
655 | ...defaultArgs,
656 | functionDeclaration: 'arrow',
657 | explicitReturnStatement: true,
658 | document: jsTest,
659 | range
660 | });
661 | assertExtraction(jsResult.getText(), jsTest.getText());
662 | });
663 | });
664 |
665 | suite(
666 | 'builds the component as an arrow function, declaring using React.FC with props type, if so configured, ensuring that nothing is changed for javascript.',
667 | function () {
668 | test('with typescript', async function () {
669 | const {
670 | ranges: { typescript: range },
671 | tsResult,
672 | tsTest
673 | } = await getDocumentsAndRange('reactFCType');
674 | await buildExtractedComponent({
675 | ...defaultArgs,
676 | functionDeclaration: 'arrow',
677 | declareWithReactFC: true,
678 | document: tsTest,
679 | range
680 | });
681 | assertExtraction(tsTest.getText(), tsResult.getText());
682 | });
683 |
684 | test('with javascript', async function () {
685 | const {
686 | ranges: { javascript: range },
687 | jsResult,
688 | jsTest
689 | } = await getDocumentsAndRange('reactFCType');
690 | await buildExtractedComponent({
691 | ...defaultArgs,
692 | functionDeclaration: 'arrow',
693 | declareWithReactFC: true,
694 | document: jsTest,
695 | range
696 | });
697 | assertExtraction(jsResult.getText(), jsTest.getText());
698 | });
699 | }
700 | );
701 |
702 | suite(
703 | 'builds the component as an arrow function, declaring using React.FC without props type, if so configured',
704 | function () {
705 | test('with typescript', async function () {
706 | const {
707 | ranges: { typescript: range },
708 | tsResult,
709 | tsTest
710 | } = await getDocumentsAndRange('reactFCTypeEmpty');
711 | await buildExtractedComponent({
712 | ...defaultArgs,
713 | functionDeclaration: 'arrow',
714 | declareWithReactFC: true,
715 | document: tsTest,
716 | range
717 | });
718 | assertExtraction(tsResult.getText(), tsTest.getText());
719 | });
720 | }
721 | );
722 |
723 | suite('extracts a component wrapping it in fragments if necessary', function () {
724 | test('with typescript', async function () {
725 | const {
726 | ranges: { typescript: range },
727 | tsResult,
728 | tsTest
729 | } = await getDocumentsAndRange('wrapInFragment');
730 | await buildExtractedComponent({ ...defaultArgs, document: tsTest, range });
731 | assertExtraction(tsResult.getText(), tsTest.getText());
732 | });
733 |
734 | test('with javascript', async function () {
735 | const {
736 | ranges: { javascript: range },
737 | jsResult,
738 | jsTest
739 | } = await getDocumentsAndRange('wrapInFragment');
740 | await buildExtractedComponent({ ...defaultArgs, document: jsTest, range });
741 | assertExtraction(jsResult.getText(), jsTest.getText());
742 | });
743 | });
744 |
745 | suite('extracts a component with undestructured props, if so configured', function () {
746 | test('with typescript', async function () {
747 | const {
748 | ranges: { typescript: range },
749 | tsResult,
750 | tsTest
751 | } = await getDocumentsAndRange('undestructuredProps');
752 | await buildExtractedComponent({ ...defaultArgs, destructureProps: false, document: tsTest, range });
753 | assertExtraction(tsResult.getText(), tsTest.getText());
754 | });
755 |
756 | test('with javascript', async function () {
757 | const {
758 | ranges: { javascript: range },
759 | jsResult,
760 | jsTest
761 | } = await getDocumentsAndRange('undestructuredProps');
762 | await buildExtractedComponent({ ...defaultArgs, destructureProps: false, document: jsTest, range });
763 | assertExtraction(jsResult.getText(), jsTest.getText());
764 | });
765 | });
766 |
767 | suite(
768 | 'extracts a component with no props, if it does not have any, despite being configured as undestructured props',
769 | function () {
770 | test('with typescript', async function () {
771 | const {
772 | ranges: { typescript: range },
773 | tsResult,
774 | tsTest
775 | } = await getDocumentsAndRange('undestructuredPropsEmpty');
776 | await buildExtractedComponent({ ...defaultArgs, destructureProps: false, document: tsTest, range });
777 | assertExtraction(tsResult.getText(), tsTest.getText());
778 | });
779 |
780 | test('with javascript', async function () {
781 | const {
782 | ranges: { javascript: range },
783 | jsResult,
784 | jsTest
785 | } = await getDocumentsAndRange('undestructuredPropsEmpty');
786 | await buildExtractedComponent({ ...defaultArgs, destructureProps: false, document: jsTest, range });
787 | assertExtraction(jsResult.getText(), jsTest.getText());
788 | });
789 | }
790 | );
791 |
792 | suite(
793 | 'extracts a component with destructured props, even if it is configured as undestructured props, if there are any spread attribute',
794 | function () {
795 | test('with typescript', async function () {
796 | const {
797 | ranges: { typescript: range },
798 | tsResult,
799 | tsTest
800 | } = await getDocumentsAndRange('undestructuredPropsSpreadAttribute');
801 | await buildExtractedComponent({ ...defaultArgs, destructureProps: false, document: tsTest, range });
802 | assertExtraction(tsResult.getText(), tsTest.getText());
803 | });
804 |
805 | test('with javascript', async function () {
806 | const {
807 | ranges: { javascript: range },
808 | jsResult,
809 | jsTest
810 | } = await getDocumentsAndRange('undestructuredPropsSpreadAttribute');
811 | await buildExtractedComponent({ ...defaultArgs, destructureProps: false, document: jsTest, range });
812 | assertExtraction(jsResult.getText(), jsTest.getText());
813 | });
814 | }
815 | );
816 |
817 | suite('extracts a component with undestructured props, if so configured, in complex scenarios', function () {
818 | test('with javascript', async function () {
819 | const {
820 | ranges: { javascript: range },
821 | jsResult,
822 | jsTest
823 | } = await getDocumentsAndRange('undestructuredPropsExtended');
824 | await buildExtractedComponent({ ...defaultArgs, destructureProps: false, document: jsTest, range });
825 | assertExtraction(jsResult.getText(), jsTest.getText());
826 | });
827 | });
828 | });
829 |
830 |
--------------------------------------------------------------------------------