UNPKG

babel-plugin-transform-barrels

Version:

A Babel plugin that transforms indirect imports through a barrel file (index.js) into direct imports.

502 lines (462 loc) 20.9 kB
const ospath = require("path"); const t = require("@babel/types"); const AST = require("./ast"); const PathFunctions = require("./path"); const resolver = require("./resolver"); const { Package, packageManager } = require("./packages"); const Cache = require("./cache"); const pluginOptions = require("./pluginOptions"); class DefaultPatternExport { constructor() { this.esmPath = ""; this.type = ""; this.localName = ""; this.exportedName = ""; this.exportedNamesList = []; this.isDefaultPatternCreated = false; this.numOfDefaultPatternUsed = 0; this.firstSpecifier = undefined; } getSpecifierPattern(specifierObj) { const esmPath = this.getEsmPathPattern(specifierObj.esmPath, specifierObj.exportedName); const type = specifierObj.type; const localName = type !=="namespace" && specifierObj.localName.replaceAll(specifierObj.exportedName, "${specifier}"); const exportedName = specifierObj.exportedName.replaceAll(specifierObj.exportedName, "${specifier}"); return { esmPath, type, localName, exportedName }; } createDefaultPattern(specifierObj) { const specifierPattern = this.getSpecifierPattern(specifierObj); if (!specifierPattern.esmPath.includes("${specifier}")) return; this.firstSpecifier = specifierObj; this.esmPath = specifierPattern.esmPath; this.type = specifierPattern.type; this.localName = specifierPattern.localName; this.exportedName = specifierPattern.exportedName; } getEsmPathPattern(esmPath, specifierName) { const regexPattern = `\\b${specifierName}\\b`; const regex = new RegExp(regexPattern, "g"); const pathPattern = esmPath.replace(regex, "${specifier}"); return pathPattern; } isMatchDefaultPattern(specifierObj) { if (!this.isDefaultPatternCreated) { this.isDefaultPatternCreated = true; this.createDefaultPattern(specifierObj); } if (this.esmPath === "") return false; const specifierPattern = this.getSpecifierPattern(specifierObj); const isMatch = specifierPattern.esmPath === this.esmPath && specifierPattern.type === this.type && specifierPattern.exportedName === this.exportedName && specifierPattern.localName === this.localName; if (isMatch) { this.numOfDefaultPatternUsed += 1; this.exportedNamesList.push(specifierObj.exportedName); } return isMatch; } getSpecifier(exportedName) { const specifierObj = SpecifierFactory.createSpecifier("export"); specifierObj.esmPath = this.esmPath.replaceAll("${specifier}", exportedName); specifierObj.type = this.type; specifierObj.localName = this.localName.replaceAll("${specifier}", exportedName); specifierObj.exportedName = this.exportedName.replaceAll("${specifier}", exportedName); return specifierObj; } } class BarrelFile { constructor(path) { this.path = path; this.exportMapping = {}; this.defaultPatternExport = new DefaultPatternExport(); this.importMapping = {}; } static isBarrelFilename(path) { const barrelFileRegex = new RegExp(`index\.(js|mjs|jsx|ts|tsx)$`); return barrelFileRegex.test(path.toLowerCase()); } get isBarrelFileContent() { return !PathFunctions.isObjectEmpty(this.exportMapping) || this.defaultPatternExport.isDefaultPatternCreated; } resetProperties() { this.exportMapping = {}; this.defaultPatternExport = new DefaultPatternExport(); this.importMapping = {}; } getAllDirectPaths() { const obj = {}; for (const exportKey in this.exportMapping) { const path = this.exportMapping[exportKey].path; obj[path] = []; } for (const exportedName of this.defaultPatternExport.exportedNamesList) { const path = this.getDirectSpecifierObject(exportedName).path; obj[path] = []; } return obj; } handleExportNamedDeclaration(node) { if (node.specifiers.length > 0) { node.specifiers.forEach((specifier) => { let specifierObj = SpecifierFactory.createSpecifier("export"); specifierObj.exportedName = specifier.exported.name; specifierObj.localName = specifier?.local?.name; specifierObj.type = AST.getSpecifierType(specifier); specifierObj.esmPath = this.path; if (node.source) { // if node.source exist -> export { abc } from './abc'; const exportPath = node.source.value; specifierObj.esmPath = resolver.resolve(exportPath, this.path).absEsmFile; } else { // if node.source doesnt exist -> export { abc }; const { localName } = specifierObj; if (localName in this.importMapping) { // import { abc } from './module'; // export { abc }; specifierObj = this.importMapping[localName].toExportSpecifier(); specifierObj.exportedName = specifier.exported.name; } else { // const name = "name"; // export { name as differentName }; specifierObj.localName = specifier.exported.name; } } const { exportedName } = specifierObj; const deepestDirectSpecifier = this.getDeepestDirectSpecifierObject(specifierObj); deepestDirectSpecifier.esmPath = PathFunctions.normalizeModulePath(deepestDirectSpecifier.esmPath); if (!this.defaultPatternExport.isMatchDefaultPattern(deepestDirectSpecifier)) { this.exportMapping[exportedName] = deepestDirectSpecifier; } }); }; if (node.declaration) { const declarations = node.declaration.declarations || [node.declaration]; // if declaration exists -> export function abc(){}; // if declaration.declarations exists -> export const abc = 5, def = 10; declarations.forEach((declaration) => { if (t.isObjectPattern(declaration.id)) { for (const property of declaration.id.properties) { const specifierObj = SpecifierFactory.createSpecifier("export"); specifierObj.type = "named"; specifierObj.esmPath = this.path; specifierObj.localName = property.value.name; specifierObj.exportedName = property.value.name; const { exportedName } = specifierObj; specifierObj.esmPath = PathFunctions.normalizeModulePath(specifierObj.esmPath); if (!this.defaultPatternExport.isMatchDefaultPattern(specifierObj)) { this.exportMapping[exportedName] = specifierObj; } } } else { const specifierObj = SpecifierFactory.createSpecifier("export"); specifierObj.type = "named"; specifierObj.esmPath = this.path; specifierObj.localName = declaration.id.name; specifierObj.exportedName = declaration.id.name; const { exportedName } = specifierObj; specifierObj.esmPath = PathFunctions.normalizeModulePath(specifierObj.esmPath); if (!this.defaultPatternExport.isMatchDefaultPattern(specifierObj)) { this.exportMapping[exportedName] = specifierObj; } } }); } } handleExportDefaultDeclaration(node) { // export default abc; if (node.declaration.name) { const localName = node.declaration.name; if (localName in this.importMapping) { // import { abc } from './module'; // export default abc; const specifierObj = this.importMapping[localName].toExportSpecifier(); specifierObj.exportedName = "default"; const { exportedName } = specifierObj; const deepestDirectSpecifier = this.getDeepestDirectSpecifierObject(specifierObj); deepestDirectSpecifier.esmPath = PathFunctions.normalizeModulePath(deepestDirectSpecifier.esmPath); this.exportMapping[exportedName] = deepestDirectSpecifier; } else { // const defaultName = "defaultName"; // export default defaultName; let specifierObj = SpecifierFactory.createSpecifier("export"); specifierObj.localName = node.declaration.name; specifierObj.exportedName = "default"; specifierObj.type = "default"; specifierObj.esmPath = PathFunctions.normalizeModulePath(this.path); const { exportedName } = specifierObj; this.exportMapping[exportedName] = specifierObj; } } } handleExportAllDeclaration(node) { // export * from './abc'; const exportPath = node.source.value; const resolvedPathObject = resolver.resolve(exportPath, this.path); let absoluteExportedPath = resolvedPathObject.absEsmFile; if (!absoluteExportedPath) return; const exportedAllFile = new BarrelFile(absoluteExportedPath); exportedAllFile.defaultPatternExport.isDefaultPatternCreated = true; exportedAllFile.resolvedPathObject = resolvedPathObject; exportedAllFile.createSpecifiersMapping(true); delete exportedAllFile.exportMapping["default"]; if (resolvedPathObject.packageJsonExports) { for (const exportKey in exportedAllFile.exportMapping) { exportedAllFile.exportMapping[exportKey]["esmPath"] = resolvedPathObject.originalPath; } } Object.assign(this.exportMapping, exportedAllFile.exportMapping); } handleImportDeclaration(node) { if (AST.isSpecialImportCases(node)) return false; if (AST.isAnySpecifierExist(node.specifiers)) { const importPath = node.source.value; const resolvedPath = resolver.resolve(importPath, this.path).absEsmFile; node.specifiers.forEach((specifier) => { // import {abc, def} from './abc'; const specifierObj = SpecifierFactory.createSpecifier("import"); specifierObj.importedName = specifier?.imported?.name || "default"; specifierObj.localName = specifier.local.name; specifierObj.type = AST.getSpecifierType(specifier); specifierObj.esmPath = resolvedPath; const { localName } = specifierObj; this.importMapping[localName] = specifierObj; }); } } createSpecifiersMapping(forceFullScan = false) { const barrelAST = AST.filenameToAST(this.path); barrelAST.program.body.every((node) => { if (t.isExportNamedDeclaration(node)) { // export { abc } from './abc'; // export { abc }; // export function abc(){}; // export const abc = 5, def = 10; if (node.exportKind === "type") return true; if (node.declaration && !forceFullScan) { this.resetProperties(); return false; } this.handleExportNamedDeclaration(node); } else if (t.isExportDefaultDeclaration(node)) { // export default abc; this.handleExportDefaultDeclaration(node); } else if (t.isExportAllDeclaration(node)) { // export * from './abc'; this.handleExportAllDeclaration(node); } else if (t.isImportDeclaration(node)) { if (!AST.isAnySpecifierExist(node.specifiers) && !forceFullScan) { // import './abc'; this.resetProperties(); return false; } // import {abc, def} from './abc'; this.handleImportDeclaration(node); } else { if (forceFullScan) { return true; } else { this.resetProperties(); return false; } } return true; }); this.path = PathFunctions.normalizeModulePath(this.path); if (this.defaultPatternExport.numOfDefaultPatternUsed === 1) { this.exportMapping[this.defaultPatternExport.firstSpecifier.exportedName] = this.defaultPatternExport.firstSpecifier; this.defaultPatternExport = new DefaultPatternExport(); } delete this.defaultPatternExport.numOfDefaultPatternUsed; delete this.defaultPatternExport.firstSpecifier; delete this.importMapping; } getDeepestDirectSpecifierObject(specifierObj) { const { esmPath, localName } = specifierObj; if (BarrelFile.isBarrelFilename(esmPath) && esmPath !== this.path) { if (this.resolvedPathObject?.packageJsonExports) return specifierObj; const resolvedPathObject = resolver.resolve(esmPath ,this.path); if (resolvedPathObject.packageJsonExports) return specifierObj; const barrelFile = BarrelFileManagerFacade.getBarrelFile(resolvedPathObject.absEsmFile); if (barrelFile.isBarrelFileContent) { const deepestSpecifier = Object.assign(new ExportSpecifier(), {...barrelFile.getDirectSpecifierObject(localName)}); deepestSpecifier.exportedName = specifierObj.exportedName; return this.getDeepestDirectSpecifierObject(deepestSpecifier); } } return specifierObj; } getDirectSpecifierObject(specifierExportedName) { return this.exportMapping[specifierExportedName] ? this.exportMapping[specifierExportedName] : (this.defaultPatternExport.isDefaultPatternCreated && this.defaultPatternExport.getSpecifier(specifierExportedName)); } } class BarrelFilesPackage { constructor(packageObj, cache) { this.packageObj = packageObj; this.cache = cache; this.barrelFiles = this.cache?.data || new Map(); } getBarrelFile(path) { let barrelFile = new BarrelFile(path); if (BarrelFile.isBarrelFilename(path)) { const barrelKeyName = PathFunctions.normalizeModulePath(path); if (!this.barrelFiles.has(barrelKeyName)) { barrelFile.createSpecifiersMapping(); this.barrelFiles.set(barrelKeyName, barrelFile); } barrelFile = this.barrelFiles.get(barrelKeyName); }; return barrelFile; } } class BarrelFilesPackageCacheStrategy { customizedParsingMethod(cacheData) { const barrelFiles = new Map(); for (const [key, value] of Object.entries(cacheData)) { const barrelFile = Object.assign(new BarrelFile(), value); for (const [exportMappingKey, exportMappingValue] of Object.entries(value.exportMapping)) { barrelFile.exportMapping[exportMappingKey] = Object.assign(SpecifierFactory.createSpecifier("export"), exportMappingValue); } barrelFile.defaultPatternExport = Object.assign(new DefaultPatternExport(), value.defaultPatternExport); barrelFiles.set(key, barrelFile); } return barrelFiles; } } class BarrelFilesPackageCacheFacade { static getCachePackageFileName(packageName) { if (PathFunctions.isNodeModule(packageName)) { return `${packageName}.json`.replace("\\","_"); } else { return `local.json`; } } static createCache(packageObj) { const { isCacheEnabled } = pluginOptions.options; if (!isCacheEnabled || !PathFunctions.isNodeModule(packageObj.path)) return; const cacheFileName = BarrelFilesPackageCacheFacade.getCachePackageFileName(packageObj.name); const packagesVersionsFileName = 'packagesVersions.json' const cacheFolderName = "babel-plugin-transform-barrels_cache" const cache = new Cache(cacheFileName, cacheFolderName, packagesVersionsFileName, packageObj.name, packageObj.version, new BarrelFilesPackageCacheStrategy()); if (cache.isCacheUpdated) { cache.loadCacheData(); } return cache; } } class BarrelFilesPackagesManager { constructor() { this.barrelFilesByPackage = new Map(); } getBarrelFileManager(path) { const moduleDir = ospath.dirname(path) const mainPackagePath = Package.getHighestParentPackageDir(moduleDir); const packageName = PathFunctions.normalizeModulePath(mainPackagePath); let barrelFilesPackage; if (!this.barrelFilesByPackage.has(packageName)) { const packageObj = packageManager.getMainPackageOfModule(path); const cache = BarrelFilesPackageCacheFacade.createCache(packageObj); barrelFilesPackage = new BarrelFilesPackage(packageObj, cache); this.barrelFilesByPackage.set(packageName, barrelFilesPackage); } barrelFilesPackage = this.barrelFilesByPackage.get(packageName); return barrelFilesPackage; } } class BarrelFileManagerFacade { static getBarrelFile(path) { if (!BarrelFile.isBarrelFilename(path)) return new BarrelFile(); const barrelFilesPackage = barrelFilesPackagesManager.getBarrelFileManager(path); const barrelFile = barrelFilesPackage.getBarrelFile(path); return barrelFile; } static saveToCacheAllPackagesBarrelFiles() { const { isCacheEnabled } = pluginOptions.options; barrelFilesPackagesManager.barrelFilesByPackage.forEach((barrelFilesPackage)=>{ if (isCacheEnabled && PathFunctions.isNodeModule(barrelFilesPackage.packageObj.path) && !barrelFilesPackage.cache.isCacheUpdated) { barrelFilesPackage.cache.saveCache(barrelFilesPackage.barrelFiles); } }) } } class SpecifierFactory { static createSpecifier(type) { switch (type) { case 'export': return new ExportSpecifier(); case 'import': return new ImportSpecifier(); default: throw new Error('Invalid specifier type'); } } } class Specifier { constructor() { this.esmPath = ""; this.type = ""; this.localName = ""; } get absEsmPath() { const from = PathFunctions.isNodeModule(this.esmPath) ? ospath.dirname(resolver.from) : process.cwd(); return PathFunctions.getAbsolutePath(this.esmPath, from, resolver.modulesDirs); } get cjsPath() { const packageObj = packageManager.getMainPackageOfModule(this.absEsmPath); if (packageObj.name === this.esmPath) return this.esmPath; const replacedPath = packageObj.convertESMToCJSPath(this.esmPath); const from = PathFunctions.isNodeModule(replacedPath) ? ospath.dirname(resolver.from) : process.cwd(); const absReplacedPath = PathFunctions.getAbsolutePath(replacedPath, from, resolver.modulesDirs); if (!absReplacedPath) return null; return replacedPath; } get absCjsPath() { const from = PathFunctions.isNodeModule(this.esmPath) ? ospath.dirname(resolver.from) : process.cwd(); return PathFunctions.getAbsolutePath(this.cjsPath, from, resolver.modulesDirs); } get path() { if (!PathFunctions.isNodeModule(this.esmPath)) { return this.absEsmPath; } else { const packageObj = packageManager.getNearestPackageJsonContent(resolver.from); if (packageObj?.type === "module") { return this.esmPath; } else { return this.cjsPath; } } } } class ExportSpecifier extends Specifier { constructor() { super(); this.exportedName = ""; } toImportSpecifier() { const specifierObj = SpecifierFactory.createSpecifier("import"); specifierObj.type = this.type; specifierObj.esmPath = this.esmPath; if (!PathFunctions.isNodeModule(specifierObj.esmPath)) { specifierObj.esmPath = ospath.join(process.cwd(), specifierObj.esmPath); } specifierObj.importedName = this.localName; return specifierObj; } } class ImportSpecifier extends Specifier { constructor() { super(); this.importedName = ""; } toExportSpecifier() { const specifierObj = SpecifierFactory.createSpecifier("export"); specifierObj.type = this.type; specifierObj.esmPath = this.esmPath; specifierObj.localName = this.importedName; return specifierObj; } } const barrelFilesPackagesManager = new BarrelFilesPackagesManager(); module.exports = BarrelFileManagerFacade;