Skip to content

Instantly share code, notes, and snippets.

@alexaivars
Created November 9, 2020 22:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexaivars/b04caa558f57cfed606b162096cfbe2e to your computer and use it in GitHub Desktop.
Save alexaivars/b04caa558f57cfed606b162096cfbe2e to your computer and use it in GitHub Desktop.
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