mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
okay I think that's most of resolving packages/imports algorithm!!!
This commit is contained in:
1
src/api/schema.d.ts
vendored
1
src/api/schema.d.ts
vendored
@@ -127,6 +127,7 @@ type uint32 = number;
|
||||
main_fields?: string[];
|
||||
platform?: Platform;
|
||||
watch?: boolean;
|
||||
extension_order?: string[];
|
||||
}
|
||||
|
||||
export interface FileHandle {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ message TransformOptions {
|
||||
Platform platform = 17;
|
||||
|
||||
bool watch = 18;
|
||||
|
||||
string[] extension_order = 19;
|
||||
}
|
||||
|
||||
struct FileHandle {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
40
src/cli.zig
40
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,
|
||||
};
|
||||
|
||||
21
src/fs.zig
21
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) };
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ pub const Global = struct {
|
||||
std.debug.panic(fmt, args);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notimpl() noreturn {
|
||||
Global.panic("Not implemented yet!!!!!", .{});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user