diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f7e75d204..84d1fcd95d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,8 +2,8 @@ cmake_minimum_required(VERSION 3.22) cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0067 NEW) -set(Bun_VERSION "1.0.13") -set(WEBKIT_TAG 5bd03f527e665621464d984a6b224bed1bafd968) +set(Bun_VERSION "1.0.14") +set(WEBKIT_TAG b72b839d2c18ae71952d215a9654797f044b67e7) set(BUN_WORKDIR "${CMAKE_CURRENT_BINARY_DIR}") message(STATUS "Configuring Bun ${Bun_VERSION} in ${BUN_WORKDIR}") diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 594eeb0954..8c50309df9 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -1836,8 +1836,7 @@ pub const ZigConsoleClient = struct { .failed = false, }; - var name_str = getObjectName(globalThis, value); - if (name_str.len > 0) { + if (getObjectName(globalThis, value)) |name_str| { writer.print("{} ", .{name_str}); } } @@ -1965,13 +1964,13 @@ pub const ZigConsoleClient = struct { }; } - fn getObjectName(globalThis: *JSC.JSGlobalObject, value: JSValue) ZigString { + fn getObjectName(globalThis: *JSC.JSGlobalObject, value: JSValue) ?ZigString { var name_str = ZigString.init(""); value.getClassName(globalThis, &name_str); - if (name_str.len > 0 and !strings.eqlComptime(name_str.slice(), "Object")) { + if (!name_str.eqlComptime("Object")) { return name_str; } - return ZigString.init(""); + return null; } extern fn JSC__JSValue__callCustomInspectFunction( @@ -2973,8 +2972,7 @@ pub const ZigConsoleClient = struct { else if (value.isCallable(this.globalThis.vm())) this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors) else { - var name_str = getObjectName(this.globalThis, value); - if (name_str.len > 0) { + if (getObjectName(this.globalThis, value)) |name_str| { writer.print("{} ", .{name_str}); } writer.writeAll("{}"); diff --git a/src/feature_flags.zig b/src/feature_flags.zig index a2503eafb5..f40ddb7207 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -178,3 +178,6 @@ pub const streaming_file_uploads_for_http_client = true; pub const concurrent_transpiler = true; pub const disable_on_windows_due_to_bugs = env.isWindows; + +// https://github.com/oven-sh/bun/issues/5426#issuecomment-1813865316 +pub const disable_auto_js_to_ts_in_node_modules = true; diff --git a/src/options.zig b/src/options.zig index bfc092b8c0..6655695dee 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1275,6 +1275,51 @@ const default_loader_ext = [_]string{ ".txt", ".text", }; +const node_modules_default_loader_ext_bun = [_]string{".node"}; +const node_modules_default_loader_ext = [_]string{ + ".jsx", + ".js", + ".cjs", + ".mjs", + ".ts", + ".mts", + ".toml", + ".txt", + ".json", + ".css", + ".tsx", + ".cts", + ".wasm", + ".text", +}; + +pub const ResolveFileExtensions = struct { + node_modules: Group = .{ + .esm = &BundleOptions.Defaults.NodeModules.ModuleExtensionOrder, + .default = &BundleOptions.Defaults.NodeModules.ExtensionOrder, + }, + default: Group = .{}, + + inline fn group(this: *const ResolveFileExtensions, is_node_modules: bool) *const Group { + return switch (is_node_modules) { + true => &this.node_modules, + false => &this.default, + }; + } + + pub fn kind(this: *const ResolveFileExtensions, kind_: bun.ImportKind, is_node_modules: bool) []const string { + return switch (kind_) { + .stmt, .entry_point, .dynamic => this.group(is_node_modules).esm, + else => this.group(is_node_modules).default, + }; + } + + pub const Group = struct { + esm: []const string = &BundleOptions.Defaults.ModuleExtensionOrder, + default: []const string = &BundleOptions.Defaults.ExtensionOrder, + }; +}; + pub fn loadersFromTransformOptions(allocator: std.mem.Allocator, _loaders: ?Api.LoaderMap, target: Target) !bun.StringArrayHashMap(Loader) { var input_loaders = _loaders orelse std.mem.zeroes(Api.LoaderMap); var loader_values = try allocator.alloc(Loader, input_loaders.loaders.len); @@ -1409,8 +1454,7 @@ pub const BundleOptions = struct { asset_naming: []const u8 = "", chunk_naming: []const u8 = "", public_path: []const u8 = "", - extension_order: []const string = &Defaults.ExtensionOrder, - esm_extension_order: []const string = &Defaults.ModuleExtensionOrder, + extension_order: ResolveFileExtensions = .{}, main_field_extension_order: []const string = &Defaults.MainFieldExtensionOrder, out_extensions: bun.StringHashMap(string), import_path_format: ImportPathFormat = ImportPathFormat.relative, @@ -1582,6 +1626,32 @@ pub const BundleOptions = struct { pub const CSSExtensionOrder = [_]string{ ".css", }; + + pub const NodeModules = struct { + pub const ExtensionOrder = [_]string{ + ".jsx", + ".cjs", + ".js", + ".mjs", + ".mts", + ".tsx", + ".ts", + ".cts", + ".json", + }; + + pub const ModuleExtensionOrder = [_]string{ + ".mjs", + ".jsx", + ".mts", + ".js", + ".cjs", + ".tsx", + ".ts", + ".cts", + ".json", + }; + }; }; pub fn fromApi( @@ -1621,7 +1691,7 @@ pub const BundleOptions = struct { } if (transform.extension_order.len > 0) { - opts.extension_order = transform.extension_order; + opts.extension_order.default.default = transform.extension_order; } if (transform.target) |t| { @@ -1666,7 +1736,8 @@ pub const BundleOptions = struct { opts.env.behavior = .load_all; if (transform.extension_order.len == 0) { // we must also support require'ing .node files - opts.extension_order = Defaults.ExtensionOrder ++ &[_][]const u8{".node"}; + opts.extension_order.default.default = comptime Defaults.ExtensionOrder ++ &[_][]const u8{".node"}; + opts.extension_order.node_modules.default = comptime Defaults.NodeModules.ExtensionOrder ++ &[_][]const u8{".node"}; } }, else => {}, diff --git a/src/resolver/dir_info.zig b/src/resolver/dir_info.zig index 92d6c2d75a..29f032b588 100644 --- a/src/resolver/dir_info.zig +++ b/src/resolver/dir_info.zig @@ -55,12 +55,19 @@ pub inline fn isNodeModules(this: *const DirInfo) bool { return this.flags.contains(.is_node_modules); } +/// Is this inside a "node_modules" directory? +pub inline fn isInsideNodeModules(this: *const DirInfo) bool { + return this.flags.contains(.inside_node_modules); +} + pub const Flags = enum { /// This directory is a node_modules directory is_node_modules, /// This directory has a node_modules subdirectory has_node_modules, + inside_node_modules, + pub const Set = std.enums.EnumSet(Flags); }; diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index a507d06802..874f4cb798 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -579,7 +579,7 @@ pub const Resolver = struct { .timer = Timer.start() catch @panic("Timer fail"), .fs = _fs, .log = log, - .extension_order = opts.extension_order, + .extension_order = opts.extension_order.default.default, .care_about_browser_field = opts.target.isWebLike(), }; } @@ -771,8 +771,8 @@ pub const Resolver = struct { defer r.extension_order = original_order; r.extension_order = switch (kind) { .url, .at_conditional, .at => options.BundleOptions.Defaults.CSSExtensionOrder[0..], - .entry_point, .stmt, .dynamic => r.opts.esm_extension_order, - else => r.opts.extension_order, + .entry_point, .stmt, .dynamic => r.opts.extension_order.default.esm, + else => r.opts.extension_order.default.default, }; if (FeatureFlags.tracing) { @@ -1211,6 +1211,13 @@ pub const Resolver = struct { } if (check_relative) { + var prev_extension_order = r.extension_order; + defer { + r.extension_order = prev_extension_order; + } + if (strings.pathContainsNodeModulesFolder(abs_path)) { + r.extension_order = r.opts.extension_order.kind(kind, true); + } if (r.loadAsFileOrDirectory(abs_path, kind)) |res| { check_package = false; result = Result{ @@ -1572,6 +1579,8 @@ pub const Resolver = struct { if (r.debug_logs) |*debug| { debug.addNoteFmt("Checking for a package in the directory \"{s}\"", .{abs_path}); } + var prev_extension_order = r.extension_order; + defer r.extension_order = prev_extension_order; if (esm_) |esm| { const abs_package_path = brk: { @@ -1580,6 +1589,11 @@ pub const Resolver = struct { }; if (r.dirInfoCached(abs_package_path) catch null) |pkg_dir_info| { + r.extension_order = switch (kind) { + .url, .at_conditional, .at => options.BundleOptions.Defaults.CSSExtensionOrder[0..], + else => r.opts.extension_order.kind(kind, true), + }; + if (pkg_dir_info.package_json) |package_json| { if (package_json.exports) |exports_map| { @@ -2163,7 +2177,7 @@ pub const Resolver = struct { break :brk r.fs.absBuf(&parts, bufs(.esm_absolute_package_path_joined)); }; - var missing_suffix: string = undefined; + var missing_suffix: string = ""; switch (esm_resolution.status) { .Exact, .ExactEndsWithStar => { @@ -2175,11 +2189,12 @@ pub const Resolver = struct { esm_resolution.status = .ModuleNotFound; return null; }; - const base = std.fs.path.basename(abs_esm_path); const extension_order = if (kind == .at or kind == .at_conditional) r.extension_order else - r.opts.extension_order; + r.opts.extension_order.kind(kind, resolved_dir_info.isInsideNodeModules()); + + const base = std.fs.path.basename(abs_esm_path); const entry_query = entries.get(base) orelse { const ends_with_star = esm_resolution.status == .ExactEndsWithStar; esm_resolution.status = .ModuleNotFound; @@ -2219,9 +2234,9 @@ pub const Resolver = struct { bun.copy(u8, file_name[index.len..], ext); const index_query = dir_entries.get(file_name); if (index_query != null and index_query.?.entry.kind(&r.fs.fs, r.store_fd) == .file) { - missing_suffix = std.fmt.allocPrint(r.allocator, "/{s}", .{file_name}) catch unreachable; - // defer r.allocator.free(missing_suffix); if (r.debug_logs) |*debug| { + missing_suffix = std.fmt.allocPrint(r.allocator, "/{s}", .{file_name}) catch unreachable; + defer r.allocator.free(missing_suffix); const parts = [_]string{ package_json.name, package_subpath }; debug.addNoteFmt("The import {s} is missing the suffix {s}", .{ ResolvePath.join(parts, .auto), missing_suffix }); } @@ -3586,7 +3601,7 @@ pub const Resolver = struct { // https://github.com/microsoft/TypeScript/issues/4595 if (strings.lastIndexOfChar(base, '.')) |last_dot| { const ext = base[last_dot..base.len]; - if (strings.eqlComptime(ext, ".js") or strings.eqlComptime(ext, ".jsx")) { + if ((strings.eqlComptime(ext, ".js") or strings.eqlComptime(ext, ".jsx") and (!FeatureFlags.disable_auto_js_to_ts_in_node_modules or !strings.pathContainsNodeModulesFolder(path)))) { const segment = base[0..last_dot]; var tail = bufs(.load_as_file)[path.len - base.len ..]; bun.copy(u8, tail, segment); @@ -3739,14 +3754,14 @@ pub const Resolver = struct { } // } - if (parent != null) { + if (parent) |parent_| { // Propagate the browser scope into child directories - info.enclosing_browser_scope = parent.?.enclosing_browser_scope; - info.package_json_for_browser_field = parent.?.package_json_for_browser_field; - info.enclosing_tsconfig_json = parent.?.enclosing_tsconfig_json; + info.enclosing_browser_scope = parent_.enclosing_browser_scope; + info.package_json_for_browser_field = parent_.package_json_for_browser_field; + info.enclosing_tsconfig_json = parent_.enclosing_tsconfig_json; - if (parent.?.package_json) |parent_package_json| { + if (parent_.package_json) |parent_package_json| { // https://github.com/oven-sh/bun/issues/229 if (parent_package_json.name.len > 0 or r.care_about_bin_folder) { info.enclosing_package_json = parent_package_json; @@ -3757,12 +3772,12 @@ pub const Resolver = struct { } } - info.enclosing_package_json = info.enclosing_package_json orelse parent.?.enclosing_package_json; - info.package_json_for_dependencies = info.package_json_for_dependencies orelse parent.?.package_json_for_dependencies; + info.enclosing_package_json = info.enclosing_package_json orelse parent_.enclosing_package_json; + info.package_json_for_dependencies = info.package_json_for_dependencies orelse parent_.package_json_for_dependencies; // Make sure "absRealPath" is the real path of the directory (resolving any symlinks) if (!r.opts.preserve_symlinks) { - if (parent.?.getEntries(r.generation)) |parent_entries| { + if (parent_.getEntries(r.generation)) |parent_entries| { if (parent_entries.get(base)) |lookup| { if (entries.fd != 0 and lookup.entry.cache.fd == 0 and r.store_fd) lookup.entry.cache.fd = entries.fd; const entry = lookup.entry; @@ -3773,7 +3788,7 @@ pub const Resolver = struct { logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable); } info.abs_real_path = symlink; - } else if (parent.?.abs_real_path.len > 0) { + } else if (parent_.abs_real_path.len > 0) { // this might leak a little i'm not sure const parts = [_]string{ parent.?.abs_real_path, base }; symlink = r.fs.dirname_store.append(string, r.fs.absBuf(&parts, bufs(.dir_info_uncached_filename))) catch unreachable; @@ -3787,6 +3802,10 @@ pub const Resolver = struct { } } } + + if (parent_.isNodeModules() or parent_.isInsideNodeModules()) { + info.flags.setPresent(.inside_node_modules, true); + } } // Record if this directory has a package.json file diff --git a/src/string_immutable.zig b/src/string_immutable.zig index dcf87a89f7..6f1b9bb40f 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -5048,3 +5048,7 @@ pub fn mustEscapeYAMLString(contents: []const u8) bool { else => true, }; } + +pub fn pathContainsNodeModulesFolder(path: []const u8) bool { + return strings.contains(path, comptime std.fs.path.sep_str ++ "node_modules" ++ std.fs.path.sep_str); +} diff --git a/test/js/bun/resolve/chooses-ts.js b/test/js/bun/resolve/chooses-ts.js new file mode 100644 index 0000000000..cb6b297f28 --- /dev/null +++ b/test/js/bun/resolve/chooses-ts.js @@ -0,0 +1 @@ +export const pass = false; diff --git a/test/js/bun/resolve/chooses-ts.ts b/test/js/bun/resolve/chooses-ts.ts new file mode 100644 index 0000000000..0b5d2b5c96 --- /dev/null +++ b/test/js/bun/resolve/chooses-ts.ts @@ -0,0 +1 @@ +export const pass = true; diff --git a/test/js/bun/resolve/resolve-ts.test.ts b/test/js/bun/resolve/resolve-ts.test.ts new file mode 100644 index 0000000000..6c130a59eb --- /dev/null +++ b/test/js/bun/resolve/resolve-ts.test.ts @@ -0,0 +1,186 @@ +import { test, expect, describe } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +import * as Chooses from "./chooses-ts"; + +test(".ts file is chosen over .js file locally", () => { + expect(Chooses.pass).toBeTrue(); +}); + +// const isTestingInNode = { type: "module" }; +const isTestingInNode = {}; + +// The idea with this test is: +// - In node_modules, prefer non-ts files over ts files +// - Outside node_modules, prefer ts files over non-ts files +// - ./dir/*.js should NOT be resolve to ./dir/*.ts +// - "package/dir" should resolve to "package/dir/index.ts" if "package/dir/index.js" does NOT exist +// - "package/dir" should resolve to "package/dir/index.js" if "package/dir/index.ts" does exist +// - it should work when no node_modules/package/package.json exists + +// A good package to try this on is `capnp-ts`: +// https://github.com/oven-sh/bun/issues/5426 + +function runTest( + { withPackageJSON = false, withPackageJSONExports = false, type = "", jsFile = false, asDir = false } = {} as { + withPackageJSON?: boolean; + withPackageJSONExports?: boolean; + type?: string; + jsFile?: boolean; + asDir?: boolean; + }, +) { + const typeFlag = type ? { type } : {}; + const exportsObject = withPackageJSONExports + ? { + exports: { + ".": (asDir ? "./dir/index." : "./index.") + (jsFile ? "js" : "ts"), + ...(asDir ? { "./*": "./dir/*.js" } : {}), + }, + } + : {}; + + const files: Record = {}; + if (jsFile) { + files[asDir ? "node_modules/abc/dir/index.js" : "node_modules/abc/index.js"] = + "export * from './sibling'; export const foo = 1;"; + files[asDir ? "node_modules/abc/dir/sibling.js" : "node_modules/abc/sibling.js"] = "export const sibling = 1;"; + } + + if (withPackageJSON) { + files["node_modules/abc/package.json"] = JSON.stringify( + { + name: "abc", + ...exportsObject, + ...typeFlag, + }, + null, + 2, + ); + } + + files["package.json"] = JSON.stringify( + { + name: "myapp", + ...isTestingInNode, + }, + null, + 2, + ); + + let extra = ""; + + if (asDir && withPackageJSONExports && jsFile) { + extra = ` + + import * as index from "abc/index"; + + if (index.foo !== 1) { + throw new Error("Unexpected value\\n" + JSON.stringify(index, null, 2)); + } + `; + } + + let entry = ""; + if (asDir && !withPackageJSONExports) { + entry = ` + import * as pkg from "abc/dir"; + + if (${jsFile ? "pkg.foo !== 1" : "pkg.bar !== 2"}) { + throw new Error("Unexpected value\\n" + JSON.stringify(pkg, null, 2)); + } + + if (${jsFile ? "pkg.sibling !== 1" : "pkg.sibling !== 2"}) { + throw new Error("Unexpected value\\n" + JSON.stringify(pkg, null, 2)); + } + `; + + if (!jsFile) { + entry += ` + import * as pkg2 from "abc/dir/index"; + import * as pkg3 from "abc/dir/index.js"; + import * as pkg4 from "abc/dir/index.ts"; + + if (pkg2.bar !== 2) { + throw new Error("Unexpected value\\n" + JSON.stringify(pkg2, null, 2)); + } + + if (pkg3.bar !== 2) { + throw new Error("Unexpected value\\n" + JSON.stringify(pkg3, null, 2)); + } + + if (pkg4.bar !== 2) { + throw new Error("Unexpected value\\n" + JSON.stringify(pkg4, null, 2)); + } + + if (pkg2 !== pkg3 || pkg3 !== pkg4) { + throw new Error("Unexpected value\\n" + JSON.stringify(pkg2, null, 2)); + } + + `; + } + } else { + entry = ` + import * as pkg from "abc"; + + if (${jsFile ? "pkg.foo !== 1" : "pkg.bar !== 2"}) { + throw new Error("Unexpected value\\n" + JSON.stringify(pkg, null, 2)); + } + `; + } + + const dirname = tempDirWithFiles("resolve" + ((Math.random() * 10000) | 0).toString(16), { + ...files, + [`node_modules/abc${asDir ? "/dir" : ""}/index.ts`]: ` + ${asDir ? `export * from "./sibling";` : ""} + export const bar = 2; +`, + [`node_modules/abc${asDir ? "/dir" : ""}/sibling.ts`]: "export const sibling = 2;", + "index.js": entry + extra, + }); + + const { exitCode } = Bun.spawnSync({ + cmd: [bunExe(), "index.js"], + env: bunEnv, + cwd: dirname, + stderr: "inherit", + stdout: "inherit", + stdin: "inherit", + }); + + expect(exitCode).toBe(0); +} + +for (let withPackageJSON of [true, false]) { + if (withPackageJSON) { + describe("with package.json", () => { + for (let withPackageJSONExports of [true, false]) { + for (let asDir of [true, false] as const) { + const callback = () => { + for (let type of ["module", "commonjs", ""]) { + for (let jsFile of [true, false]) { + test(`resolve ${withPackageJSONExports ? "with" : "without"} package.json exports${ + type ? " type:" + type : " " + } ${jsFile ? "with" : "without"} .js file`, () => { + runTest({ withPackageJSON, withPackageJSONExports, type, jsFile, asDir }); + }); + } + } + }; + + if (asDir) { + describe("as dir", callback); + } else { + callback(); + } + } + } + }); + } else { + for (let jsFile of [true, false]) { + test(`resolve without package.json and ${jsFile ? "with" : "without"} .js file`, () => { + runTest({ withPackageJSON: false, jsFile, asDir: false }); + }); + } + } +}