Skip to content

Instantly share code, notes, and snippets.

@matheus1lva
Created October 16, 2024 14:35
Show Gist options
  • Save matheus1lva/881bf40f9cfc79cb1f152d0c8aca79ee to your computer and use it in GitHub Desktop.
Save matheus1lva/881bf40f9cfc79cb1f152d0c8aca79ee to your computer and use it in GitHub Desktop.
import { Visitor, Program, ImportDeclaration, ExportDeclaration, ModuleItem, ModuleDeclaration } from "@swc/core";
import { parse } from "@swc/core";
import * as path from "path";
import * as fs from "fs";
interface DebarrelPluginOptions {
include?: RegExp[];
possibleBarrelFiles?: RegExp[];
}
interface ResolvedSource {
id: string;
exportName: string;
aliasedImportName?: string;
}
class DebarrelVisitor extends Visitor {
private options: DebarrelPluginOptions;
private currentFilePath: string;
private fileCache: Map<string, string> = new Map();
private parseCache: Map<string, Program> = new Map();
constructor(options: DebarrelPluginOptions, currentFilePath: string) {
super();
this.options = options;
this.currentFilePath = currentFilePath;
}
visitProgram(program: Program): Program {
const newBody: ModuleItem[] = [];
for (const item of program.body) {
if (item.type === "ImportDeclaration") {
const debarrelledImports = this.debarrelImports(item as ImportDeclaration);
newBody.push(...debarrelledImports);
} else if (item.type === "ExportDeclaration") {
const debarrelledExports = this.debarrelExports(item as ExportDeclaration);
newBody.push(...debarrelledExports);
} else {
newBody.push(item);
}
}
return { ...program, body: newBody };
}
private debarrelImports(node: ImportDeclaration): ModuleItem[] {
if (!this.shouldProcessFile(this.currentFilePath)) {
return [node];
}
const source = node.source.value;
const resolvedPath = this.resolvePath(source);
if (!this.isPossibleBarrelFile(resolvedPath)) {
return [node];
}
const debarrelledImports: ImportDeclaration[] = [];
for (const specifier of node.specifiers) {
const resolved = this.resolveThroughBarrel(resolvedPath, specifier.local.value);
if (resolved) {
debarrelledImports.push({
...node,
source: { ...node.source, value: resolved.id },
specifiers: [{
...specifier,
local: { ...specifier.local, value: resolved.exportName },
imported: resolved.aliasedImportName ? { ...specifier.local, value: resolved.aliasedImportName } : undefined
}]
});
}
}
return debarrelledImports.length > 0 ? debarrelledImports : [node];
}
private debarrelExports(node: ExportDeclaration): ModuleItem[] {
if (!this.shouldProcessFile(this.currentFilePath) || !node.source) {
return [node];
}
const source = node.source.value;
const resolvedPath = this.resolvePath(source);
if (!this.isPossibleBarrelFile(resolvedPath)) {
return [node];
}
const debarrelledExports: ExportDeclaration[] = [];
for (const specifier of node.specifiers) {
const resolved = this.resolveThroughBarrel(resolvedPath, specifier.exported.value);
if (resolved) {
debarrelledExports.push({
...node,
source: { ...node.source, value: resolved.id },
specifiers: [{
...specifier,
exported: { ...specifier.exported, value: resolved.exportName },
orig: resolved.aliasedImportName ? { ...specifier.exported, value: resolved.aliasedImportName } : undefined
}]
});
}
}
return debarrelledExports.length > 0 ? debarrelledExports : [node];
}
private shouldProcessFile(filePath: string): boolean {
if (this.options.include) {
return this.options.include.some(pattern => pattern.test(filePath));
}
return true;
}
private isPossibleBarrelFile(filePath: string): boolean {
if (this.options.possibleBarrelFiles) {
return this.options.possibleBarrelFiles.some(pattern => pattern.test(filePath));
}
return /(?:\.ts|\/index\.tsx?)$/.test(filePath);
}
private resolvePath(source: string): string {
return path.resolve(path.dirname(this.currentFilePath), source);
}
private readFile(filePath: string): string {
if (!this.fileCache.has(filePath)) {
this.fileCache.set(filePath, fs.readFileSync(filePath, 'utf8'));
}
return this.fileCache.get(filePath)!;
}
private parseFile(filePath: string): Program {
if (!this.parseCache.has(filePath)) {
const code = this.readFile(filePath);
this.parseCache.set(filePath, parse(code, { syntax: "typescript" }));
}
return this.parseCache.get(filePath)!;
}
private resolveThroughBarrel(id: string, exportName: string): ResolvedSource | null {
const program = this.parseFile(id);
for (const item of program.body) {
if (item.type === "ExportDeclaration") {
const resolved = this.resolveExport(item, exportName);
if (resolved) {
if (resolved.id === id) {
return resolved;
}
return this.resolveThroughBarrel(resolved.id, resolved.exportName);
}
}
}
return null;
}
private resolveExport(node: ExportDeclaration, exportName: string): ResolvedSource | null {
for (const specifier of node.specifiers) {
if (specifier.exported.value === exportName) {
if (node.source) {
return {
id: this.resolvePath(node.source.value),
exportName: specifier.orig ? specifier.orig.value : exportName,
aliasedImportName: specifier.orig ? specifier.orig.value : undefined
};
} else {
return { id: this.currentFilePath, exportName };
}
}
}
return null;
}
}
export default function debarrelPlugin(options: DebarrelPluginOptions = {}): { visitor: typeof Visitor } {
return {
visitor: class extends Visitor {
constructor(program: Program, currentFilePath: string) {
super();
const debarrelVisitor = new DebarrelVisitor(options, currentFilePath);
this.visitProgram = (program: Program) => debarrelVisitor.visitProgram(program);
}
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment