Compare commits

...

1 Commits

Author SHA1 Message Date
Don Isaac
c57ef78b79 fix(resolve): exports field should take precedence over tsconfig paths 2025-01-28 19:37:16 -08:00
7 changed files with 171 additions and 6 deletions

View File

@@ -1617,6 +1617,13 @@ pub const Api = struct {
}
};
/// Configures how the resolver behaves.
pub const ResolveOptions = struct {
/// Do not consider tsconfig paths. Used primarily for node
/// compatibility.
ignore_tsconfig: bool = false,
};
pub const TransformOptions = struct {
/// jsx
jsx: ?Jsx = null,
@@ -1707,6 +1714,7 @@ pub const Api = struct {
serve_minify_identifiers: ?bool = null,
bunfig_path: []const u8,
/// FIXME: unused
pub fn decode(reader: anytype) anyerror!TransformOptions {
var this = std.mem.zeroes(TransformOptions);

View File

@@ -232,9 +232,11 @@ pub const Run = struct {
b.resolver.opts.global_cache = ctx.debug.global_cache;
b.resolver.opts.prefer_offline_install = (ctx.debug.offline_mode_setting orelse .online) == .offline;
b.resolver.opts.prefer_latest_install = (ctx.debug.offline_mode_setting orelse .online) == .latest;
b.resolver.opts.load_tsconfig_json = ctx.bundler_options.tsconfig.isEnabled();
b.options.global_cache = b.resolver.opts.global_cache;
b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install;
b.options.prefer_latest_install = b.resolver.opts.prefer_latest_install;
b.options.load_tsconfig_json = b.resolver.opts.load_tsconfig_json;
b.resolver.env_loader = b.env;
b.options.minify_identifiers = ctx.bundler_options.minify_identifiers;

View File

@@ -28,6 +28,7 @@ pub const BundlePackageOverride = bun.StringArrayHashMapUnmanaged(options.Bundle
const LoaderMap = bun.StringArrayHashMapUnmanaged(options.Loader);
const JSONParser = bun.JSON;
const Command = @import("cli.zig").Command;
const TSConfigOptions = Command.ContextData.BundlerOptions.TSConfigOptions;
const TOML = @import("./toml/toml_parser.zig").TOML;
// TODO: replace Api.TransformOptions with Bunfig
@@ -218,6 +219,15 @@ pub const Bunfig = struct {
this.bunfig.origin = try expr.data.e_string.string(allocator);
}
if (json.get("tsconfig")) |tsconfig| {
// TODO: support object for fine-grained control
try this.expect(tsconfig, .e_boolean);
this.ctx.bundler_options.tsconfig = if (tsconfig.asBool().?)
TSConfigOptions.on()
else
TSConfigOptions.off();
}
if (comptime cmd == .RunCommand or cmd == .AutoCommand) {
if (json.get("serve")) |expr| {
if (expr.get("port")) |port| {

View File

@@ -1562,6 +1562,28 @@ pub const Command = struct {
compile_target: Cli.CompileTarget = .{},
windows_hide_console: bool = false,
windows_icon: ?[]const u8 = null,
/// tsconfig options.
tsconfig: TSConfigOptions = TSConfigOptions.on(),
pub const TSConfigOptions = packed struct(u8) {
/// Use [TSConfig/ paths](https://www.typescriptlang.org/tsconfig/#paths)
/// when resolving imports.
tsconfig_paths: bool = true,
_: u7 = 0,
pub fn off() TSConfigOptions {
return .{ .tsconfig_paths = false };
}
pub fn on() TSConfigOptions {
return .{ .tsconfig_paths = true };
}
/// Returns `true` if any options are enabled
pub fn isEnabled(self: TSConfigOptions) bool {
return @as(u8, @bitCast(self)) > 0;
}
};
};
pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context {

View File

@@ -819,8 +819,9 @@ pub const RunCommand = struct {
this_transpiler.resolver.care_about_scripts = true;
this_transpiler.resolver.store_fd = store_root_fd;
this_transpiler.resolver.opts.load_tsconfig_json = true;
this_transpiler.options.load_tsconfig_json = true;
const should_load_tsconfig = ctx.bundler_options.tsconfig.isEnabled();
this_transpiler.resolver.opts.load_tsconfig_json = should_load_tsconfig;
this_transpiler.options.load_tsconfig_json = should_load_tsconfig;
this_transpiler.configureLinker();

View File

@@ -1627,8 +1627,8 @@ pub const Resolver = struct {
}
// First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file
if (dir_info.enclosing_tsconfig_json) |tsconfig| {
if (r.opts.load_tsconfig_json and dir_info.enclosing_tsconfig_json != null) {
const tsconfig = dir_info.enclosing_tsconfig_json.?;
// Try path substitutions first
if (tsconfig.paths.count() > 0) {
if (r.matchTSConfigPaths(tsconfig, import_path, kind)) |res| {
@@ -2095,6 +2095,7 @@ pub const Resolver = struct {
return .{ .not_found = {} };
}
fn dirInfoForResolution(
r: *ThisResolver,
dir_path_maybe_trail_slash: string,

View File

@@ -1,9 +1,19 @@
import { describe, it, expect } from "bun:test";
import { describe, it, beforeAll, expect, afterAll } from "bun:test";
import { mkdirSync, writeFileSync, rmdirSync } from "fs";
import { pathToFileURL } from "bun";
import { mkdirSync, writeFileSync } from "fs";
import { join, sep } from "path";
import { bunExe, bunEnv, tempDirWithFiles, joinP, isWindows } from "harness";
const env = { ...bunEnv };
beforeAll(() => {
for (const key in env) {
if (key.startsWith("BUN_DEBUG_") && key !== "BUN_DEBUG_QUIET_LOGS") {
delete env[key];
}
}
});
it("spawn test file", () => {
writePackageJSONImportsFixture();
writePackageJSONExportsFixture();
@@ -406,3 +416,114 @@ describe("NODE_PATH test", () => {
expect(stdout.toString().trim()).toBe("NODE_PATH from lib works");
});
});
/**
* When resolving imports, if `package.json` has `exports` fields that conflict
* with tsconfig paths, then `imports` should take precedence.
* notes:
* 1. self-referrential imports hit a different code path
* 2. resolve walks up the directory tree, finding the nearest tsconfig.json.
* 3. I *think* different logic happens when the resolved path (i.e. following symlinks)
* is in node_modules instead of in the project directory.
*
* All of this is to say yes, this is a complicated test, but it broke
* playwright ¯\_(ツ)_/¯
*/
describe("when both package.json imports and tsconfig.json paths are present", () => {
let dir: string;
beforeAll(() => {
dir = tempDirWithFiles("package-json-imports", {
"bunfig.toml": /* toml */ `
tsconfig = false
`,
"tsconfig.json": /* json */ `
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"foo/lib/*": ["./packages/foo/src/*"]
}
}
}
`,
"package.json": /* json */ `
{
"name": "root",
"private": true,
"version": "0.1.0",
"workspaces": ["packages/*"],
}
`,
packages: {
foo: {
"package.json": /* json */ `
{
"name": "foo",
"version": "0.1.0",
"main": "lib/target.js",
"exports": {
"./lib/target": "./lib/target.js",
"./lib/imported": "./lib/imported.js",
}
}
`,
lib: {
"target.js": /* js */ `module.exports.foo = require('./imported').foo;`,
"imported.js": /* js */ `module.exports.foo = 1;`,
},
src: {
"target.ts": /* ts */ `export {foo} from './imported';`,
// no imported
},
},
bar: {
"package.json": /* json */ `
{
"name": "bar",
"version": "0.1.0",
"dependencies": {
"foo": "*"
}
}
`,
src: {
"index.js": /* ts */ `const {foo} = require('foo/lib/target'); console.log(foo)`,
},
},
},
});
Bun.spawnSync([bunExe(), "install"], { cwd: dir, env, stdout: "inherit", stderr: "inherit" });
});
afterAll(() => {
rmdirSync(dir, { recursive: true });
});
it("when target is imported from package 'bar', imports from the actual lib directory", () => {
const { stdout, stderr, exitCode } = Bun.spawnSync(
[bunExe(), "--config=./bunfig.toml", "packages/bar/src/index.js"],
{
cwd: dir,
env,
stdout: "pipe",
stderr: "pipe",
},
);
if (exitCode !== 0) {
console.error(stderr.toString("utf8"));
}
expect(stdout.toString("utf8").trim()).toBe("1");
expect(exitCode).toBe(0);
});
it("when tsconfig-paths is not disabled in bunfig.toml, fails to find 'imported'", () => {
const { stderr, exitCode } = Bun.spawnSync([bunExe(), "src/index.js"], {
cwd: join(dir, "packages", "bar"),
env,
stdout: "pipe",
stderr: "pipe",
});
expect(exitCode).not.toBe(0);
expect(stderr.toString("utf8")).toContain("Cannot find module './imported'");
});
});