aboutsummaryrefslogtreecommitdiff
path: root/src/resolver/resolver.zig
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-05-12 20:33:58 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-05-12 20:33:58 -0700
commitf12ed9904b03e11f755dce7b614925ea087f40da (patch)
treecfbbcab5ee4931d67b8e15d8175291675019ab92 /src/resolver/resolver.zig
parent1010bae1a350d12f7db49b8ca7f94aa748790b77 (diff)
downloadbun-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.zig835
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