Created
December 9, 2016 19:22
-
-
Save tibdex/f491a2d264ba14af5643de300957b4f9 to your computer and use it in GitHub Desktop.
jscodeshift codemod to change imports of internal modules using absolute path to relative path
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
/** | |
* This codemod expects 2 CLI arguments: | |
* - packageDir: the path of the directory containing the package.json. It's used to detect whether a path points | |
* to a dependency or an internal module. | |
* - rootDirs: root directory paths separated by a comma if you have more than one root. | |
* Let's say that you have two files: | |
* - src/component.js | |
* - src/index.js containing the import "import Component from 'component.js';" | |
* To have the transformation successfully working you need to call jscodeshift with the "--rootDirs ./src" argument. | |
*/ | |
const {existsSync} = require('fs'); | |
const {_builtinLibs} = require('repl'); // List of Node.js built in modules. | |
const path = require('path'); | |
const makeIsDependency = packageDir => { | |
const {dependencies, devDependencies, peerDependencies} = require(path.resolve(packageDir, 'package.json')); | |
const allDependencies = [..._builtinLibs, ...Object.keys(dependencies), ...Object.keys(devDependencies), ...Object.keys(peerDependencies)]; | |
return importedModulePath => allDependencies.some(name => (importedModulePath === name || importedModulePath.startsWith(`${name}/`))); | |
}; | |
const isRelative = importedModulePath => importedModulePath.startsWith('./') || importedModulePath.startsWith('../'); | |
const makeGetDiskPathFromImportPath = rootDirs => { | |
const importPathToDiskPath = {}; | |
return importedModulePath => { | |
let diskPath = importPathToDiskPath[importedModulePath]; | |
if (diskPath) { | |
return diskPath; | |
} | |
const importedModuleRoot = rootDirs.find(root => existsSync(path.join(root, importedModulePath))); | |
if (importedModuleRoot) { | |
diskPath = path.join(importedModuleRoot, importedModulePath); | |
importPathToDiskPath[importedModulePath] = diskPath; | |
return diskPath; | |
} | |
throw new Error(`Cannot find root for imported module ${importedModulePath}`); | |
}; | |
}; | |
const makeChangePathToRelativeIfNeeded = (currentModuleDirectoryPath, isDependency, rootDirs) => { | |
const getDiskPathFromImportPath = makeGetDiskPathFromImportPath(rootDirs); | |
return importedModulePath => { | |
if (isRelative(importedModulePath) || isDependency(importedModulePath)) { | |
return importedModulePath; | |
} | |
const diskPath = getDiskPathFromImportPath(importedModulePath); | |
const relativePath = path.relative(currentModuleDirectoryPath, diskPath).replace(/\\/g, '/'); | |
return relativePath.startsWith('../') ? relativePath : `./${relativePath}`; | |
}; | |
}; | |
const sortImportsAlphabetically = imports => { | |
imports.sort((a, b) => a.path.localeCompare(b.path)); | |
}; | |
const sortImportDeclarations = (importDeclarations, changePathToRelativeIfNeeded) => { | |
let lastEndOfImportLine = undefined; | |
const importGroups = []; | |
importDeclarations.forEach(path => { | |
const node = path.value; | |
const isFirstImport = lastEndOfImportLine === undefined; | |
const isNewImportBlock = !isFirstImport && lastEndOfImportLine < (node.loc.start.line - 1); | |
if (isFirstImport || isNewImportBlock) { | |
importGroups.push([]); | |
} | |
lastEndOfImportLine = node.loc.end.line; | |
const currentImportPath = node.source.value; | |
const newImportPath = changePathToRelativeIfNeeded(currentImportPath); | |
importGroups[importGroups.length - 1].push({ | |
path: newImportPath, | |
specifiers: node.specifiers | |
}); | |
}); | |
importGroups.forEach(sortImportsAlphabetically); | |
const flattenedImports = [].concat.apply([], importGroups); | |
return flattenedImports; | |
}; | |
const replaceBySortedImportDeclarations = (j, importDeclarations, sortedImportDeclarations) => { | |
return importDeclarations | |
.forEach((path, index) => { | |
const newImport = sortedImportDeclarations[index]; | |
j(path).replaceWith( | |
j.importDeclaration(newImport.specifiers, j.literal(newImport.path)) | |
); | |
}); | |
}; | |
module.exports = (fileInfo, api, options) => { | |
const currentModuleDirectoryPath = path.dirname(path.resolve(fileInfo.path)); | |
const isDependency = makeIsDependency(options.packageDir); | |
const rootDirs = options.rootDirs.split(',').map(rootPath => path.resolve(rootPath)); | |
const changePathToRelativeIfNeeded = makeChangePathToRelativeIfNeeded(currentModuleDirectoryPath, isDependency, rootDirs); | |
const j = api.jscodeshift; | |
const root = j(fileInfo.source); | |
const {comments} = root.find(j.Program).get('body', 0).node; | |
const importDeclarations = root.find(j.ImportDeclaration); | |
const sortedImportDeclarations = sortImportDeclarations(importDeclarations, changePathToRelativeIfNeeded); | |
replaceBySortedImportDeclarations(j, importDeclarations, sortedImportDeclarations); | |
root.get().node.comments = comments; | |
return root.toSource({ | |
objectCurlySpacing: false, | |
quote: 'single' | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment