diff options
author | 2021-05-12 20:33:58 -0700 | |
---|---|---|
committer | 2021-05-12 20:33:58 -0700 | |
commit | f12ed9904b03e11f755dce7b614925ea087f40da (patch) | |
tree | cfbbcab5ee4931d67b8e15d8175291675019ab92 /src/resolver/resolver.zig | |
parent | 1010bae1a350d12f7db49b8ca7f94aa748790b77 (diff) | |
download | bun-f12ed9904b03e11f755dce7b614925ea087f40da.tar.gz bun-f12ed9904b03e11f755dce7b614925ea087f40da.tar.zst bun-f12ed9904b03e11f755dce7b614925ea087f40da.zip |
okay I think that's most of resolving packages/imports algorithm!!!
Former-commit-id: 80037859ec5236e13314a336e28d5f46a96c3300
Diffstat (limited to 'src/resolver/resolver.zig')
-rw-r--r-- | src/resolver/resolver.zig | 835 |
1 files changed, 819 insertions, 16 deletions
diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 88bc13cd5..118a6d45c 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -2,7 +2,7 @@ usingnamespace @import("../global.zig"); const ast = @import("../import_record.zig"); const logger = @import("../logger.zig"); const options = @import("../options.zig"); -const fs = @import("../fs.zig"); +const Fs = @import("../fs.zig"); const std = @import("std"); const cache = @import("../cache.zig"); @@ -12,7 +12,7 @@ usingnamespace @import("./data_url.zig"); const StringBoolMap = std.StringHashMap(bool); -const Path = fs.Path; +const Path = Fs.Path; pub const SideEffectsData = struct { source: *logger.Source, range: logger.Range, @@ -31,17 +31,23 @@ pub const DirInfo = struct { enclosing_browser_scope: ?*DirInfo = null, abs_path: string, - entries: fs.FileSystem.DirEntry, + entries: Fs.FileSystem.DirEntry, has_node_modules: bool = false, // Is there a "node_modules" subdirectory? package_json: ?*PackageJSON = null, // Is there a "package.json" file? tsconfig_json: ?*TSConfigJSON = null, // Is there a "tsconfig.json" file in this directory or a parent directory? abs_real_path: string = "", // If non-empty, this is the real absolute path resolving any symlinks }; +pub const TemporaryBuffer = struct { + pub var ExtensionPathBuf = std.mem.zeroes([512]u8); + pub var TSConfigMatchStarBuf = std.mem.zeroes([512]u8); + pub var TSConfigMatchPathBuf = std.mem.zeroes([512]u8); + pub var TSConfigMatchFullBuf = std.mem.zeroes([512]u8); +}; pub const Resolver = struct { opts: options.BundleOptions, - fs: *fs.FileSystem, + fs: *Fs.FileSystem, log: *logger.Log, allocator: *std.mem.Allocator, @@ -96,7 +102,7 @@ pub const Resolver = struct { pub fn init1( allocator: *std.mem.Allocator, log: *logger.Log, - _fs: *fs.FileSystem, + _fs: *Fs.FileSystem, opts: options.BundleOptions, ) Resolver { return Resolver{ @@ -160,6 +166,25 @@ pub const Resolver = struct { pub const PathPair = struct { primary: Path, secondary: ?Path = null, + + pub const Iter = struct { + index: u2, + ctx: *PathPair, + pub fn next(i: *Iter) ?Path { + const ind = i.index; + i.index += 1; + + switch (ind) { + 0 => return i.ctx.primary, + 1 => return i.ctx.secondary, + else => return null, + } + } + }; + + pub fn iter(p: *PathPair) Iter { + return Iter{ .ctx = p, .index = 0 }; + } }; pub const Result = struct { @@ -169,7 +194,7 @@ pub const Resolver = struct { is_external: bool = false, - diff_case: ?fs.FileSystem.Entry.Lookup.DifferentCase = null, + diff_case: ?Fs.FileSystem.Entry.Lookup.DifferentCase = null, // If present, any ES6 imports to this file can be considered to have no side // effects. This means they should be removed if unused. @@ -309,10 +334,10 @@ pub const Resolver = struct { return result; } - pub fn resolveWithoutSymlinks(r: *Resolver, source_dir: string, import_path: string, kind: ast.ImportKind) !Result { + pub fn resolveWithoutSymlinks(r: *Resolver, source_dir: string, import_path: string, kind: ast.ImportKind) !?Result { // This implements the module resolution algorithm from node.js, which is // described here: https://nodejs.org/api/modules.html#modules_all_together - var result: Result = undefined; + var result: Result = Result{ .path_pair = PathPair{ .primary = Path.init("") } }; // Return early if this is already an absolute path. In addition to asking // the file system whether this is an absolute path, we also explicitly check @@ -333,16 +358,264 @@ pub const Resolver = struct { const dir_info: *DirInfo = _dir_info; if (dir_info.tsconfig_json) |tsconfig| { if (tsconfig.paths.count() > 0) { - const res = r.matchTSConfigPaths(tsconfig, import_path, kind); - return Result{ .path_pair = res.path_pair, .diff_case = res.diff_case }; + if (r.matchTSConfigPaths(tsconfig, import_path, kind)) |res| { + return Result{ .path_pair = res.path_pair, .diff_case = res.diff_case }; + } + } + } + } + + if (r.opts.external.abs_paths.count() > 0 and r.opts.external.abs_paths.exists(import_path)) { + // If the string literal in the source text is an absolute path and has + // been marked as an external module, mark it as *not* an absolute path. + // That way we preserve the literal text in the output and don't generate + // a relative path from the output directory to that path. + if (r.debug_logs) |*debug| { + debug.addNoteFmt("The path \"{s}\" is marked as external by the user", .{import_path}) catch {}; + } + + return Result{ + .path_pair = .{ .primary = Path.init(import_path) }, + .is_external = true, + }; + } + + // Run node's resolution rules (e.g. adding ".js") + if (r.loadAsFileOrDirectory(import_path, kind)) |entry| { + return Result{ .path_pair = entry.path_pair, .diff_case = entry.diff_case }; + } + + return null; + } + + // Check both relative and package paths for CSS URL tokens, with relative + // paths taking precedence over package paths to match Webpack behavior. + const is_package_path = isPackagePath(import_path); + var check_relative = !is_package_path or kind == .url; + var check_package = is_package_path; + + if (check_relative) { + const parts = [_]string{ source_dir, import_path }; + const abs_path = std.fs.path.join(r.allocator, &parts) catch unreachable; + + if (r.opts.external.abs_paths.count() > 0 and r.opts.external.abs_paths.exists(abs_path)) { + // If the string literal in the source text is an absolute path and has + // been marked as an external module, mark it as *not* an absolute path. + // That way we preserve the literal text in the output and don't generate + // a relative path from the output directory to that path. + if (r.debug_logs) |*debug| { + debug.addNoteFmt("The path \"{s}\" is marked as external by the user", .{abs_path}) catch {}; + } + + return Result{ + .path_pair = .{ .primary = Path.init(abs_path) }, + .is_external = true, + }; + } + + // Check the "browser" map for the first time (1 out of 2) + if (r.dirInfoCached(std.fs.path.dirname(abs_path) orelse unreachable) catch null) |_import_dir_info| { + if (_import_dir_info.enclosing_browser_scope) |import_dir_info| { + if (import_dir_info.package_json) |pkg| { + const pkg_json_dir = std.fs.path.dirname(pkg.source.key_path.text) orelse unreachable; + + const rel_path = try std.fs.path.relative(r.allocator, pkg_json_dir, abs_path); + if (r.checkBrowserMap(pkg, rel_path)) |remap| { + // Is the path disabled? + if (remap.len == 0) { + var _path = Path.init(abs_path); + _path.is_disabled = true; + return Result{ + .path_pair = PathPair{ + .primary = _path, + }, + }; + } + + if (r.resolveWithoutRemapping(import_dir_info, remap, kind)) |_result| { + result = Result{ .path_pair = _result.path_pair, .diff_case = _result.diff_case }; + check_relative = false; + check_package = false; + } + } + } + } + } + + if (check_relative) { + if (r.loadAsFileOrDirectory(abs_path, kind)) |res| { + check_package = false; + result = Result{ .path_pair = res.path_pair, .diff_case = res.diff_case }; + } else if (!check_package) { + return null; + } + } + + if (check_package) { + // Check for external packages first + if (r.opts.external.node_modules.count() > 0) { + var query = import_path; + while (true) { + if (r.opts.external.node_modules.exists(query)) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("The path \"{s}\" was marked as external by the user", .{query}) catch {}; + } + return Result{ + .path_pair = .{ .primary = Path.init(query) }, + .is_external = true, + }; + } + + // If the module "foo" has been marked as external, we also want to treat + // paths into that module such as "foo/bar" as external too. + var slash = strings.lastIndexOfChar(query, '/') orelse break; + query = query[0..slash]; } } + + const source_dir_info = (r.dirInfoCached(source_dir) catch null) orelse return null; + + // Support remapping one package path to another via the "browser" field + if (source_dir_info.enclosing_browser_scope) |browser_scope| { + if (browser_scope.package_json) |package_json| { + if (r.checkBrowserMap(package_json, import_path)) |remapped| { + if (remapped.len == 0) { + // "browser": {"module": false} + if (r.loadNodeModules(import_path, kind, source_dir_info)) |node_module| { + var pair = node_module.path_pair; + pair.primary.is_disabled = true; + if (pair.secondary != null) { + pair.secondary.?.is_disabled = true; + } + return Result{ .path_pair = pair, .diff_case = node_module.diff_case }; + } + } else { + var primary = Path.init(import_path); + primary.is_disabled = true; + return Result{ + .path_pair = PathPair{ .primary = primary }, + // this might not be null? i think it is + .diff_case = null, + }; + } + } + } + } + + if (r.resolveWithoutRemapping(source_dir_info, import_path, kind)) |res| { + result = Result{ .path_pair = res.path_pair, .diff_case = res.diff_case }; + } else { + // Note: node's "self references" are not currently supported + return null; + } + } + } + + var iter = result.path_pair.iter(); + while (iter.next()) |*path| { + const dirname = std.fs.path.dirname(path.text) orelse continue; + const base_dir_info = ((r.dirInfoCached(dirname) catch null)) orelse continue; + const dir_info = base_dir_info.enclosing_browser_scope orelse continue; + const pkg_json = dir_info.package_json orelse continue; + const rel_path = std.fs.path.relative(r.allocator, pkg_json.source.key_path.text, path.text) catch continue; + if (r.checkBrowserMap(pkg_json, rel_path)) |remapped| { + if (remapped.len == 0) { + r.allocator.free(rel_path); + path.is_disabled = true; + } else if (r.resolveWithoutRemapping(dir_info, remapped, kind)) |remapped_result| { + switch (iter.index) { + 0 => { + result.path_pair.primary = remapped_result.path_pair.primary; + }, + else => { + result.path_pair.secondary = remapped_result.path_pair.primary; + }, + } + } else { + r.allocator.free(rel_path); + return null; + } + } else { + r.allocator.free(rel_path); } } return result; } + pub fn loadNodeModules(r: *Resolver, import_path: string, kind: ast.ImportKind, _dir_info: *DirInfo) ?MatchResult { + var dir_info = _dir_info; + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Searching for {s} in \"node_modules\" directories starting from \"{s}\"", .{ import_path, dir_info.abs_path }) catch {}; + debug.increaseIndent() catch {}; + } + + defer { + if (r.debug_logs) |*debug| { + debug.decreaseIndent() catch {}; + } + } + + // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file + + if (dir_info.tsconfig_json) |tsconfig| { + // Try path substitutions first + if (tsconfig.paths.count() > 0) { + if (r.matchTSConfigPaths(tsconfig, import_path, kind)) |res| { + return res; + } + } + + // Try looking up the path relative to the base URL + if (tsconfig.base_url) |base| { + const paths = [_]string{ base, import_path }; + const abs = std.fs.path.join(r.allocator, &paths) catch unreachable; + + if (r.loadAsFileOrDirectory(abs, kind)) |res| { + return res; + } + r.allocator.free(abs); + } + } + + // Then check for the package in any enclosing "node_modules" directories + while (true) { + // Skip directories that are themselves called "node_modules", since we + // don't ever want to search for "node_modules/node_modules" + if (dir_info.has_node_modules) { + var _paths = [_]string{ dir_info.abs_path, "node_modules", import_path }; + const abs_path = std.fs.path.join(r.allocator, &_paths) catch unreachable; + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Checking for a package in the directory \"{s}\"", .{abs_path}) catch {}; + } + + // TODO: esm "exports" field goes here!!! Here!! + + if (r.loadAsFileOrDirectory(abs_path, kind)) |res| { + return res; + } + r.allocator.free(abs_path); + } + + dir_info = dir_info.parent orelse break; + } + + // Mostly to cut scope, we don't resolve `NODE_PATH` environment variable. + // But also: https://github.com/nodejs/node/issues/38128#issuecomment-814969356 + + return null; + } + + pub fn resolveWithoutRemapping(r: *Resolver, source_dir_info: *DirInfo, import_path: string, kind: ast.ImportKind) ?MatchResult { + if (isPackagePath(import_path)) { + return r.loadNodeModules(import_path, kind, source_dir_info); + } else { + const paths = [_]string{ source_dir_info.abs_path, import_path }; + var resolved = std.fs.path.join(r.allocator, &paths) catch unreachable; + return r.loadAsFileOrDirectory(resolved, kind); + } + } + pub const TSConfigExtender = struct { visited: *StringBoolMap, file_dir: string, @@ -433,16 +706,546 @@ pub const Resolver = struct { pub const MatchResult = struct { path_pair: PathPair, - ok: bool = false, - diff_case: ?fs.FileSystem.Entry.Lookup.DifferentCase = null, + diff_case: ?Fs.FileSystem.Entry.Lookup.DifferentCase = null, }; - pub fn matchTSConfigPaths(r: *Resolver, tsconfig: *TSConfigJSON, path: string, kind: ast.ImportKind) MatchResult { - Global.notimpl(); + // This closely follows the behavior of "tryLoadModuleUsingPaths()" in the + // official TypeScript compiler + pub fn matchTSConfigPaths(r: *Resolver, tsconfig: *TSConfigJSON, path: string, kind: ast.ImportKind) ?MatchResult { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Matching \"{s}\" against \"paths\" in \"{s}\"", .{ path, tsconfig.abs_path }) catch unreachable; + } + + var abs_base_url = tsconfig.base_url_for_paths; + + // The explicit base URL should take precedence over the implicit base URL + // if present. This matters when a tsconfig.json file overrides "baseUrl" + // from another extended tsconfig.json file but doesn't override "paths". + if (tsconfig.base_url) |base| { + abs_base_url = base; + } + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Using \"{s}\" as \"baseURL\"", .{abs_base_url}) catch unreachable; + } + + // Check for exact matches first + { + var iter = tsconfig.paths.iterator(); + while (iter.next()) |entry| { + const key = entry.key; + + if (strings.eql(key, path)) { + for (entry.value) |original_path| { + var absolute_original_path = original_path; + var was_alloc = false; + + if (!std.fs.path.isAbsolute(absolute_original_path)) { + const parts = [_]string{ abs_base_url, original_path }; + absolute_original_path = std.fs.path.join(r.allocator, &parts) catch unreachable; + was_alloc = true; + } + + if (r.loadAsFileOrDirectory(absolute_original_path, kind)) |res| { + return res; + } else if (was_alloc) { + r.allocator.free(absolute_original_path); + } + } + + return null; + } + } + } + + const TSConfigMatch = struct { + prefix: string, + suffix: string, + original_paths: []string, + }; + + var longest_match: TSConfigMatch = undefined; + var longest_match_prefix_length: i32 = -1; + var longest_match_suffix_length: i32 = -1; + + var iter = tsconfig.paths.iterator(); + while (iter.next()) |entry| { + const key = entry.key; + const original_paths = entry.value; + + if (strings.indexOfChar(key, '*')) |star_index| { + const prefix = key[0..star_index]; + const suffix = key[star_index..key.len]; + + // Find the match with the longest prefix. If two matches have the same + // prefix length, pick the one with the longest suffix. This second edge + // case isn't handled by the TypeScript compiler, but we handle it + // because we want the output to always be deterministic and Go map + // iteration order is deliberately non-deterministic. + if (strings.startsWith(path, prefix) and strings.endsWith(path, suffix) and (prefix.len > longest_match_prefix_length or (prefix.len == longest_match_prefix_length and suffix.len > longest_match_suffix_length))) { + longest_match_prefix_length = @intCast(i32, prefix.len); + longest_match_suffix_length = @intCast(i32, suffix.len); + longest_match = TSConfigMatch{ .prefix = prefix, .suffix = suffix, .original_paths = original_paths }; + } + } + } + + // If there is at least one match, only consider the one with the longest + // prefix. This matches the behavior of the TypeScript compiler. + if (longest_match_prefix_length > -1) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found a fuzzy match for \"{s}*{s}\" in \"paths\"", .{ longest_match.prefix, longest_match.suffix }) catch unreachable; + } + + for (longest_match.original_paths) |original_path| { + // Swap out the "*" in the original path for whatever the "*" matched + const matched_text = path[longest_match.prefix.len .. path.len - longest_match.suffix.len]; + + std.mem.copy( + u8, + &TemporaryBuffer.TSConfigMatchPathBuf, + original_path, + ); + var start: usize = 0; + var total_length: usize = 0; + const star = std.mem.indexOfScalar(u8, original_path, '*') orelse unreachable; + total_length = star; + std.mem.copy(u8, &TemporaryBuffer.TSConfigMatchPathBuf, original_path[0..total_length]); + start = total_length; + total_length += matched_text.len; + std.mem.copy(u8, TemporaryBuffer.TSConfigMatchPathBuf[start..total_length], matched_text); + start = total_length; + + total_length += original_path.len - star + 1; // this might be an off by one. + std.mem.copy(u8, TemporaryBuffer.TSConfigMatchPathBuf[start..TemporaryBuffer.TSConfigMatchPathBuf.len], original_path[star..original_path.len]); + const region = TemporaryBuffer.TSConfigMatchPathBuf[0..total_length]; + + // Load the original path relative to the "baseUrl" from tsconfig.json + var absolute_original_path = region; + + var did_allocate = false; + if (!std.fs.path.isAbsolute(region)) { + const paths = [_]string{ abs_base_url, original_path }; + absolute_original_path = std.fs.path.join(r.allocator, &paths) catch unreachable; + did_allocate = true; + } else { + absolute_original_path = std.mem.dupe(r.allocator, u8, region) catch unreachable; + } + + if (r.loadAsFileOrDirectory(absolute_original_path, kind)) |res| { + return res; + } + } + } + + return null; + } + + pub const LoadResult = struct { + path: string, + diff_case: ?Fs.FileSystem.Entry.Lookup.DifferentCase, + }; + + pub fn checkBrowserMap(r: *Resolver, pkg: *PackageJSON, input_path: string) ?string { + // Normalize the path so we can compare against it without getting confused by "./" + var cleaned = Path.normalizeNoAlloc(input_path, true); + const original_cleaned = cleaned; + + if (cleaned.len == 1 and cleaned[0] == '.') { + // No bundler supports remapping ".", so we don't either + return null; + } + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Checking for \"{s}\" in the \"browser\" map in \"{s}\"", .{ input_path, pkg.source.path.text }) catch {}; + } + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Checking for \"{s}\" ", .{cleaned}) catch {}; + } + var remapped = pkg.browser_map.get(cleaned); + if (remapped == null) { + for (r.opts.extension_order) |ext| { + std.mem.copy(u8, &TemporaryBuffer.ExtensionPathBuf, cleaned); + std.mem.copy(u8, TemporaryBuffer.ExtensionPathBuf[cleaned.len .. cleaned.len + ext.len], ext); + const new_path = TemporaryBuffer.ExtensionPathBuf[0 .. cleaned.len + ext.len]; + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Checking for \"{s}\" ", .{new_path}) catch {}; + } + if (pkg.browser_map.get(new_path)) |_remapped| { + remapped = _remapped; + cleaned = new_path; + break; + } + } + } + + if (remapped) |remap| { + // "" == disabled, {"browser": { "file.js": false }} + if (remap.len == 0 or (remap.len == 1 and remap[0] == '.')) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found \"{s}\" marked as disabled", .{remap}) catch {}; + } + return remap; + } + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found \"{s}\" remapped to \"{s}\"", .{ original_cleaned, remap }) catch {}; + } + + // Only allocate on successful remapping. + return r.allocator.dupe(u8, remap) catch unreachable; + } + + return null; + } + + pub fn loadFromMainField(r: *Resolver, path: string, dir_info: *DirInfo, _field_rel_path: string, field: string, extension_order: []const string) ?MatchResult { + var field_rel_path = _field_rel_path; + // Is this a directory? + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found main field \"{s}\" with path \"{s}\"", .{ field, field_rel_path }) catch {}; + debug.increaseIndent() catch {}; + } + + defer { + if (r.debug_logs) |*debug| { + debug.decreaseIndent() catch {}; + } + } + + // Potentially remap using the "browser" field + if (dir_info.enclosing_browser_scope) |browser_scope| { + if (browser_scope.package_json) |browser_json| { + if (r.checkBrowserMap(browser_json, field_rel_path)) |remap| { + // Is the path disabled? + if (remap.len == 0) { + const paths = [_]string{ path, field_rel_path }; + const new_path = std.fs.path.join(r.allocator, &paths) catch unreachable; + var _path = Path.init(new_path); + _path.is_disabled = true; + return MatchResult{ + .path_pair = PathPair{ + .primary = _path, + }, + }; + } + + field_rel_path = remap; + } + } + } + + return r.loadAsIndex(dir_info, path, extension_order); + } + + pub fn loadAsIndex(r: *Resolver, dir_info: *DirInfo, path: string, extension_order: []const string) ?MatchResult { + var rfs = &r.fs.fs; + // Try the "index" file with extensions + for (extension_order) |ext| { + var base = TemporaryBuffer.ExtensionPathBuf[0 .. "index".len + ext.len]; + base[0.."index".len].* = "index".*; + std.mem.copy(u8, base["index".len..base.len], ext); + + if (dir_info.entries.get(base)) |lookup| { + if (lookup.entry.kind(rfs) == .file) { + const parts = [_]string{ path, base }; + const out_buf = std.fs.path.join(r.allocator, &parts) catch unreachable; + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found file: \"{s}\"", .{out_buf}) catch unreachable; + } + + return MatchResult{ .path_pair = .{ .primary = Path.init(out_buf) }, .diff_case = lookup.diff_case }; + } + } + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Failed to find file: \"{s}/{s}\"", .{ path, base }) catch unreachable; + } + } + + return null; + } + + pub fn loadAsIndexWithBrowserRemapping(r: *Resolver, dir_info: *DirInfo, path: string, extension_order: []const string) ?MatchResult { + if (dir_info.enclosing_browser_scope) |browser_scope| { + comptime const field_rel_path = "index"; + if (browser_scope.package_json) |browser_json| { + if (r.checkBrowserMap(browser_json, field_rel_path)) |remap| { + // Is the path disabled? + // This doesn't really make sense to me. + if (remap.len == 0) { + const paths = [_]string{ path, field_rel_path }; + const new_path = std.fs.path.join(r.allocator, &paths) catch unreachable; + var _path = Path.init(new_path); + _path.is_disabled = true; + return MatchResult{ + .path_pair = PathPair{ + .primary = _path, + }, + }; + } + + const new_paths = [_]string{ path, remap }; + const remapped_abs = std.fs.path.join(r.allocator, &new_paths) catch unreachable; + + // Is this a file + if (r.loadAsFile(remapped_abs, extension_order)) |file_result| { + return MatchResult{ .path_pair = .{ .primary = Path.init(file_result.path) }, .diff_case = file_result.diff_case }; + } + + // Is it a directory with an index? + if (r.dirInfoCached(remapped_abs) catch null) |new_dir| { + if (r.loadAsIndex(new_dir, remapped_abs, extension_order)) |absolute| { + return absolute; + } + } + + return null; + } + } + } + + return r.loadAsIndex(dir_info, path, extension_order); + } + + pub fn loadAsFileOrDirectory(r: *Resolver, path: string, kind: ast.ImportKind) ?MatchResult { + const extension_order = r.opts.extension_order; + + // Is this a file? + if (r.loadAsFile(path, extension_order)) |file| { + return MatchResult{ .path_pair = .{ .primary = Path.init(file.path) }, .diff_case = file.diff_case }; + } + + // Is this a directory? + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Attempting to load \"{s}\" as a directory", .{path}) catch {}; + debug.increaseIndent() catch {}; + } + defer { + if (r.debug_logs) |*debug| { + debug.decreaseIndent() catch {}; + } + } + + const dir_info = (r.dirInfoCached(path) catch null) orelse return null; + + // Try using the main field(s) from "package.json" + if (dir_info.package_json) |pkg_json| { + if (pkg_json.main_fields.count() > 0) { + const main_field_values = pkg_json.main_fields; + const main_field_keys = r.opts.main_fields; + // TODO: check this works right. Not sure this will really work. + const auto_main = r.opts.main_fields.ptr == options.Platform.DefaultMainFields.get(r.opts.platform).ptr; + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Searching for main fields in \"{s}\"", .{pkg_json.source.path.text}) catch {}; + } + + for (main_field_keys) |key| { + const field_rel_path = (main_field_values.get(key)) orelse { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Did not find main field \"{s}\"", .{key}) catch {}; + } + continue; + }; + + var _result = r.loadFromMainField(path, dir_info, field_rel_path, key, extension_order) orelse continue; + + // If the user did not manually configure a "main" field order, then + // use a special per-module automatic algorithm to decide whether to + // use "module" or "main" based on whether the package is imported + // using "import" or "require". + if (auto_main and strings.eqlComptime(key, "module")) { + var absolute_result: ?MatchResult = null; + + if (main_field_values.get("main")) |main_rel_path| { + if (main_rel_path.len > 0) { + absolute_result = r.loadFromMainField(path, dir_info, main_rel_path, "main", extension_order); + } + } else { + // Some packages have a "module" field without a "main" field but + // still have an implicit "index.js" file. In that case, treat that + // as the value for "main". + absolute_result = r.loadAsIndexWithBrowserRemapping(dir_info, path, extension_order); + } + + if (absolute_result) |auto_main_result| { + // If both the "main" and "module" fields exist, use "main" if the + // path is for "require" and "module" if the path is for "import". + // If we're using "module", return enough information to be able to + // fall back to "main" later if something ended up using "require()" + // with this same path. The goal of this code is to avoid having + // both the "module" file and the "main" file in the bundle at the + // same time. + if (kind != ast.ImportKind.require) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Resolved to \"{s}\" using the \"module\" field in \"{s}\"", .{ auto_main_result.path_pair.primary.text, pkg_json.source.key_path.text }) catch {}; + + debug.addNoteFmt("The fallback path in case of \"require\" is {s}", .{auto_main_result.path_pair.primary.text}) catch {}; + } + + return MatchResult{ + .path_pair = .{ + .primary = auto_main_result.path_pair.primary, + .secondary = _result.path_pair.primary, + }, + .diff_case = auto_main_result.diff_case, + }; + } else { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Resolved to \"{s}\" using the \"{s}\" field in \"{s}\"", .{ + auto_main_result.path_pair.primary.text, + key, + pkg_json.source.key_path.text, + }) catch {}; + } + + return auto_main_result; + } + } + } + } + } + } + + // Look for an "index" file with known extensions + return r.loadAsIndexWithBrowserRemapping(dir_info, path, extension_order); + } + + pub fn loadAsFile(r: *Resolver, path: string, extension_order: []const string) ?LoadResult { + var rfs: *Fs.FileSystem.RealFS = &r.fs.fs; + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Attempting to load \"{s}\" as a file", .{path}) catch {}; + debug.increaseIndent() catch {}; + } + defer { + if (r.debug_logs) |*debug| { + debug.decreaseIndent() catch {}; + } + } + + // Read the directory entries once to minimize locking + const dir_path = std.fs.path.dirname(path) orelse unreachable; // Expected path to be a file. + const dir_entry: Fs.FileSystem.RealFS.EntriesOption = r.fs.fs.readDirectory(dir_path) catch { + return null; + }; + + if (@as(Fs.FileSystem.RealFS.EntriesOption.Tag, dir_entry) == .err) { + if (dir_entry.err.original_err != error.ENOENT) { + r.log.addErrorFmt( + null, + logger.Loc.Empty, + r.allocator, + "Cannot read directory \"{s}\": {s}", + .{ + r.prettyPath(Path.init(dir_path)), + @errorName(dir_entry.err.original_err), + }, + ) catch {}; + } + return null; + } + + var entries = dir_entry.entries; + + const base = std.fs.path.basename(path); + + // Try the plain path without any extensions + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Checking for file \"{s}\" ", .{base}) catch {}; + } + + if (entries.get(base)) |query| { + if (query.entry.kind(rfs) == .file) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found file \"{s}\" ", .{base}) catch {}; + } + + return LoadResult{ .path = base, .diff_case = query.diff_case }; + } + } + + // Try the path with extensions + std.mem.copy(u8, &TemporaryBuffer.ExtensionPathBuf, path); + for (r.opts.extension_order) |ext| { + var buffer = TemporaryBuffer.ExtensionPathBuf[0 .. path.len + ext.len]; + std.mem.copy(u8, buffer[path.len..buffer.len], ext); + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Checking for file \"{s}{s}\" ", .{ base, ext }) catch {}; + } + + if (entries.get(buffer)) |query| { + if (query.entry.kind(rfs) == .file) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found file \"{s}\" ", .{buffer}) catch {}; + } + + // now that we've found it, we allocate it. + return LoadResult{ + .path = rfs.allocator.dupe(u8, buffer) catch unreachable, + .diff_case = query.diff_case, + }; + } + } + } + + // TypeScript-specific behavior: if the extension is ".js" or ".jsx", try + // replacing it with ".ts" or ".tsx". At the time of writing this specific + // behavior comes from the function "loadModuleFromFile()" in the file + // "moduleNameResolver.ts" in the TypeScript compiler source code. It + // contains this comment: + // + // If that didn't work, try stripping a ".js" or ".jsx" extension and + // replacing it with a TypeScript one; e.g. "./foo.js" can be matched + // by "./foo.ts" or "./foo.d.ts" + // + // We don't care about ".d.ts" files because we can't do anything with + // those, so we ignore that part of the behavior. + // + // See the discussion here for more historical context: + // https://github.com/microsoft/TypeScript/issues/4595 + if (strings.lastIndexOfChar(base, '.')) |last_dot| { + const ext = base[last_dot..base.len]; + if (strings.eql(ext, ".js") or strings.eql(ext, ".jsx")) { + const segment = base[0..last_dot]; + std.mem.copy(u8, &TemporaryBuffer.ExtensionPathBuf, segment); + + comptime const exts = [_]string{ ".ts", ".tsx" }; + + for (exts) |ext_to_replace| { + var buffer = TemporaryBuffer.ExtensionPathBuf[0 .. segment.len + ext_to_replace.len]; + std.mem.copy(u8, buffer[segment.len..buffer.len], ext_to_replace); + + if (entries.get(buffer)) |query| { + if (query.entry.kind(rfs) == .file) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Rewrote to \"{s}\" ", .{buffer}) catch {}; + } + + return LoadResult{ + .path = rfs.allocator.dupe(u8, buffer) catch unreachable, + .diff_case = query.diff_case, + }; + } + } + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Failed to rewrite \"{s}\" ", .{base}) catch {}; + } + } + } + } + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Failed to find \"{s}\" ", .{path}) catch {}; + } + return null; } fn dirInfoUncached(r: *Resolver, path: string) anyerror!?*DirInfo { - var rfs: *fs.FileSystem.RealFS = &r.fs.fs; + var rfs: *Fs.FileSystem.RealFS = &r.fs.fs; var parent: ?*DirInfo = null; const parent_dir = std.fs.path.dirname(path) orelse return null; if (!strings.eql(parent_dir, path)) { @@ -459,7 +1262,7 @@ pub const Resolver = struct { // continue to check the directories above it, which is now node behaves. switch (_entries.err.original_err) { error.EACCESS => { - entries = fs.FileSystem.DirEntry.empty(path, r.allocator); + entries = Fs.FileSystem.DirEntry.empty(path, r.allocator); }, // Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves |