react-native-component-splitter
Version:
Split React Naitve components into sub-components
303 lines (256 loc) • 8.45 kB
text/typescript
import * as _ from "lodash";
import { window, Range, Position } from "vscode";
import * as fs from "fs-extra";
import * as path from "path";
import parseUtils from "./parse";
import { regexNormalizeResult } from "./common";
const validateSelection = () => {
const editor = window.activeTextEditor;
const selection = editor.document.getText(editor.selection);
try {
parseUtils.transformCode(`<View>${selection}</View>`);
} catch (error) {
console.log(error, "error");
throw new Error(
"Invalid selection. Make sure your selection represents a valid React component"
);
}
const codeWithoutSelection = replaceCodeByRange(
editor.document.getText(),
editor.selection,
"<View></View>"
);
try {
parseUtils.transformCode(codeWithoutSelection);
} catch (error) {
console.log(error, "error");
throw new Error(
"Invalid selection. Make sure the code remains valid without your selection"
);
}
};
const buildComponentPath = (name) => {
const activeDocumentPath = window.activeTextEditor.document.uri.fsPath;
const activeDocumentExtension = activeDocumentPath.replace(
/(.*)+\.[^\.]+/,
"$1"
);
const nameWithoutExtension = name.replace(/\.[^\.]+$/, "");
return path.join(
activeDocumentPath,
"..",
`${nameWithoutExtension}.${activeDocumentExtension}`
);
};
const askForComponentName = async () => {
let name = await window.showInputBox({
prompt: "Choose a name for the new component",
ignoreFocusOut: true,
placeHolder: "New component name...",
});
if (_.isNil(name)) {
throw new Error("Empty name received");
}
if (!/^[A-Z][0-9a-zA-Z_$]*$/g.test(name)) {
name = name.charAt(0).toUpperCase() + name.slice(1);
}
if (fs.pathExistsSync(buildComponentPath(name))) {
throw new Error(
"File with this component name already exists in the current folder"
);
}
return name;
};
const replaceCodeByRange = (code: any, range: any, replaceValue: any) => {
const lines = _.split(code, "\n");
const { startIndex, endIndex } = _.reduce(
lines,
(res, line, index) => {
if (index < range.start.line) {
res.startIndex = res.startIndex + _.size(line) + 1;
}
if (index === range.start.line) {
res.startIndex = res.startIndex + range.start.character;
}
if (index < range.end.line) {
res.endIndex = res.endIndex + _.size(line) + 1;
}
if (index === range.end.line) {
res.endIndex = res.endIndex + range.end.character;
}
return res;
},
{ startIndex: 0, endIndex: 0 }
);
return `${code.substring(0, startIndex)}${replaceValue}${code.substring(
endIndex
)}`;
};
const getFullDocumentRange = (document) =>
new Range(
document.positionAt(0),
document.positionAt(_.size(document.getText()))
);
const getFirstAndLastImportLineIndexes = (codeLines) => {
const isImportLine = (line) => /^\s*import.*from.*/.test(line);
const firstImportLineIndex = _.findIndex(codeLines, isImportLine);
const lastImportLineIndex = _.findLastIndex(codeLines, isImportLine);
return { firstImportLineIndex, lastImportLineIndex };
};
const replaceSelection = async ({ reactElement, name }) => {
const editor = window.activeTextEditor;
const { document } = editor;
const codeLines = _.split(document.getText(), "\n");
const { lastImportLineIndex } = getFirstAndLastImportLineIndexes(codeLines);
await editor.edit((edit) => {
edit.replace(editor.selection, reactElement);
edit.insert(
new Position(lastImportLineIndex + 1, 0),
`import ${name} from './${name}';\n`
);
});
parseUtils
.eslintAutofix(document.getText(), { filePath: document.uri.fsPath })
.then((output: any) => {
if (!output) {
return;
}
editor.edit((edit) =>
edit.replace(getFullDocumentRange(document), output)
);
});
};
const removeUnusedImports = async () => {
const editor = window.activeTextEditor;
const { document } = editor;
const code = document.getText();
const codeLines = _.split(code, "\n");
const { firstImportLineIndex, lastImportLineIndex } =
getFirstAndLastImportLineIndexes(codeLines);
const importsString = _.chain(codeLines)
.slice(firstImportLineIndex, lastImportLineIndex + 1)
.join("\n")
.value();
const usedImportsString = _.join(parseUtils.getUsedImports(code), "\n");
const codeWithUsedImports = _.replace(code, importsString, usedImportsString);
await editor.edit((edit) =>
edit.replace(getFullDocumentRange(document), codeWithUsedImports)
);
parseUtils
.eslintAutofix(document.getText(), { filePath: document.uri.fsPath })
.then((output: any) => {
if (!output) {
return;
}
editor.edit((edit) =>
edit.replace(getFullDocumentRange(document), output)
);
});
};
const updateOriginalComponent = async ({ newComponent }) => {
await replaceSelection(newComponent);
await removeUnusedImports();
};
const generateReactElement = ({ name, props, jsx }) => {
const numberOfProps = _.size(props);
const numberOfLeadingSpacesFromStart =
parseUtils.getNumberOfLeadingSpaces(jsx);
const leadingSpacesFromStart = _.repeat(" ", numberOfLeadingSpacesFromStart);
let propsString = "";
if (numberOfProps > 3) {
const numberOfLeadingSpacesFromEnd = parseUtils.getNumberOfLeadingSpaces(
jsx,
{ endToStart: true }
);
const leadingSpacesFromEnd = _.repeat(" ", numberOfLeadingSpacesFromEnd);
propsString = `\n${leadingSpacesFromEnd} {...{\n${leadingSpacesFromEnd} ${_.join(
props,
`,\n${leadingSpacesFromEnd} `
)},\n ${leadingSpacesFromEnd}}}\n${leadingSpacesFromEnd}`;
} else if (numberOfProps > 0) {
propsString = ` {...{ ${_.join(props, ", ")} }}`;
}
return `${leadingSpacesFromStart}<${name}${propsString}/>`;
};
const extractRelevantImportsAndPropsAndStylesheet = () => {
const editor = window.activeTextEditor;
const code = editor.document.getText();
const selection = editor.document.getText(editor.selection);
const selectionAndImports = `
${buildImportsString(parseUtils.getImports(code))}\n
export default () => { return (<View>${selection}</View>)};
`;
const imports = parseUtils.getUsedImports(selectionAndImports);
let { stylesheetName, stylesheet } = parseUtils.getStylesheet(code, selection);
stylesheet = regexNormalizeResult(
stylesheet
);
const props = parseUtils.getUndefinedVars(selectionAndImports, stylesheetName);
return {
props,
imports,
stylesheet,
};
};
const buildImportsString = (imports) => _.join(imports, "\n");
const buildPropsString = (props) => {
const numberOfProps = _.size(props);
if (numberOfProps > 2) {
return `{\n ${_.join(props, `,\n `)},\n}`;
}
if (numberOfProps === 2) {
return `{${_.join(props, ", ")}}`;
}
if (numberOfProps === 1) {
return `{${props[0]}}`;
}
return "";
};
const shouldWrapCodeWithEmptyTag = (code) => {
try {
parseUtils.transformCode(code);
} catch {
return true;
}
};
const createNewComponent = async (componentName: any) => {
const editor = window.activeTextEditor;
const uri = editor.document.uri;
const fileExtension = parseUtils.getUriExtension(uri.fsPath);
const selection = editor.document.getText(editor.selection);
const { imports, props, stylesheet } =
extractRelevantImportsAndPropsAndStylesheet();
const newComponent = {
code: parseUtils.pretify(
`${buildImportsString(imports)}\n\n` +
`const ${componentName} = (${buildPropsString(props)}) => {\nreturn (\n` +
`${shouldWrapCodeWithEmptyTag(selection)
? `<View>\n${selection}\n</View>`
: selection
}\n` +
`)\n\n}\n\n` +
`export default ${componentName};\n\n${stylesheet}\n`
),
reactElement: generateReactElement({
name: componentName,
props,
jsx: selection,
}),
imports,
name: componentName,
path: path.join(uri.fsPath, "..", `${componentName}.${fileExtension}`),
props,
};
parseUtils
.eslintAutofix(newComponent.code, { filePath: newComponent.path })
.then((output) => {
fs.writeFileSync(newComponent.path, output || newComponent.code);
});
return newComponent;
};
export {
createNewComponent,
askForComponentName,
validateSelection,
updateOriginalComponent,
};