Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/beige-sheep-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"debarrel": patch
---

Fix debarreling codemod's issue with alias imports
7 changes: 4 additions & 3 deletions codemods/debarrel/scripts/codemod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
hasPackageJson,
isBarrelFile,
isInsideNodeModules,
isPackageEntrypoint,
} from "./utils/paths.ts";
import { isPureBarrel } from "./utils/barrel.ts";
import { resolveSpecifier, type SpecRewrite } from "./utils/specifiers.ts";
Expand Down Expand Up @@ -136,12 +137,12 @@ const codemod: Codemod<Language> = async (root, options) => {
rewriteMockCalls(rootNode, barrelRewrites, edits);

// Barrel rename — skip files inside node_modules or inside a package
// (renaming a package entry point would break consumers importing via
// the package name).
// when the barrel is an actual package entrypoint (renaming it would break
// consumers importing via the package name).
if (
isBarrelFile(filename) &&
!isInsideNodeModules(filename) &&
!hasPackageJson(filename)
(!hasPackageJson(filename) || !isPackageEntrypoint(filename))
) {
const { pure, hasWildcards } = isPureBarrel(rootNode);
if (pure && !hasWildcards) {
Expand Down
90 changes: 86 additions & 4 deletions codemods/debarrel/scripts/utils/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,94 @@ function fileExists(filePath: string): boolean {
}
}

export function hasPackageJson(filename: string): boolean {
function readJsonFile(filePath: string): unknown | null {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch {
return null;
}
}

function normalizePackageTarget(target: string): string {
return target.replace(/\\/g, "/").replace(/^\.\//, "");
}

function collectExportTargets(value: unknown, targets: string[]): void {
if (typeof value === "string") {
targets.push(normalizePackageTarget(value));
return;
}

if (!value || typeof value !== "object") return;

if (Array.isArray(value)) {
for (const item of value) collectExportTargets(item, targets);
return;
}

for (const nested of Object.values(value)) {
collectExportTargets(nested, targets);
}
}

export function findNearestPackageJson(filename: string): string | null {
let dir = path.dirname(filename);
const root = path.parse(dir).root || "/";
while (dir !== root) {
if (fileExists(path.join(dir, "package.json"))) return true;
while (true) {
const packageJsonPath = path.join(dir, "package.json");
if (fileExists(packageJsonPath)) return packageJsonPath;
if (dir === root) return null;
dir = path.dirname(dir);
}
return false;
}

export function getPackageName(filename: string): string | null {
const packageJsonPath = findNearestPackageJson(filename);
if (!packageJsonPath) return null;
const parsed = readJsonFile(packageJsonPath);
if (!parsed || typeof parsed !== "object") return null;
const name = (parsed as { name?: unknown }).name;
return typeof name === "string" && name.length > 0 ? name : null;
}

export function hasPackageJson(filename: string): boolean {
return findNearestPackageJson(filename) !== null;
}

export function isPackageEntrypoint(filename: string): boolean {
const packageJsonPath = findNearestPackageJson(filename);
if (!packageJsonPath) return false;

const parsed = readJsonFile(packageJsonPath);
if (!parsed || typeof parsed !== "object") return false;

const packageDir = path.dirname(packageJsonPath);
const relativeFilename = path
.relative(packageDir, filename)
.replace(/\\/g, "/");
if (!relativeFilename || relativeFilename.startsWith("../")) return false;

const manifest = parsed as {
main?: unknown;
module?: unknown;
types?: unknown;
typings?: unknown;
exports?: unknown;
};
const entrypoints: string[] = [];

for (const field of [
manifest.main,
manifest.module,
manifest.types,
manifest.typings,
]) {
if (typeof field === "string") {
entrypoints.push(normalizePackageTarget(field));
}
}

collectExportTargets(manifest.exports, entrypoints);

return entrypoints.includes(relativeFilename);
}
28 changes: 21 additions & 7 deletions codemods/debarrel/scripts/utils/specifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ import type { SgNode, SgRoot } from "codemod:ast-grep";
import type { Language } from "./language.ts";
import { getStringContent } from "./ast.ts";
import {
hasPackageJson,
getPackageName,
isBarrelFile,
isInsideNodeModules,
isLocalRelativePath,
joinImportPaths,
} from "./paths.ts";
import { parseBarrelExport } from "./barrel.ts";

function getImportPackageName(importPath: string): string | null {
if (importPath.startsWith("@")) {
const segments = importPath.split("/");
return segments.length >= 2 ? `${segments[0]}/${segments[1]}` : null;
}

const [packageName] = importPath.split("/");
return packageName || null;
}

export interface SpecRewrite {
consumerName: string;
newImportPath: string;
Expand All @@ -34,12 +44,16 @@ export function resolveSpecifier(
// package.json "exports" that would break if we change the import subpath.
if (isInsideNodeModules(def.root.filename())) return null;

// For non-relative imports (package names, aliases), skip if the resolved
// file lives inside a package (has a package.json ancestor). The package's
// "exports" field controls valid subpaths — rewriting the import could
// produce a path that isn't exported (e.g. @acme/validators → @acme/validators/foo).
if (!isLocalRelativePath(importPath) && hasPackageJson(def.root.filename())) {
return null;
// For non-relative imports, only preserve the package boundary when the
// resolved file belongs to the same named package as the import specifier.
// This keeps tsconfig aliases like `~/foo` or `@acme/pkg/*` rewriteable
// even when the surrounding repo has an unrelated package.json.
if (!isLocalRelativePath(importPath)) {
const packageName = getPackageName(def.root.filename());
const importPackage = getImportPackageName(importPath);
if (packageName && importPackage === packageName) {
return null;
}
}

if (isBarrelFile(def.root.filename())) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "fixture-app"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "fixture-app"
}
Loading