A barrel file in JavaScript is a module that consolidates and re-exports multiple exports from other files into a single convenient entry point.
This allows developers to import multiple items from a single file instead of importing them individually from separate files. You've probably seen and created barrel files before, perhaps via an index.js
in the root of a directory with multiple files.
While barrel files can be convenient, they can also be a pain in the ass and negatively impact build performance, tree-shaking, and developer tooling. For a deeper dive into why barrel files can be problematic, check out this great recap from Jason Miller: Scaling Vite at Shopify, ViteConf 2024.
I recently had to kill some huge barrel files in a big codebase. The goal was to stop the codebase from referencing barrel files (e.g., src/design-system/index.ts
) by changing all the imports referencing such files.
For example, changing this:
import { Button } from "src/design-system"
import { Button } from "src/design-system"
To this:
import { Button } from "src/design-system/atoms/button"
import { Button } from "src/design-system/atoms/button"
Doing this manually or via some smart search & replace was not an option, as there were several import paths and edge cases to cover that would require manual tweaks.
Initially, I thought about rewiring the imports at build time. But... this seemed too complex to scale without impacting the build time.
Instead, I decided to change the codebase by building a codemod. My prior experience with codemods was limited to tiny scripts, so I leveraged AI a lot here. After a lot of back and forth with Claude 3.5 Sonnet, I was able to build and run a codemod successfully. I'm sharing it here for anyone interested :)
This codemod is designed to work with jscodeshift to modify TypeScript/JavaScript files.
Features:
- Packaged in a single file you can drag and drop into your codebase.
- Handles multiple barrel files.
- Transforms both value and type imports.
- Supports both relative and absolute imports.
- Supports both named and default imports (even renamed ones).
- Allows specifying some specific paths for which you want to drop the last segment (see
DROP_LAST_SEGMENT_PATHS
). For example, you can configure it to re-mapsrc/design-system/atoms/button/index.ts
tosrc/design-system/atoms/button
instead.
Usage:
- Copy and paste the
transform-barrel-file-imports.ts
codemod (it's down below) anywhere in your codebase. - Update the
BARREL_IMPORTS
array with the paths of your barrel files. - Update the
DROP_LAST_SEGMENT_PATHS
array if you want to drop the last segment of the path (for example, when importing from a folder with an index file). - Run the script!
npx jscodeshift -t ./transform-barrel-file-imports.ts --parser=ts ./src
Options:
--dry
: Use this flag to see what changes would be made without actually changing files.--run-in-band
: Runs the script sequentially instead of in parallel (recommended for debugging).
Example:
npx jscodeshift -t ./transform-barrel-file-imports.ts --parser=ts ./src --run-in-band --dry ./src > transform-log.txt
Notes:
- Always run with
--dry
first and review the changes before applying them to your codebase. - The codemod doesn't delete the barrel files: you should manually delete them after a successful run (or you can easily change the codemod to do it for you).
- There might be multiple uncovered edge cases 🙃
import * as fs from "fs";
import * as path from "path";
import * as ts from "typescript";
import type { API, FileInfo, Options, Transform } from "jscodeshift";
// List of barrel files to process; in an ideal world this shouldn't be
// hardcoded, but it's a good starting point.
const BARREL_IMPORTS = [
"src/services/my-service",
"src/design-system",
// Add more barrel files here
];
// Optional - List of paths where we want to drop the last segment
const DROP_LAST_SEGMENT_PATHS = [
"src/design-system/components",
"src/design-system/atoms",
"src/design-system/molecules",
"src/design-system/organisms"
];
// This map will store the real paths of all exported components, types, and enums
const exportedItemsMap = new Map<
string,
{ path: string; kind: "value" | "type" }
>();
function getCompilerOptions(filePath: string): ts.CompilerOptions {
const configPath = ts.findConfigFile(
path.dirname(filePath),
ts.sys.fileExists,
"tsconfig.json"
);
if (!configPath) {
throw new Error("Could not find a valid 'tsconfig.json'.");
}
const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
const { options } = ts.parseJsonConfigFileContent(
config,
ts.sys,
path.dirname(configPath)
);
return options;
}
function resolveModule(
importPath: string,
containingFile: string
): string | null {
const options = getCompilerOptions(containingFile);
const moduleResolutionHost: ts.ModuleResolutionHost = {
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
realpath: ts.sys.realpath,
directoryExists: ts.sys.directoryExists,
getCurrentDirectory: () => process.cwd(),
getDirectories: ts.sys.getDirectories,
};
const resolved = ts.resolveModuleName(
importPath,
containingFile,
options,
moduleResolutionHost
);
return resolved.resolvedModule?.resolvedFileName || null;
}
function buildExportMap(filePath: string, visited = new Set<string>()) {
if (visited.has(filePath)) return;
visited.add(filePath);
const fileContent = fs.readFileSync(filePath, "utf-8");
const sourceFile = ts.createSourceFile(
filePath,
fileContent,
ts.ScriptTarget.Latest,
true
);
function visit(node: ts.Node) {
if (ts.isExportDeclaration(node)) {
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
node.exportClause.elements.forEach((element) => {
const kind = element.isTypeOnly ? "type" : "value";
if (node.moduleSpecifier) {
const modulePath = (node.moduleSpecifier as ts.StringLiteral).text;
const resolvedPath = resolveModule(modulePath, filePath);
if (resolvedPath) {
exportedItemsMap.set(element.name.text, {
path: resolvedPath,
kind,
});
}
} else {
exportedItemsMap.set(element.name.text, { path: filePath, kind });
}
});
} else if (node.moduleSpecifier) {
const modulePath = (node.moduleSpecifier as ts.StringLiteral).text;
const resolvedPath = resolveModule(modulePath, filePath);
if (resolvedPath) {
buildExportMap(resolvedPath, visited);
}
}
} else if (ts.isExportAssignment(node)) {
exportedItemsMap.set("default", { path: filePath, kind: "value" });
} else if (
(ts.isFunctionDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isVariableStatement(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isEnumDeclaration(node)) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (
ts.isFunctionDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isEnumDeclaration(node)
) {
if (node.name) {
const kind =
ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)
? "type"
: "value";
exportedItemsMap.set(node.name.text, { path: filePath, kind });
}
} else if (ts.isVariableStatement(node)) {
node.declarationList.declarations.forEach((decl) => {
if (ts.isIdentifier(decl.name)) {
exportedItemsMap.set(decl.name.text, {
path: filePath,
kind: "value",
});
}
});
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
}
const transform: Transform = (
fileInfo: FileInfo,
api: API,
options: Options
) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// Build the export map if it hasn't been built yet
if (exportedItemsMap.size === 0) {
BARREL_IMPORTS.forEach((barrelImport) => {
const barrelPath = resolveModule(barrelImport, fileInfo.path);
if (barrelPath) {
buildExportMap(barrelPath);
} else {
console.warn(`Could not resolve barrel file: ${barrelImport}`);
}
});
}
let modified = false;
root.find(j.ImportDeclaration).forEach((nodePath) => {
const importPath = nodePath.node.source.value;
const matchingBarrel = BARREL_IMPORTS.find(
(barrel) => importPath === barrel || importPath.endsWith(`/${barrel}`)
);
if (matchingBarrel) {
const newImports = new Map<
string,
{ valueSpecifiers: any[]; typeSpecifiers: any[] }
>();
nodePath.node.specifiers.forEach((specifier) => {
if (specifier.type === "ImportSpecifier") {
const itemName = specifier.imported.name;
const localName = specifier.local.name;
const exportedItem = exportedItemsMap.get(itemName);
if (exportedItem) {
// Get the path relative to the barrel file
const barrelDir = path.dirname(
resolveModule(matchingBarrel, fileInfo.path) || ""
);
let relativePath = path.relative(barrelDir, exportedItem.path);
// If the relative path is empty, it means the export is from the barrel file itself
if (relativePath === "") {
relativePath = ".";
}
// Remove the file extension
relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
// Ensure the path starts with the correct barrel import
let newImportPath = path
.join(matchingBarrel, relativePath)
.replace(/\\/g, "/");
// Check if we need to drop the last segment
const shouldDropLastSegment = DROP_LAST_SEGMENT_PATHS.some(
(dropPath) => newImportPath.startsWith(dropPath)
);
if (shouldDropLastSegment) {
newImportPath = path.dirname(newImportPath);
}
if (!newImports.has(newImportPath)) {
newImports.set(newImportPath, {
valueSpecifiers: [],
typeSpecifiers: [],
});
}
const importGroup = newImports.get(newImportPath)!;
const newSpecifier = j.importSpecifier(
j.identifier(itemName),
itemName !== localName ? j.identifier(localName) : null
);
if (
exportedItem.kind === "type" ||
specifier.importKind === "type"
) {
importGroup.typeSpecifiers.push(newSpecifier);
} else {
importGroup.valueSpecifiers.push(newSpecifier);
}
} else {
console.warn(`Could not find export information for ${itemName}`);
}
}
});
const newImportNodes = [...newImports.entries()].flatMap(
([importPath, { valueSpecifiers, typeSpecifiers }]) => {
const imports = [];
if (valueSpecifiers.length > 0) {
imports.push(
j.importDeclaration(valueSpecifiers, j.literal(importPath))
);
}
if (typeSpecifiers.length > 0) {
imports.push(
j.importDeclaration(typeSpecifiers, j.literal(importPath), "type")
);
}
return imports;
}
);
if (newImportNodes.length > 0) {
j(nodePath).replaceWith(newImportNodes);
modified = true;
}
}
});
if (modified) {
console.log(`Modified imports in ${fileInfo.path}`);
return root.toSource();
}
return null;
};
export default transform;
import * as fs from "fs";
import * as path from "path";
import * as ts from "typescript";
import type { API, FileInfo, Options, Transform } from "jscodeshift";
// List of barrel files to process; in an ideal world this shouldn't be
// hardcoded, but it's a good starting point.
const BARREL_IMPORTS = [
"src/services/my-service",
"src/design-system",
// Add more barrel files here
];
// Optional - List of paths where we want to drop the last segment
const DROP_LAST_SEGMENT_PATHS = [
"src/design-system/components",
"src/design-system/atoms",
"src/design-system/molecules",
"src/design-system/organisms"
];
// This map will store the real paths of all exported components, types, and enums
const exportedItemsMap = new Map<
string,
{ path: string; kind: "value" | "type" }
>();
function getCompilerOptions(filePath: string): ts.CompilerOptions {
const configPath = ts.findConfigFile(
path.dirname(filePath),
ts.sys.fileExists,
"tsconfig.json"
);
if (!configPath) {
throw new Error("Could not find a valid 'tsconfig.json'.");
}
const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
const { options } = ts.parseJsonConfigFileContent(
config,
ts.sys,
path.dirname(configPath)
);
return options;
}
function resolveModule(
importPath: string,
containingFile: string
): string | null {
const options = getCompilerOptions(containingFile);
const moduleResolutionHost: ts.ModuleResolutionHost = {
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
realpath: ts.sys.realpath,
directoryExists: ts.sys.directoryExists,
getCurrentDirectory: () => process.cwd(),
getDirectories: ts.sys.getDirectories,
};
const resolved = ts.resolveModuleName(
importPath,
containingFile,
options,
moduleResolutionHost
);
return resolved.resolvedModule?.resolvedFileName || null;
}
function buildExportMap(filePath: string, visited = new Set<string>()) {
if (visited.has(filePath)) return;
visited.add(filePath);
const fileContent = fs.readFileSync(filePath, "utf-8");
const sourceFile = ts.createSourceFile(
filePath,
fileContent,
ts.ScriptTarget.Latest,
true
);
function visit(node: ts.Node) {
if (ts.isExportDeclaration(node)) {
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
node.exportClause.elements.forEach((element) => {
const kind = element.isTypeOnly ? "type" : "value";
if (node.moduleSpecifier) {
const modulePath = (node.moduleSpecifier as ts.StringLiteral).text;
const resolvedPath = resolveModule(modulePath, filePath);
if (resolvedPath) {
exportedItemsMap.set(element.name.text, {
path: resolvedPath,
kind,
});
}
} else {
exportedItemsMap.set(element.name.text, { path: filePath, kind });
}
});
} else if (node.moduleSpecifier) {
const modulePath = (node.moduleSpecifier as ts.StringLiteral).text;
const resolvedPath = resolveModule(modulePath, filePath);
if (resolvedPath) {
buildExportMap(resolvedPath, visited);
}
}
} else if (ts.isExportAssignment(node)) {
exportedItemsMap.set("default", { path: filePath, kind: "value" });
} else if (
(ts.isFunctionDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isVariableStatement(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isEnumDeclaration(node)) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (
ts.isFunctionDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isEnumDeclaration(node)
) {
if (node.name) {
const kind =
ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)
? "type"
: "value";
exportedItemsMap.set(node.name.text, { path: filePath, kind });
}
} else if (ts.isVariableStatement(node)) {
node.declarationList.declarations.forEach((decl) => {
if (ts.isIdentifier(decl.name)) {
exportedItemsMap.set(decl.name.text, {
path: filePath,
kind: "value",
});
}
});
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
}
const transform: Transform = (
fileInfo: FileInfo,
api: API,
options: Options
) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// Build the export map if it hasn't been built yet
if (exportedItemsMap.size === 0) {
BARREL_IMPORTS.forEach((barrelImport) => {
const barrelPath = resolveModule(barrelImport, fileInfo.path);
if (barrelPath) {
buildExportMap(barrelPath);
} else {
console.warn(`Could not resolve barrel file: ${barrelImport}`);
}
});
}
let modified = false;
root.find(j.ImportDeclaration).forEach((nodePath) => {
const importPath = nodePath.node.source.value;
const matchingBarrel = BARREL_IMPORTS.find(
(barrel) => importPath === barrel || importPath.endsWith(`/${barrel}`)
);
if (matchingBarrel) {
const newImports = new Map<
string,
{ valueSpecifiers: any[]; typeSpecifiers: any[] }
>();
nodePath.node.specifiers.forEach((specifier) => {
if (specifier.type === "ImportSpecifier") {
const itemName = specifier.imported.name;
const localName = specifier.local.name;
const exportedItem = exportedItemsMap.get(itemName);
if (exportedItem) {
// Get the path relative to the barrel file
const barrelDir = path.dirname(
resolveModule(matchingBarrel, fileInfo.path) || ""
);
let relativePath = path.relative(barrelDir, exportedItem.path);
// If the relative path is empty, it means the export is from the barrel file itself
if (relativePath === "") {
relativePath = ".";
}
// Remove the file extension
relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
// Ensure the path starts with the correct barrel import
let newImportPath = path
.join(matchingBarrel, relativePath)
.replace(/\\/g, "/");
// Check if we need to drop the last segment
const shouldDropLastSegment = DROP_LAST_SEGMENT_PATHS.some(
(dropPath) => newImportPath.startsWith(dropPath)
);
if (shouldDropLastSegment) {
newImportPath = path.dirname(newImportPath);
}
if (!newImports.has(newImportPath)) {
newImports.set(newImportPath, {
valueSpecifiers: [],
typeSpecifiers: [],
});
}
const importGroup = newImports.get(newImportPath)!;
const newSpecifier = j.importSpecifier(
j.identifier(itemName),
itemName !== localName ? j.identifier(localName) : null
);
if (
exportedItem.kind === "type" ||
specifier.importKind === "type"
) {
importGroup.typeSpecifiers.push(newSpecifier);
} else {
importGroup.valueSpecifiers.push(newSpecifier);
}
} else {
console.warn(`Could not find export information for ${itemName}`);
}
}
});
const newImportNodes = [...newImports.entries()].flatMap(
([importPath, { valueSpecifiers, typeSpecifiers }]) => {
const imports = [];
if (valueSpecifiers.length > 0) {
imports.push(
j.importDeclaration(valueSpecifiers, j.literal(importPath))
);
}
if (typeSpecifiers.length > 0) {
imports.push(
j.importDeclaration(typeSpecifiers, j.literal(importPath), "type")
);
}
return imports;
}
);
if (newImportNodes.length > 0) {
j(nodePath).replaceWith(newImportNodes);
modified = true;
}
}
});
if (modified) {
console.log(`Modified imports in ${fileInfo.path}`);
return root.toSource();
}
return null;
};
export default transform;