Created
November 9, 2020 22:42
-
-
Save alexaivars/b04caa558f57cfed606b162096cfbe2e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = function (file, api, options) { | |
if (file.source.indexOf("export default") < 0) { | |
// no need to process the file if no `export default` | |
return; | |
} | |
const j = api.jscodeshift; | |
const root = j(file.source); | |
const filePath = file.path; // replace `filePath` if you want to test different names | |
const EMPTY_LINE = "$$__EMPTY_LINE_PLACEHOLDER__$$"; | |
let hasConflict = false; | |
let shouldAddDefaultExportAtTheEnd = false; | |
let baseName; | |
let exportName; | |
function getAndUpdateExportName(nodePath) { | |
// since we can only have a single `export default` it's safe | |
// to assume this function will only be called once | |
const fileName = getNameFromFilePath(); | |
const declaration = nodePath.get("declaration"); | |
// capitalize the name if it's a React component | |
baseName = isReactComponent(declaration) || isClass(declaration) ? upperFirstLetter(fileName) : fileName; | |
exportName = getFirstNameThatDoesntConflict(baseName); | |
return exportName; | |
} | |
function getFixMeComment() { | |
return j.line(` FIXME: identifier "${baseName}" has already been declared or isn't helpful, rename this please!`); | |
} | |
function isReactComponent(nodePath) { | |
// return true if any JSXElement inside that node, could be made smarter by | |
// checking if it's a high-order component (HOC) or not, but decided to keep it simple | |
return j(nodePath).find(j.JSXElement).length > 0; | |
} | |
function isClass(nodePath) { | |
return nodePath.value.type === "ClassDeclaration"; | |
} | |
function upperFirstLetter(str) { | |
// converts first char to uppercase | |
return str.replace(/^[a-z]/, (s) => s.toUpperCase()); | |
} | |
function camelCaseIfNeeded(fileName) { | |
// convert to camelCame if split by "-" or "_" or " " | |
// example "is-some-helper" will become "isSomeHelper" | |
if (/[-_\s]/.test(fileName)) { | |
return fileName | |
.replace(/[\-_]/g, " ") // convert all hyphens and underscores to spaces | |
.replace(/\s[a-z]/g, (s) => s.toUpperCase()) // convert first char of each word to UPPERCASE | |
.replace(/\s+/g, "") //remove spaces | |
.replace(/^[A-Z]/g, (s) => s.toLowerCase()); // convert first char to lowercase | |
} | |
return fileName; | |
} | |
function getNameFromFilePath() { | |
// split folders (eg. "foo/bar/baz.js" into ["foo", "bar", "baz.js"]) | |
const paths = filePath.split(/[\/\\]/); | |
// remove file extension (eg. "Bar.test.js" will become just "Bar") | |
const fileName = paths[paths.length - 1].replace(/\..+$/, ""); | |
// handle cases like "Foo/index.js" | |
if (fileName === "index") { | |
const folder = paths[paths.length - 2]; | |
if (!folder) { | |
// if can't derive name from folder, flag it as a conflict | |
hasConflict = true; | |
} | |
return folder ? camelCaseIfNeeded(folder) : fileName; | |
} | |
return camelCaseIfNeeded(fileName); | |
} | |
function getFirstNameThatDoesntConflict(baseName) { | |
let name = baseName; | |
let i = 0; | |
while (hasNameConflicts(name)) { | |
i += 1; | |
name = baseName + i; | |
hasConflict = true; | |
} | |
return name; | |
} | |
function hasNameConflicts(name) { | |
// this will get any identifier inside any scope, assuming that some people have | |
// the eslint rule https://eslint.org/docs/rules/no-shadow enabled | |
return root.find(j.Identifier, { name }).length !== 0; | |
} | |
function variableDeclarationReplacer(path) { | |
const dec = j.variableDeclaration("const", [ | |
j.variableDeclarator(j.identifier(getAndUpdateExportName(path)), path.value.declaration) | |
]); | |
// make sure we keep the comments | |
dec.comments = path.value.comments; | |
shouldAddDefaultExportAtTheEnd = true; | |
return dec; | |
} | |
function filterVariableDeclarations(node) { | |
return ["ArrowFunctionExpression", "ArrayExpression", "ObjectExpression", "Literal", "NumericLiteral", "StringLiteral"].includes( | |
node.declaration.type | |
); | |
} | |
// type: ArrowFunctionExpression | |
// convert: `export default () => ();` | |
// into: `const foo = () => ();` | |
// | |
// type: ArrayExpression | |
// convert: `export default [1, 2, 3]` | |
// into: `const foo = [1, 2, 3];` | |
// | |
// Type: ObjectExpression | |
// convert: `export default { n: 123 }` | |
// into: `const foo = { n: 123 };` | |
// | |
// type: Literal, NumericLiteral, StringLiteral | |
// convert: `export default 123;` | |
// into: `const foo = 123;` | |
root.find(j.ExportDefaultDeclaration, filterVariableDeclarations).replaceWith(variableDeclarationReplacer); | |
function addIdToDeclaration(path) { | |
const dec = path.value.declaration; | |
dec.id = j.identifier(getAndUpdateExportName(path)); | |
const newExport = j.exportDefaultDeclaration(dec); | |
// make sure we keep the comments | |
newExport.comments = path.value.comments; | |
if (hasConflict) { | |
newExport.comments.push(getFixMeComment()); | |
} | |
return newExport; | |
} | |
// convert: `export default function(){}` | |
// into: `export default function foo(){}` | |
root.find(j.ExportDefaultDeclaration, { declaration: { type: "FunctionDeclaration", id: null } }).replaceWith(addIdToDeclaration); | |
// convert: `export default class {}` | |
// into: `export default class Foo {}` | |
root.find(j.ExportDefaultDeclaration, { declaration: { type: "ClassDeclaration", id: null } }).replaceWith(addIdToDeclaration); | |
if (shouldAddDefaultExportAtTheEnd) { | |
const programBody = root.find(j.Program).get("body"); | |
const defaultExport = j.exportDefaultDeclaration(j.identifier(exportName)); | |
if (hasConflict) { | |
defaultExport.comments = [getFixMeComment()]; | |
} else { | |
// add empty line placeholder before `export default` (no easy way to add empty line on jscodeshift) | |
programBody.push(j.expressionStatement(j.identifier(EMPTY_LINE))); | |
} | |
// add the `export default foo;` at the end of the Program | |
programBody.push(defaultExport); | |
} | |
return root | |
.toSource() // convert back to string | |
.replace(`${EMPTY_LINE};`, ""); // remove empty line placeholder | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment