diff options
author | 2022-11-22 01:31:02 +0000 | |
---|---|---|
committer | 2022-11-21 17:31:02 -0800 | |
commit | a3dc33c13350f4227c1cfb669f095d011d34ed76 (patch) | |
tree | 06df82628c0a95d483c43423d1334ec61772933d | |
parent | a274ddba3af6d7438508b0ee97a467fc14505efd (diff) | |
download | bun-a3dc33c13350f4227c1cfb669f095d011d34ed76.tar.gz bun-a3dc33c13350f4227c1cfb669f095d011d34ed76.tar.zst bun-a3dc33c13350f4227c1cfb669f095d011d34ed76.zip |
Wildcard imports map (#1483)
* Log extra info on missing file extension
* Improve error messages for missing /index.js on import
* Remove unnecessary function parameter path
* Add loadPackageImports function to match esbuild
* Add support for pattern trailer import syntax
* Fix review comments
-rw-r--r-- | src/resolver/package_json.zig | 101 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 178 | ||||
-rw-r--r-- | src/string_immutable.zig | 42 |
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: { |