aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/resolver/package_json.zig101
-rw-r--r--src/resolver/resolver.zig178
-rw-r--r--src/string_immutable.zig42
3 files changed, 228 insertions, 93 deletions
diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig
index 5377df556..49b3ad227 100644
--- a/src/resolver/package_json.zig
+++ b/src/resolver/package_json.zig
@@ -732,13 +732,13 @@ pub const PackageJSON = struct {
}
if (json.asProperty("exports")) |exports_prop| {
- if (ExportsMap.parse(r.allocator, &json_source, r.log, exports_prop.expr)) |exports_map| {
+ if (ExportsMap.parse(r.allocator, &json_source, r.log, exports_prop.expr, exports_prop.loc)) |exports_map| {
package_json.exports = exports_map;
}
}
if (json.asProperty("imports")) |imports_prop| {
- if (ExportsMap.parse(r.allocator, &json_source, r.log, imports_prop.expr)) |imports_map| {
+ if (ExportsMap.parse(r.allocator, &json_source, r.log, imports_prop.expr, imports_prop.loc)) |imports_map| {
package_json.imports = imports_map;
}
}
@@ -955,8 +955,9 @@ pub const PackageJSON = struct {
pub const ExportsMap = struct {
root: Entry,
exports_range: logger.Range = logger.Range.None,
+ property_key_loc: logger.Loc,
- pub fn parse(allocator: std.mem.Allocator, source: *const logger.Source, log: *logger.Log, json: js_ast.Expr) ?ExportsMap {
+ pub fn parse(allocator: std.mem.Allocator, source: *const logger.Source, log: *logger.Log, json: js_ast.Expr, property_key_loc: logger.Loc) ?ExportsMap {
var visitor = Visitor{ .allocator = allocator, .source = source, .log = log };
const root = visitor.visit(json);
@@ -968,6 +969,7 @@ pub const ExportsMap = struct {
return ExportsMap{
.root = root,
.exports_range = source.rangeOfString(json.loc),
+ .property_key_loc = property_key_loc,
};
}
@@ -1050,7 +1052,7 @@ pub const ExportsMap = struct {
map_data_ranges[i] = key_range;
map_data_entries[i] = this.visit(prop.value.?);
- if (strings.endsWithAnyComptime(key, "/*")) {
+ if (strings.endsWithComptime(key, "/") or strings.containsChar(key, '*')) {
expansion_keys[expansion_key_i] = Entry.Data.Map.MapEntry{
.value = map_data_entries[i],
.key = key,
@@ -1063,11 +1065,12 @@ pub const ExportsMap = struct {
// this leaks a lil, but it's fine.
expansion_keys = expansion_keys[0..expansion_key_i];
- // Let expansion_keys be the list of keys of matchObj ending in "/" or "*",
- // sorted by length descending.
- const LengthSorter: type = strings.NewLengthSorter(Entry.Data.Map.MapEntry, "key");
- var sorter = LengthSorter{};
- std.sort.sort(Entry.Data.Map.MapEntry, expansion_keys, sorter, LengthSorter.lessThan);
+ // Let expansionKeys be the list of keys of matchObj either ending in "/"
+ // or containing only a single "*", sorted by the sorting function
+ // PATTERN_KEY_COMPARE which orders in descending order of specificity.
+ const GlobLengthSorter: type = strings.NewGlobLengthSorter(Entry.Data.Map.MapEntry, "key");
+ var sorter = GlobLengthSorter{};
+ std.sort.sort(Entry.Data.Map.MapEntry, expansion_keys, sorter, GlobLengthSorter.lessThan);
return Entry{
.data = .{
@@ -1186,6 +1189,7 @@ pub const ESModule = struct {
UndefinedNoConditionsMatch, // A more friendly error message for when no conditions are matched
Null,
Exact,
+ ExactEndsWithStar,
Inexact, // This means we may need to try CommonJS-style extension suffixes
/// Module specifier is an invalid URL, package name or package subpath specifier.
@@ -1203,9 +1207,15 @@ pub const ESModule = struct {
/// The package or module requested does not exist.
ModuleNotFound,
+ /// The user just needs to add the missing extension
+ ModuleNotFoundMissingExtension,
+
/// The resolved path corresponds to a directory, which is not a supported target for module imports.
UnsupportedDirectoryImport,
+ /// The user just needs to add the missing "/index.js" suffix
+ UnsupportedDirectoryImportMissingIndex,
+
/// When a package path is explicitly set to null, that means it's not exported.
PackagePathDisabled,
@@ -1383,7 +1393,7 @@ pub const ESModule = struct {
pub fn finalize(result_: Resolution) Resolution {
var result = result_;
- if (result.status != .Exact and result.status != .Inexact) {
+ if (result.status != .Exact and result.status != .ExactEndsWithStar and result.status != .Inexact) {
return result;
}
@@ -1489,7 +1499,8 @@ pub const ESModule = struct {
logs.addNoteFmt("Checking object path map for \"{s}\"", .{match_key});
}
- if (!strings.endsWithChar(match_key, '.')) {
+ // If matchKey is a key of matchObj and does not end in "/" or contain "*", then
+ if (!strings.endsWithChar(match_key, '/') and !strings.containsChar(match_key, '*')) {
if (match_obj.valueForKey(match_key)) |target| {
if (r.debug_logs) |log| {
log.addNoteFmt("Found \"{s}\"", .{match_key});
@@ -1502,36 +1513,46 @@ pub const ESModule = struct {
if (match_obj.data == .map) {
const expansion_keys = match_obj.data.map.expansion_keys;
for (expansion_keys) |expansion| {
- // If expansionKey ends in "*" and matchKey starts with but is not equal to
- // the substring of expansionKey excluding the last "*" character
- if (strings.endsWithChar(expansion.key, '*')) {
- const substr = expansion.key[0 .. expansion.key.len - 1];
- if (strings.startsWith(match_key, substr) and !strings.eql(match_key, substr)) {
+
+ // If expansionKey contains "*", set patternBase to the substring of
+ // expansionKey up to but excluding the first "*" character
+ if (strings.indexOfChar(expansion.key, '*')) |star| {
+ const pattern_base = expansion.key[0..star];
+ // If patternBase is not null and matchKey starts with but is not equal
+ // to patternBase, then
+ if (strings.startsWith(match_key, pattern_base)) {
+ // Let patternTrailer be the substring of expansionKey from the index
+ // after the first "*" character.
+ const pattern_trailer = expansion.key[star + 1 ..];
+
+ // If patternTrailer has zero length, or if matchKey ends with
+ // patternTrailer and the length of matchKey is greater than or
+ // equal to the length of expansionKey, then
+ if (pattern_trailer.len == 0 or (strings.endsWith(match_key, pattern_trailer) and match_key.len >= expansion.key.len)) {
+ const target = expansion.value;
+ const subpath = match_key[pattern_base.len .. match_key.len - pattern_trailer.len];
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath });
+ }
+ return r.resolveTarget(package_url, target, subpath, is_imports, true);
+ }
+ }
+ } else {
+ // Otherwise if patternBase is null and matchKey starts with
+ // expansionKey, then
+ if (strings.startsWith(match_key, expansion.key)) {
const target = expansion.value;
- const subpath = match_key[expansion.key.len - 1 ..];
+ const subpath = match_key[expansion.key.len..];
if (r.debug_logs) |log| {
log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath });
}
-
- return r.resolveTarget(package_url, target, subpath, is_imports, true);
- }
- }
-
- if (strings.startsWith(match_key, expansion.key)) {
- const target = expansion.value;
- const subpath = match_key[expansion.key.len..];
- if (r.debug_logs) |log| {
- log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath });
+ var result = r.resolveTarget(package_url, target, subpath, is_imports, false);
+ if (result.status == .Exact or result.status == .ExactEndsWithStar) {
+ // Return the object { resolved, exact: false }.
+ result.status = .Inexact;
+ }
+ return result;
}
-
- var result = r.resolveTarget(package_url, target, subpath, is_imports, false);
- result.status = if (result.status == .Exact)
- // Return the object { resolved, exact: false }.
- .Inexact
- else
- result.status;
-
- return result;
}
if (r.debug_logs) |log| {
@@ -1645,10 +1666,14 @@ pub const ESModule = struct {
_ = std.mem.replace(u8, resolved_target, "*", subpath, &resolve_target_buf2);
const result = resolve_target_buf2[0..len];
if (r.debug_logs) |log| {
- log.addNoteFmt("Subsituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, resolved_target, result });
+ log.addNoteFmt("Substituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, resolved_target, result });
}
- return Resolution{ .path = result, .status = .Exact, .debug = .{ .token = target.first_token } };
+ const status: Status = if (strings.endsWithChar(result, '*') and strings.indexOfChar(result, '*').? == result.len - 1)
+ .ExactEndsWithStar
+ else
+ .Exact;
+ return Resolution{ .path = result, .status = status, .debug = .{ .token = target.first_token } };
} else {
var parts2 = [_]string{ package_url, str, subpath };
const result = resolve_path.joinStringBuf(&resolve_target_buf2, parts2, .auto);
diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig
index 9fad998cb..b88855c84 100644
--- a/src/resolver/resolver.zig
+++ b/src/resolver/resolver.zig
@@ -192,6 +192,9 @@ pub const Result = struct {
notes: std.ArrayList(logger.Data),
suggestion_text: string = "",
suggestion_message: string = "",
+ suggestion_range: SuggestionRange,
+
+ pub const SuggestionRange = enum { full, end };
pub fn init(allocator: std.mem.Allocator) DebugMeta {
return DebugMeta{ .notes = std.ArrayList(logger.Data).init(allocator) };
@@ -199,7 +202,11 @@ pub const Result = struct {
pub fn logErrorMsg(m: *DebugMeta, log: *logger.Log, _source: ?*const logger.Source, r: logger.Range, comptime fmt: string, args: anytype) !void {
if (_source != null and m.suggestion_message.len > 0) {
- const data = logger.rangeData(_source.?, r, m.suggestion_message);
+ const suggestion_range = if (m.suggestion_range == .end)
+ logger.Range{ .loc = logger.Loc{ .start = r.endI() - 1 } }
+ else
+ r;
+ const data = logger.rangeData(_source.?, suggestion_range, m.suggestion_message);
data.location.?.suggestion = m.suggestion_text;
try m.notes.append(data);
}
@@ -1402,51 +1409,17 @@ pub const Resolver = struct {
}
}
- const esm_ = ESModule.Package.parse(import_path, &esm_subpath_buf);
-
- if (import_path[0] == '#' and !forbid_imports) {
- if (esm_ != null) {
- if (dir_info.enclosing_package_json) |package_json| {
- load_from_imports_map: {
- const imports_map = package_json.imports orelse break :load_from_imports_map;
-
- if (import_path.len == 1 or strings.hasPrefix(import_path, "#/")) {
- if (r.debug_logs) |*debug| {
- debug.addNoteFmt("The path \"{s}\" must not equal \"#\" and must not start with \"#/\"", .{import_path});
- }
- return .{ .not_found = {} };
- }
-
- const esmodule = ESModule{
- .conditions = switch (kind) {
- ast.ImportKind.require, ast.ImportKind.require_resolve => r.opts.conditions.require,
- else => r.opts.conditions.import,
- },
- .allocator = r.allocator,
- .debug_logs = if (r.debug_logs) |*debug| debug else null,
- };
-
- const esm_resolution = esmodule.resolveImports(import_path, imports_map.root);
-
- if (esm_resolution.status == .PackageResolve)
- return r.loadNodeModules(
- esm_resolution.path,
- kind,
- dir_info,
- global_cache,
- true,
- );
-
- if (r.handleESMResolution(esm_resolution, package_json.source.path.name.dir, kind, package_json)) |result| {
- return .{ .success = result };
- }
+ // Find the parent directory with the "package.json" file
+ var dir_info_package_json: ?*DirInfo = dir_info;
+ while (dir_info_package_json != null and dir_info_package_json.?.package_json == null) : (dir_info_package_json = dir_info_package_json.?.getParent()) {}
- return .{ .not_found = {} };
- }
- }
- }
+ // Check for subpath imports: https://nodejs.org/api/packages.html#subpath-imports
+ if (dir_info_package_json != null and strings.hasPrefix(import_path, "#") and !forbid_imports and dir_info_package_json.?.package_json.?.imports != null) {
+ return r.loadPackageImports(import_path, dir_info_package_json.?, kind, global_cache);
}
+ const esm_ = ESModule.Package.parse(import_path, &esm_subpath_buf);
+
var source_dir_info = dir_info;
var any_node_modules_folder = false;
const use_node_module_resolver = global_cache != .force;
@@ -1492,7 +1465,7 @@ pub const Resolver = struct {
// directory path accidentally being interpreted as URL escapes.
const esm_resolution = esmodule.resolve("/", esm.subpath, exports_map.root);
- if (r.handleESMResolution(esm_resolution, abs_package_path, kind, package_json)) |result| {
+ if (r.handleESMResolution(esm_resolution, abs_package_path, kind, package_json, esm.subpath)) |result| {
return .{ .success = result };
}
@@ -1709,7 +1682,7 @@ pub const Resolver = struct {
// directory path accidentally being interpreted as URL escapes.
const esm_resolution = esmodule.resolve("/", esm.subpath, exports_map.root);
- if (r.handleESMResolution(esm_resolution, abs_package_path, kind, package_json)) |*result| {
+ if (r.handleESMResolution(esm_resolution, abs_package_path, kind, package_json, esm.subpath)) |*result| {
result.is_node_module = true;
return .{ .success = result.* };
}
@@ -1928,22 +1901,24 @@ pub const Resolver = struct {
bun.unreachablePanic("TODO: implement enqueueDependencyToResolve for non-root packages", .{});
}
- fn handleESMResolution(r: *ThisResolver, esm_resolution_: ESModule.Resolution, abs_package_path: string, kind: ast.ImportKind, package_json: *PackageJSON) ?MatchResult {
+ fn handleESMResolution(r: *ThisResolver, esm_resolution_: ESModule.Resolution, abs_package_path: string, kind: ast.ImportKind, package_json: *PackageJSON, package_subpath: string) ?MatchResult {
var esm_resolution = esm_resolution_;
- if (!((esm_resolution.status == .Inexact or esm_resolution.status == .Exact) and
+ if (!((esm_resolution.status == .Inexact or esm_resolution.status == .Exact or esm_resolution.status == .ExactEndsWithStar) and
esm_resolution.path.len > 0 and esm_resolution.path[0] == '/'))
return null;
const abs_esm_path: string = brk: {
var parts = [_]string{
abs_package_path,
- esm_resolution.path[1..],
+ strings.withoutLeadingSlash(esm_resolution.path),
};
break :brk r.fs.absBuf(&parts, &esm_absolute_package_path_joined);
};
+ var missing_suffix: string = undefined;
+
switch (esm_resolution.status) {
- .Exact => {
+ .Exact, .ExactEndsWithStar => {
const resolved_dir_info = (r.dirInfoCached(std.fs.path.dirname(abs_esm_path).?) catch null) orelse {
esm_resolution.status = .ModuleNotFound;
return null;
@@ -1952,13 +1927,63 @@ pub const Resolver = struct {
esm_resolution.status = .ModuleNotFound;
return null;
};
- const entry_query = entries.get(std.fs.path.basename(abs_esm_path)) orelse {
+ const base = std.fs.path.basename(abs_esm_path);
+ const extension_order = if (kind == .at or kind == .at_conditional)
+ r.extension_order
+ else
+ r.opts.extension_order;
+ const entry_query = entries.get(base) orelse {
+ const ends_with_star = esm_resolution.status == .ExactEndsWithStar;
esm_resolution.status = .ModuleNotFound;
+
+ // Try to have a friendly error message if people forget the extension
+ if (ends_with_star) {
+ std.mem.copy(u8, &load_as_file_buf, base);
+ for (extension_order) |ext| {
+ var file_name = load_as_file_buf[0 .. base.len + ext.len];
+ std.mem.copy(u8, file_name[base.len..], ext);
+ if (entries.get(file_name) != null) {
+ if (r.debug_logs) |*debug| {
+ const parts = [_]string{ package_json.name, package_subpath };
+ debug.addNoteFmt("The import {s} is missing the extension {s}", .{ ResolvePath.join(parts, .auto), ext });
+ }
+ esm_resolution.status = .ModuleNotFoundMissingExtension;
+ missing_suffix = ext;
+ break;
+ }
+ }
+ }
return null;
};
if (entry_query.entry.kind(&r.fs.fs) == .dir) {
+ const ends_with_star = esm_resolution.status == .ExactEndsWithStar;
esm_resolution.status = .UnsupportedDirectoryImport;
+
+ // Try to have a friendly error message if people forget the "/index.js" suffix
+ if (ends_with_star) {
+ if (r.dirInfoCached(abs_esm_path) catch null) |dir_info| {
+ if (dir_info.getEntries()) |dir_entries| {
+ const index = "index";
+ std.mem.copy(u8, &load_as_file_buf, index);
+ for (extension_order) |ext| {
+ var file_name = load_as_file_buf[0 .. index.len + ext.len];
+ std.mem.copy(u8, file_name[index.len..], ext);
+ const index_query = dir_entries.get(file_name);
+ if (index_query != null and index_query.?.entry.kind(&r.fs.fs) == .file) {
+ missing_suffix = std.fmt.allocPrint(r.allocator, "/{s}", .{file_name}) catch unreachable;
+ // defer r.allocator.free(missing_suffix);
+ if (r.debug_logs) |*debug| {
+ const parts = [_]string{ package_json.name, package_subpath };
+ debug.addNoteFmt("The import {s} is missing the suffix {s}", .{ ResolvePath.join(parts, .auto), missing_suffix });
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
return null;
}
@@ -2509,6 +2534,49 @@ pub const Resolver = struct {
return null;
}
+ pub fn loadPackageImports(r: *ThisResolver, import_path: string, dir_info: *DirInfo, kind: ast.ImportKind, global_cache: GlobalCache) MatchResult.Union {
+ const package_json = dir_info.package_json.?;
+ if (r.debug_logs) |*debug| {
+ debug.addNoteFmt("Looking for {s} in \"imports\" map in {s}", .{ import_path, package_json.source.key_path.text });
+ debug.increaseIndent();
+ defer debug.decreaseIndent();
+ }
+ const imports_map = package_json.imports.?;
+
+ if (import_path.len == 1 or strings.hasPrefix(import_path, "#/")) {
+ if (r.debug_logs) |*debug| {
+ debug.addNoteFmt("The path \"{s}\" must not equal \"#\" and must not start with \"#/\"", .{import_path});
+ }
+ return .{ .not_found = {} };
+ }
+
+ const esmodule = ESModule{
+ .conditions = switch (kind) {
+ ast.ImportKind.require, ast.ImportKind.require_resolve => r.opts.conditions.require,
+ else => r.opts.conditions.import,
+ },
+ .allocator = r.allocator,
+ .debug_logs = if (r.debug_logs) |*debug| debug else null,
+ };
+
+ const esm_resolution = esmodule.resolveImports(import_path, imports_map.root);
+
+ if (esm_resolution.status == .PackageResolve)
+ return r.loadNodeModules(
+ esm_resolution.path,
+ kind,
+ dir_info,
+ global_cache,
+ true,
+ );
+
+ if (r.handleESMResolution(esm_resolution, package_json.source.path.name.dir, kind, package_json, "")) |result| {
+ return .{ .success = result };
+ }
+
+ return .{ .not_found = {} };
+ }
+
const BrowserMapPath = struct {
remapped: string = "",
cleaned: string = "",
@@ -2741,7 +2809,7 @@ pub const Resolver = struct {
};
}
- pub fn loadAsIndex(r: *ThisResolver, dir_info: *DirInfo, path: string, extension_order: []const string) ?MatchResult {
+ pub fn loadAsIndex(r: *ThisResolver, dir_info: *DirInfo, extension_order: []const string) ?MatchResult {
var rfs = &r.fs.fs;
// Try the "index" file with extensions
for (extension_order) |ext| {
@@ -2754,7 +2822,7 @@ pub const Resolver = struct {
if (lookup.entry.kind(rfs) == .file) {
const out_buf = brk: {
if (lookup.entry.abs_path.isEmpty()) {
- const parts = [_]string{ path, base };
+ const parts = [_]string{ dir_info.abs_path, base };
const out_buf_ = r.fs.absBuf(&parts, &index_buf);
lookup.entry.abs_path =
PathString.init(r.fs.dirname_store.append(@TypeOf(out_buf_), out_buf_) catch unreachable);
@@ -2786,7 +2854,7 @@ pub const Resolver = struct {
}
if (r.debug_logs) |*debug| {
- debug.addNoteFmt("Failed to find file: \"{s}/{s}\"", .{ path, base });
+ debug.addNoteFmt("Failed to find file: \"{s}/{s}\"", .{ dir_info.abs_path, base });
}
}
@@ -2837,7 +2905,7 @@ pub const Resolver = struct {
// 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| {
+ if (r.loadAsIndex(new_dir, extension_order)) |absolute| {
return absolute;
}
}
@@ -2847,7 +2915,7 @@ pub const Resolver = struct {
}
}
- return r.loadAsIndex(dir_info, path_, extension_order);
+ return r.loadAsIndex(dir_info, extension_order);
}
pub fn loadAsFileOrDirectory(r: *ThisResolver, path: string, kind: ast.ImportKind) ?MatchResult {
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index 38701698c..654080f8a 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -3582,6 +3582,48 @@ pub fn NewLengthSorter(comptime Type: type, comptime field: string) type {
};
}
+pub fn NewGlobLengthSorter(comptime Type: type, comptime field: string) type {
+ return struct {
+ const GlobLengthSorter = @This();
+ pub fn lessThan(_: GlobLengthSorter, lhs: Type, rhs: Type) bool {
+ // Assert: keyA ends with "/" or contains only a single "*".
+ // Assert: keyB ends with "/" or contains only a single "*".
+ const key_a = @field(lhs, field);
+ const key_b = @field(rhs, field);
+
+ // Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise.
+ // Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise.
+ const star_a = indexOfChar(key_a, '*');
+ const star_b = indexOfChar(key_b, '*');
+ const base_length_a = star_a orelse key_a.len;
+ const base_length_b = star_b orelse key_b.len;
+
+ // If baseLengthA is greater than baseLengthB, return -1.
+ // If baseLengthB is greater than baseLengthA, return 1.
+ if (base_length_a > base_length_b)
+ return true;
+ if (base_length_b > base_length_a)
+ return false;
+
+ // If keyA does not contain "*", return 1.
+ // If keyB does not contain "*", return -1.
+ if (star_a == null)
+ return false;
+ if (star_b == null)
+ return true;
+
+ // If the length of keyA is greater than the length of keyB, return -1.
+ // If the length of keyB is greater than the length of keyA, return 1.
+ if (key_a.len > key_b.len)
+ return true;
+ if (key_b.len > key_a.len)
+ return false;
+
+ return false;
+ }
+ };
+}
+
/// Update all strings in a struct pointing to "from" to point to "to".
pub fn moveAllSlices(comptime Type: type, container: *Type, from: string, to: string) void {
const fields_we_care_about = comptime brk: {