Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
2578a63610 Fix --packages=external incorrectly externalizing tsconfig path aliases
When using `--packages=external`, tsconfig path aliases (like `~/foo`
mapping to `./src/foo`) were being incorrectly marked as external packages
instead of being resolved and bundled.

The issue occurred because the resolver checked if something should be
external before checking if it matched a tsconfig path alias. Since path
aliases like `~/foo` don't start with `./` or `../`, they were classified
as "package paths" and marked external.

This change adds a `couldMatchTSConfigPaths` function that checks if an
import could match a tsconfig path pattern before marking it as external.
This ensures:
- Tsconfig path aliases are resolved and bundled correctly
- Real npm packages remain external as expected
- Minimal performance impact (only pattern matching, not full resolution)

Fixes #6351

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 14:06:51 +00:00
2 changed files with 128 additions and 2 deletions

View File

@@ -591,8 +591,12 @@ pub const Resolver = struct {
r.dir_cache.deinit();
}
pub fn isExternalPattern(r: *ThisResolver, import_path: string) bool {
pub fn isExternalPattern(r: *ThisResolver, import_path: string, source_dir: string) bool {
if (r.opts.packages == .external and isPackagePath(import_path)) {
// Before marking as external, check if this might be a tsconfig path alias
if (r.couldMatchTSConfigPaths(import_path, source_dir)) {
return false;
}
return true;
}
for (r.opts.external.patterns) |pattern| {
@@ -609,6 +613,38 @@ pub const Resolver = struct {
return false;
}
/// Check if an import path could potentially match a tsconfig path alias
/// This is used to avoid marking tsconfig path aliases as external when using --packages=external
pub fn couldMatchTSConfigPaths(r: *ThisResolver, import_path: string, source_dir: string) bool {
const dir_info_ptr = r.dirInfoCached(source_dir) catch return false;
const dir_info = dir_info_ptr orelse return false;
const tsconfig = dir_info.enclosing_tsconfig_json orelse return false;
if (tsconfig.paths.count() == 0) return false;
// Check for exact matches
if (tsconfig.paths.contains(import_path)) {
return true;
}
// Check for wildcard matches
var iter = tsconfig.paths.iterator();
while (iter.next()) |entry| {
const key = entry.key_ptr.*;
if (strings.indexOfChar(key, '*')) |star| {
const prefix = if (star == 0) "" else key[0..star];
const suffix = if (star == key.len - 1) "" else key[star + 1 ..];
if (strings.startsWith(import_path, prefix) and strings.endsWith(import_path, suffix)) {
return true;
}
}
}
return false;
}
pub fn flushDebugLogs(r: *ThisResolver, flush_mode: DebugLogs.FlushMode) !void {
if (r.debug_logs) |*debug| {
if (flush_mode == DebugLogs.FlushMode.fail) {
@@ -699,7 +735,7 @@ pub const Resolver = struct {
// Certain types of URLs default to being external for convenience,
// while these rules should not be applied to the entrypoint as it is never external (#12734)
if (kind != .entry_point_build and kind != .entry_point_run and
(r.isExternalPattern(import_path) or
(r.isExternalPattern(import_path, source_dir) or
// "fill: url(#filter);"
(kind.isFromCSS() and strings.startsWith(import_path, "#")) or

View File

@@ -1385,6 +1385,96 @@ describe("bundler", () => {
`,
},
});
itBundled("edgecase/PackageExternalShouldNotExternalizeTsconfigPaths#6351", {
files: {
"/src/entry.ts": /* ts */ `
import { foo } from "~/foo";
import { bar } from "@/bar";
import { external } from "external-package";
console.log(foo, bar, external);
`,
"/src/foo.ts": /* ts */ `
export const foo = "foo value";
`,
"/src/bar.ts": /* ts */ `
export const bar = "bar value";
`,
"/tsconfig.json": /* json */ `
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"],
"@/*": ["./src/*"]
}
}
}
`,
},
packages: "external",
target: "bun",
runtimeFiles: {
"/node_modules/external-package/index.js": `export const external = "external value";`,
"/node_modules/external-package/package.json": /* json */ `
{
"name": "external-package",
"version": "1.0.0",
"main": "index.js"
}
`,
},
onAfterBundle(api) {
const out = api.readFile("/out.js");
// Tsconfig path aliases should be bundled, not externalized
expect(out).toContain("foo value");
expect(out).toContain("bar value");
// Real external packages should be externalized
expect(out).toContain('from "external-package"');
},
run: {
file: "/src/entry.ts",
stdout: `
foo value bar value external value
`,
},
});
itBundled("edgecase/PackageExternalWithoutTsconfigPathsShouldStillExternalize", {
files: {
"/src/entry.ts": /* ts */ `
import { external } from "external-package";
import { local } from "./local";
console.log(external, local);
`,
"/src/local.ts": /* ts */ `
export const local = "local value";
`,
},
packages: "external",
target: "bun",
runtimeFiles: {
"/node_modules/external-package/index.js": `export const external = "external value";`,
"/node_modules/external-package/package.json": /* json */ `
{
"name": "external-package",
"version": "1.0.0",
"main": "index.js"
}
`,
},
onAfterBundle(api) {
const out = api.readFile("/out.js");
// Local files should be bundled
expect(out).toContain("local value");
// External packages should be externalized
expect(out).toContain('from "external-package"');
},
run: {
file: "/src/entry.ts",
stdout: `
external value local value
`,
},
});
itBundled("edgecase/IntegerUnderflow#12547", {
files: {
"/entry.js": `