diff options
author | 2021-09-21 18:05:42 -0700 | |
---|---|---|
committer | 2021-09-21 18:05:42 -0700 | |
commit | 5e634cd612fa6320ec0f72eeac2a4c3a2d2f1869 (patch) | |
tree | d397025040a3ebe8c4a02723cae48e6596b5e017 | |
parent | 79c7fe0f7afae7f1746190725394c6440d77f16d (diff) | |
download | bun-5e634cd612fa6320ec0f72eeac2a4c3a2d2f1869.tar.gz bun-5e634cd612fa6320ec0f72eeac2a4c3a2d2f1869.tar.zst bun-5e634cd612fa6320ec0f72eeac2a4c3a2d2f1869.zip |
Parse "exports" field from package.json (WIP)
-rw-r--r-- | src/logger.zig | 24 | ||||
-rw-r--r-- | src/resolver/package_json.zig | 234 | ||||
-rw-r--r-- | src/string_immutable.zig | 13 |
3 files changed, 270 insertions, 1 deletions
diff --git a/src/logger.zig b/src/logger.zig index 338b35f03..b00d30466 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -751,6 +751,30 @@ pub const Log = struct { }); } + pub fn addRangeWarningFmtWithNote( + log: *Log, + source: ?*const Source, + r: Range, + allocator: *std.mem.Allocator, + comptime fmt: string, + args: anytype, + comptime note_fmt: string, + note_args: anytype, + note_range: logger.Range, + ) !void { + if (!Kind.shouldPrint(.warn, log.level)) return; + log.warnings += 1; + + var notes = try allocator.alloc(Data, 1); + notes[0] = rangeData(source, note_range, std.fmt.allocPrint(allocator, note_fmt, note_args) catch unreachable); + + try log.addMsg(Msg{ + .kind = .warn, + .data = rangeData(source, r, std.fmt.allocPrint(allocator, fmt, args) catch unreachable), + .notes = notes, + }); + } + pub fn addWarning(log: *Log, source: ?*const Source, l: Loc, text: string) !void { if (!Kind.shouldPrint(.warn, log.level)) return; log.warnings += 1; diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index 90a9392f6..93ca3b481 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -8,7 +8,7 @@ const js_ast = @import("../js_ast.zig"); const alloc = @import("../alloc.zig"); const fs = @import("../fs.zig"); const resolver = @import("./resolver.zig"); - +const js_lexer = @import("../js_lexer.zig"); // Assume they're not going to have hundreds of main fields or browser map // so use an array-backed hash table instead of bucketed const MainFieldMap = std.StringArrayHashMap(string); @@ -622,3 +622,235 @@ pub const PackageJSON = struct { return @truncate(u32, hasher.final()); } }; + +pub const ExportsMap = struct { + root: Entry, + exports_range: logger.Range = logger.Range.None, + + pub const Status = enum { + Undefined, + UndefinedNoConditionsMatch, // A more friendly error message for when no conditions are matched + Null, + Exact, + Inexact, // This means we may need to try CommonJS-style extension suffixes + + // Module specifier is an invalid URL, package name or package subpath specifier. + InvalidModuleSpecifier, + + // package.json configuration is invalid or contains an invalid configuration. + InvalidPackageConfiguration, + + // Package exports or imports define a target module for the package that is an invalid type or string target. + InvalidPackageTarget, + + // Package exports do not define or permit a target subpath in the package for the given module. + PackagePathNotExported, + + // The package or module requested does not exist. + ModuleNotFound, + + // The resolved path corresponds to a directory, which is not a supported target for module imports. + UnsupportedDirectoryImport, + + pub inline fn isUndefined(this: Status) bool { + return switch (this) { + .Undefined, .UndefinedNoConditionsMatch => true, + else => false, + }; + } + }; + + pub fn parse(allocator: *std.mem.Allocator, source: *const logger.Source, log: *logger.Log, json: js_ast.Expr) ?ExportsMap { + var visitor = Visitor{ .allocator = allocator, .source = source, .log = log }; + + const root = visitor.visit(json); + + if (root.data == .null) { + return null; + } + + return ExportsMap{ .root = root, .exports_range = root.first_token }; + } + + pub const Visitor = struct { + allocator: *std.mem.Allocator, + source: *const logger.Source, + log: *logger.Log, + + pub fn visit(this: Visitor, expr: js_ast.Expr) Entry { + var first_token: logger.Range = logger.Range.None; + + switch (expr.data) { + .e_null => { + return Entry{ .first_token = js_lexer.rangeOfIdentifier(this.source, expr.loc), .data = .{ .@"null" = void{} } }; + }, + .e_string => |str| { + return Entry{ + .data = .{ + .string = str.string(this.allocator) catch unreachable, + }, + .first_token = this.source.rangeOfString(this.source, expr.loc), + }; + }, + .e_array => |e_array| { + var array = this.allocator.alloc(Entry, array.items.len) catch unreachable; + for (e_array.items) |item, i| { + array[i] = this.visit(item); + } + return Entry{ + .data = .{ + .array = array, + }, + .first_token = logger.Range{ .loc = expr.loc, .len = 1 }, + }; + }, + .e_object => |e_obj| { + var map_data = Entry.Data.Map.List{}; + map_data.ensureTotalCapacity(this.allocator, e_obj.*.properties.len) catch unreachable; + var expansion_keys = this.allocator.alloc(string, e_obj.*.properties.len) catch unreachable; + var expansion_key_i: usize = 0; + var map_data_slices = map_data.slice(); + var map_data_keys = map_data_slices.items(.key); + var map_data_ranges = map_data_slices.items(.key_range); + var map_data_entries = map_data_slices.items(.value); + var is_conditional_sugar = false; + first_token.loc = expr.loc; + first_token.len = 1; + for (e_obj.properties) |prop, i| { + const key: string = prop.key.?.data.e_string.string(this.allocator) catch unreachable; + const key_range: logger.Range = this.source.rangeOfString(property.key.?.loc); + + // If exports is an Object with both a key starting with "." and a key + // not starting with ".", throw an Invalid Package Configuration error. + var cur_is_conditional_sugar = !strings.startsWithChar(key, '.'); + if (i == 0) { + is_conditional_sugar = cur_is_conditional_sugar; + } else if (is_conditional_sugar != cur_is_conditional_sugar) { + const prev_key_range = map_data_ranges[i - 1]; + const prev_key = map_data_keys[i - 1]; + this.log.addRangeWarningFmtWithNote( + this.source, + key_range, + this.allocator, + "This object cannot contain keys that both start with \".\" and don't start with \".\"", + .{}, + "The previous key \"{s}\" is incompatible with the current key \"{s}\"", + .{ prev_key, key }, + prev_key_range, + ) catch unreachable; + map_data.deinit(this.allocator); + this.allocator.free(expansion_keys); + return Entry{ + .data = .{ .invalid = void{} }, + .first_token = first_token, + }; + } + + map_data_keys[i] = key; + map_data_ranges[i] = key_range; + map_data_entries[i] = this.visit(prop.value.?); + + if (strings.endsWithAny(key, "/*")) { + expansion_keys[expansion_key_i] = Entry.Data.Map.MapEntry{ + .value = map_data_entries[i], + .key = key, + .key_range = key_range, + }; + expansion_key_i += 1; + } + } + + // this leaks, 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); + + return Entry{ + .data = .{ + .map = Entry.Data.Map{ + .list = map_data, + .expansion_keys = expansion_keys, + }, + }, + .first_token = first_token, + }; + }, + .e_boolean => { + first_token = js_lexer.rangeOfIdentifier(this.source, expr.loc); + }, + .e_number => { + // TODO: range of number + first_token.loc = expr.loc; + first_token.len = 1; + }, + else => { + first_token.loc = expr.loc; + }, + } + + this.log.addRangeWarning(this.source, first_token, "This value must be a string, an object, an array, or null") catch unreachable; + return Entry{ + .data = .{ .invalid = void{} }, + .first_token = first_token, + }; + } + }; + + pub const Entry = struct { + first_token: logger.Range, + data: Data, + + pub const Data = union(Tag) { + invalid: void, + null: void, + boolean: bool, + string: string, + array: []const Entry, + map: Map, + + pub const Tag = enum { + null, + string, + array, + map, + invalid, + }; + + pub const Map = struct { + // This is not a std.ArrayHashMap because we also store the key_range which is a little weird + pub const List = std.MultiArrayList(MapEntry); + expansion_keys: []MapEntry, + list: List, + + pub const MapEntry = struct { + key: string, + key_range: logger.Range, + value: Entry, + }; + }; + }; + + pub fn valueForKey(this: *const Entry, key_: string) ?Entry { + switch (this.data) { + .map => |map| { + var slice = this.data.map.list.slice(); + const keys = slice.items(.key); + for (keys) |key, i| { + if (strings.eql(key, key_)) { + return slice.items(.value)[i]; + } + } + + return null; + }, + else => { + return null; + }, + } + } + }; +}; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 75bfcfd70..b657c26cd 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -206,6 +206,10 @@ pub inline fn endsWith(self: string, str: string) bool { return str.len == 0 or @call(.{ .modifier = .always_inline }, std.mem.endsWith, .{ u8, self, str }); } +pub inline fn startsWithChar(self: string, char: u8) bool { + return self.len > 0 and self[0] == char; +} + pub inline fn endsWithChar(self: string, char: u8) bool { return self.len == 0 or self[self.len - 1] == char; } @@ -755,6 +759,15 @@ pub fn NewCodePointIterator(comptime CodePointType: type, comptime zeroValue: co pub const CodepointIterator = NewCodePointIterator(CodePoint, -1); pub const UnsignedCodepointIterator = NewCodePointIterator(u32, 0); +pub fn NewLengthSorter(comptime Type: type, comptime field: string) type { + return struct { + const LengthSorter = @This(); + pub fn lessThan(context: LengthSorter, lhs: Type, rhs: Type) bool { + return @field(lhs, field).len < @field(rhs, field).len; + } + }; +} + test "join" { var string_list = &[_]string{ "abc", "def", "123", "hello" }; const list = try join(string_list, "-", std.heap.page_allocator); |