aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/schema.d.ts1
-rw-r--r--src/api/schema.js17
-rw-r--r--src/api/schema.peechy2
-rw-r--r--src/api/schema.zig31
-rw-r--r--src/cli.zig40
-rw-r--r--src/fs.zig21
-rw-r--r--src/global.zig1
-rw-r--r--src/options.zig10
-rw-r--r--src/resolver/resolver.zig835
-rw-r--r--src/resolver/tsconfig_json.zig5
10 files changed, 927 insertions, 36 deletions
diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts
index ce4521d6b..c78d7fc6c 100644
--- a/src/api/schema.d.ts
+++ b/src/api/schema.d.ts
@@ -127,6 +127,7 @@ type uint32 = number;
main_fields?: string[];
platform?: Platform;
watch?: boolean;
+ extension_order?: string[];
}
export interface FileHandle {
diff --git a/src/api/schema.js b/src/api/schema.js
index acd53ac51..43670dff0 100644
--- a/src/api/schema.js
+++ b/src/api/schema.js
@@ -231,6 +231,12 @@ function decodeTransformOptions(bb) {
result["watch"] = !!bb.readByte();
break;
+ case 19:
+ var length = bb.readVarUint();
+ var values = result["extension_order"] = Array(length);
+ for (var i = 0; i < length; i++) values[i] = bb.readString();
+ break;
+
default:
throw new Error("Attempted to parse invalid message");
}
@@ -392,6 +398,17 @@ bb.writeByte(encoded);
bb.writeByte(18);
bb.writeByte(value);
}
+
+ var value = message["extension_order"];
+ if (value != null) {
+ bb.writeByte(19);
+ var values = value, n = values.length;
+ bb.writeVarUint(n);
+ for (var i = 0; i < n; i++) {
+ value = values[i];
+ bb.writeString(value);
+ }
+ }
bb.writeByte(0);
}
diff --git a/src/api/schema.peechy b/src/api/schema.peechy
index f36a968b2..6c6889f00 100644
--- a/src/api/schema.peechy
+++ b/src/api/schema.peechy
@@ -69,6 +69,8 @@ message TransformOptions {
Platform platform = 17;
bool watch = 18;
+
+ string[] extension_order = 19;
}
struct FileHandle {
diff --git a/src/api/schema.zig b/src/api/schema.zig
index d32b8fb23..bba5a3c94 100644
--- a/src/api/schema.zig
+++ b/src/api/schema.zig
@@ -203,6 +203,9 @@ pub const Api = struct {
/// watch
watch: ?bool = null,
+ /// extension_order
+ extension_order: []const []const u8,
+
pub fn decode(allocator: *std.mem.Allocator, reader: anytype) anyerror!TransformOptions {
var obj = std.mem.zeroes(TransformOptions);
try update(&obj, allocator, reader);
@@ -380,6 +383,21 @@ pub const Api = struct {
18 => {
result.watch = (try reader.readByte()) == @as(u8, 1);
},
+ 19 => {
+ {
+ var array_count = try reader.readIntNative(u32);
+ if (array_count != result.extension_order.len) {
+ result.extension_order = try allocator.alloc([]const u8, array_count);
+ }
+ length = try reader.readIntNative(u32);
+ for (result.extension_order) |content, j| {
+ if (result.extension_order[j].len != length and length > 0) {
+ result.extension_order[j] = try allocator.alloc(u8, length);
+ }
+ _ = try reader.readAll(result.extension_order[j].?);
+ }
+ }
+ },
else => {
return error.InvalidMessage;
},
@@ -545,6 +563,19 @@ pub const Api = struct {
try writer.writeByte(18);
try writer.writeByte(@boolToInt(watch));
}
+
+ if (result.extension_order) |extension_order| {
+ try writer.writeByte(19);
+ n = result.extension_order.len;
+ _ = try writer.writeIntNative(u32, @intCast(u32, n));
+ {
+ var j: usize = 0;
+ while (j < n) : (j += 1) {
+ _ = try writer.writeIntNative(u32, @intCast(u32, result.extension_order[j].len));
+ try writer.writeAll(std.mem.sliceAsBytes(extension_order[j]));
+ }
+ }
+ }
try writer.writeByte(0);
return;
}
diff --git a/src/cli.zig b/src/cli.zig
index ecebc2ee0..01240482d 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -104,25 +104,26 @@ pub const Cli = struct {
pub fn parse(allocator: *std.mem.Allocator, stdout: anytype, stderr: anytype) !Api.TransformOptions {
@setEvalBranchQuota(9999);
const params = comptime [_]clap.Param(clap.Help){
- clap.parseParam("-h, --help Display this help and exit. ") catch unreachable,
- clap.parseParam("-r, --resolve <STR> Determine import/require behavior. \"disable\" ignores. \"dev\" bundles node_modules and builds everything else as independent entry points") catch unreachable,
- clap.parseParam("-d, --define <STR>... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:development") catch unreachable,
- clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: jsx, js, json, tsx (not implemented yet), ts (not implemented yet), css (not implemented yet)") catch unreachable,
- clap.parseParam("-o, --outdir <STR> Save output to directory (default: \"out\" if none provided and multiple entry points passed)") catch unreachable,
- clap.parseParam("-e, --external <STR>... Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable,
- clap.parseParam("-i, --inject <STR>... Inject module at the top of every file") catch unreachable,
- clap.parseParam("--cwd <STR> Absolute path to resolve entry points from. Defaults to cwd") catch unreachable,
- clap.parseParam("--public-url <STR> Rewrite import paths to start with --public-url. Useful for web browsers.") catch unreachable,
- clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable,
- clap.parseParam("--jsx-fragment <STR> Changes the function called when compiling JSX fragments using the classic JSX runtime") catch unreachable,
- clap.parseParam("--jsx-import-source <STR> Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: \"react\"") catch unreachable,
- clap.parseParam("--jsx-runtime <STR> \"automatic\" (default) or \"classic\"") catch unreachable,
- clap.parseParam("--jsx-production Use jsx instead of jsxDEV (default) for the automatic runtime") catch unreachable,
- clap.parseParam("--react-fast-refresh Enable React Fast Refresh (not implemented yet)") catch unreachable,
- clap.parseParam("--tsconfig-override <STR> Load tsconfig from path instead of cwd/tsconfig.json") catch unreachable,
- clap.parseParam("--platform <STR> \"browser\" or \"node\". Defaults to \"browser\"") catch unreachable,
- clap.parseParam("--main-fields <STR>... Main fields to lookup in package.json. Defaults to --platform dependent") catch unreachable,
- clap.parseParam("<POS>... Entry points to use") catch unreachable,
+ clap.parseParam("-h, --help Display this help and exit. ") catch unreachable,
+ clap.parseParam("-r, --resolve <STR> Determine import/require behavior. \"disable\" ignores. \"dev\" bundles node_modules and builds everything else as independent entry points") catch unreachable,
+ clap.parseParam("-d, --define <STR>... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:development") catch unreachable,
+ clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: jsx, js, json, tsx (not implemented yet), ts (not implemented yet), css (not implemented yet)") catch unreachable,
+ clap.parseParam("-o, --outdir <STR> Save output to directory (default: \"out\" if none provided and multiple entry points passed)") catch unreachable,
+ clap.parseParam("-e, --external <STR>... Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable,
+ clap.parseParam("-i, --inject <STR>... Inject module at the top of every file") catch unreachable,
+ clap.parseParam("--cwd <STR> Absolute path to resolve entry points from. Defaults to cwd") catch unreachable,
+ clap.parseParam("--public-url <STR> Rewrite import paths to start with --public-url. Useful for web browsers.") catch unreachable,
+ clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable,
+ clap.parseParam("--jsx-fragment <STR> Changes the function called when compiling JSX fragments using the classic JSX runtime") catch unreachable,
+ clap.parseParam("--jsx-import-source <STR> Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: \"react\"") catch unreachable,
+ clap.parseParam("--jsx-runtime <STR> \"automatic\" (default) or \"classic\"") catch unreachable,
+ clap.parseParam("--jsx-production Use jsx instead of jsxDEV (default) for the automatic runtime") catch unreachable,
+ clap.parseParam("--extension-order <STR>... defaults to: .tsx,.ts,.jsx,.js,.json ") catch unreachable,
+ clap.parseParam("--react-fast-refresh Enable React Fast Refresh (not implemented yet)") catch unreachable,
+ clap.parseParam("--tsconfig-override <STR> Load tsconfig from path instead of cwd/tsconfig.json") catch unreachable,
+ clap.parseParam("--platform <STR> \"browser\" or \"node\". Defaults to \"browser\"") catch unreachable,
+ clap.parseParam("--main-fields <STR>... Main fields to lookup in package.json. Defaults to --platform dependent") catch unreachable,
+ clap.parseParam("<POS>... Entry points to use") catch unreachable,
};
var diag = clap.Diagnostic{};
@@ -260,6 +261,7 @@ pub const Cli = struct {
.write = write,
.inject = inject,
.entry_points = entry_points,
+ .extension_order = args.options("--extension-order"),
.main_fields = args.options("--main-fields"),
.platform = platform,
};
diff --git a/src/fs.zig b/src/fs.zig
index 2337bdb19..e5ab8e832 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -629,6 +629,7 @@ pub const Path = struct {
text: string,
namespace: string = "unspecified",
name: PathName,
+ is_disabled: bool = false,
pub fn generateKey(p: *Path, allocator: *std.mem.Allocator) !string {
return try std.fmt.allocPrint(allocator, "{s}://{s}", .{ p.namespace, p.text });
@@ -643,6 +644,26 @@ pub const Path = struct {
return str;
}
+ // for now, assume you won't try to normalize a path longer than 1024 chars
+ pub fn normalizeNoAlloc(str: string, comptime remap_windows_paths: bool) string {
+ if (str.len == 0 or (str.len == 1 and (str[0] == ' ' or str[0] == '\\'))) return ".";
+
+ if (remap_windows_paths) {
+ std.mem.copy(u8, &normalize_buf, str);
+ var i: usize = 0;
+ while (i < str.len) : (i += 1) {
+ if (str[i] == '\\') {
+ normalize_buf[i] = '/';
+ }
+ }
+ }
+
+ if (resolvePath(&normalize_buf, str)) |out| {
+ return out;
+ }
+ return str;
+ }
+
pub fn init(text: string) Path {
return Path{ .pretty = text, .text = text, .namespace = "file", .name = PathName.init(text) };
}
diff --git a/src/global.zig b/src/global.zig
index 8f15aa3a2..5a48d1b9c 100644
--- a/src/global.zig
+++ b/src/global.zig
@@ -88,6 +88,7 @@ pub const Global = struct {
std.debug.panic(fmt, args);
}
}
+
pub fn notimpl() noreturn {
Global.panic("Not implemented yet!!!!!", .{});
}
diff --git a/src/options.zig b/src/options.zig
index 265c11148..9d5f91699 100644
--- a/src/options.zig
+++ b/src/options.zig
@@ -294,6 +294,12 @@ pub const BundleOptions = struct {
log: *logger.Log,
external: ExternalModules = ExternalModules{},
entry_points: []const string,
+ extension_order: []const string = &Defaults.ExtensionOrder,
+
+ pub const Defaults = struct {
+ pub var ExtensionOrder = [_]string{ ".tsx", ".ts", ".jsx", ".js", ".json" };
+ };
+
pub fn fromApi(
allocator: *std.mem.Allocator,
fs: *Fs.FileSystem,
@@ -338,6 +344,10 @@ pub const BundleOptions = struct {
opts.jsx = try JSX.Pragma.fromApi(jsx, allocator);
}
+ if (transform.extension_order.len > 0) {
+ opts.extension_order = transform.extension_order;
+ }
+
if (transform.platform) |plat| {
opts.platform = if (plat == .browser) .browser else .node;
opts.main_fields = Platform.DefaultMainFields.get(opts.platform);
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
diff --git a/src/resolver/tsconfig_json.zig b/src/resolver/tsconfig_json.zig
index 6c7992833..52469d6bf 100644
--- a/src/resolver/tsconfig_json.zig
+++ b/src/resolver/tsconfig_json.zig
@@ -7,7 +7,10 @@ const js_ast = @import("../js_ast.zig");
const js_lexer = @import("../js_lexer.zig");
const alloc = @import("../alloc.zig");
-const PathsMap = std.StringHashMap([]string);
+// Heuristic: you probably don't have 100 of these
+// Probably like 5-10
+// Array iteration is faster and deterministically ordered in that case.
+const PathsMap = std.StringArrayHashMap([]string);
pub const TSConfigJSON = struct {
abs_path: string,