Files
bun.sh/src/resolver/package_json.zig
2024-11-22 15:13:32 -08:00

2085 lines
87 KiB
Zig

const bun = @import("root").bun;
const string = bun.string;
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
const strings = bun.strings;
const MutableString = bun.MutableString;
const StoredFileDescriptorType = bun.StoredFileDescriptorType;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const Api = @import("../api/schema.zig").Api;
const std = @import("std");
const options = @import("../options.zig");
const cache = @import("../cache.zig");
const logger = bun.logger;
const js_ast = bun.JSAst;
const fs = @import("../fs.zig");
const resolver = @import("./resolver.zig");
const js_lexer = bun.js_lexer;
const resolve_path = @import("./resolve_path.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 = bun.StringMap;
pub const BrowserMap = bun.StringMap;
pub const MacroImportReplacementMap = bun.StringArrayHashMap(string);
pub const MacroMap = bun.StringArrayHashMapUnmanaged(MacroImportReplacementMap);
const ScriptsMap = bun.StringArrayHashMap(string);
const Semver = @import("../install/semver.zig");
const Dependency = @import("../install/dependency.zig");
const String = @import("../install/semver.zig").String;
const Version = Semver.Version;
const Install = @import("../install/install.zig");
const FolderResolver = @import("../install/resolvers/folder_resolver.zig");
const Architecture = @import("../install/npm.zig").Architecture;
const OperatingSystem = @import("../install/npm.zig").OperatingSystem;
pub const DependencyMap = struct {
map: HashMap = .{},
source_buf: []const u8 = "",
pub const HashMap = std.ArrayHashMapUnmanaged(
String,
Dependency,
String.ArrayHashContext,
false,
);
};
pub const PackageJSON = struct {
pub const LoadFramework = enum {
none,
development,
production,
};
pub usingnamespace bun.New(@This());
pub fn generateHash(package_json: *PackageJSON) void {
var hashy: [1024]u8 = undefined;
@memset(&hashy, 0);
var used: usize = 0;
bun.copy(u8, &hashy, package_json.name);
used = package_json.name.len;
hashy[used] = '@';
used += 1;
bun.copy(u8, hashy[used..], package_json.version);
used += package_json.version.len;
package_json.hash = std.hash.Murmur3_32.hash(hashy[0..used]);
}
const node_modules_path = std.fs.path.sep_str ++ "node_modules" ++ std.fs.path.sep_str;
pub fn nameForImport(this: *const PackageJSON, allocator: std.mem.Allocator) !string {
if (strings.indexOf(this.source.path.text, node_modules_path)) |_| {
return this.name;
} else {
const parent = this.source.path.name.dirWithTrailingSlash();
if (strings.indexOf(parent, fs.FileSystem.instance.top_level_dir)) |i| {
const relative_dir = parent[i + fs.FileSystem.instance.top_level_dir.len ..];
var out_dir = try allocator.alloc(u8, relative_dir.len + 2);
bun.copy(u8, out_dir[2..], relative_dir);
out_dir[0..2].* = ("." ++ std.fs.path.sep_str).*;
return out_dir;
}
return this.name;
}
}
pub const FrameworkRouterPair = struct {
framework: *options.Framework,
router: *options.RouteConfig,
loaded_routes: bool = false,
};
name: string = "",
source: logger.Source,
main_fields: MainFieldMap,
module_type: options.ModuleType,
version: string = "",
hash: u32 = 0xDEADBEEF,
scripts: ?*ScriptsMap = null,
config: ?*bun.StringArrayHashMap(string) = null,
arch: Architecture = Architecture.all,
os: OperatingSystem = OperatingSystem.all,
package_manager_package_id: Install.PackageID = Install.invalid_package_id,
dependencies: DependencyMap = .{},
side_effects: SideEffects = .unspecified,
// Present if the "browser" field is present. This field is intended to be
// used by bundlers and lets you redirect the paths of certain 3rd-party
// modules that don't work in the browser to other modules that shim that
// functionality. That way you don't have to rewrite the code for those 3rd-
// party modules. For example, you might remap the native "util" node module
// to something like https://www.npmjs.com/package/util so it works in the
// browser.
//
// This field contains a mapping of absolute paths to absolute paths. Mapping
// to an empty path indicates that the module is disabled. As far as I can
// tell, the official spec is an abandoned GitHub repo hosted by a user account:
// https://github.com/defunctzombie/package-browser-field-spec. The npm docs
// say almost nothing: https://docs.npmjs.com/files/package.json.
//
// Note that the non-package "browser" map has to be checked twice to match
// Webpack's behavior: once before resolution and once after resolution. It
// leads to some unintuitive failure cases that we must emulate around missing
// file extensions:
//
// * Given the mapping "./no-ext": "./no-ext-browser.js" the query "./no-ext"
// should match but the query "./no-ext.js" should NOT match.
//
// * Given the mapping "./ext.js": "./ext-browser.js" the query "./ext.js"
// should match and the query "./ext" should ALSO match.
//
browser_map: BrowserMap,
exports: ?ExportsMap = null,
imports: ?ExportsMap = null,
pub const SideEffects = union(enum) {
/// either `package.json` is missing "sideEffects", it is true, or some
/// other unsupported value. Treat all files as side effects
unspecified,
/// "sideEffects": false
false,
/// "sideEffects": ["file.js", "other.js"]
map: Map,
// /// "sideEffects": ["side_effects/*.js"]
// glob: TODO,
pub const Map = std.HashMapUnmanaged(
bun.StringHashMapUnowned.Key,
void,
bun.StringHashMapUnowned.Adapter,
80,
);
pub fn hasSideEffects(side_effects: SideEffects, path: []const u8) bool {
return switch (side_effects) {
.unspecified => true,
.false => false,
.map => |map| map.contains(bun.StringHashMapUnowned.Key.init(path)),
};
}
};
pub inline fn isAppPackage(this: *const PackageJSON) bool {
return this.hash == 0xDEADBEEF;
}
fn loadDefineDefaults(
env: *options.Env,
json: *const js_ast.E.Object,
allocator: std.mem.Allocator,
) !void {
var valid_count: usize = 0;
for (json.properties.slice()) |prop| {
if (prop.value.?.data != .e_string) continue;
valid_count += 1;
}
env.defaults.shrinkRetainingCapacity(0);
env.defaults.ensureTotalCapacity(allocator, valid_count) catch {};
for (json.properties.slice()) |prop| {
if (prop.value.?.data != .e_string) continue;
env.defaults.appendAssumeCapacity(.{
.key = prop.key.?.data.e_string.string(allocator) catch unreachable,
.value = prop.value.?.data.e_string.string(allocator) catch unreachable,
});
}
}
fn loadOverrides(
framework: *options.Framework,
json: *const js_ast.E.Object,
allocator: std.mem.Allocator,
) void {
var valid_count: usize = 0;
for (json.properties.slice()) |prop| {
if (prop.value.?.data != .e_string) continue;
valid_count += 1;
}
var buffer = allocator.alloc([]const u8, valid_count * 2) catch unreachable;
var keys = buffer[0..valid_count];
var values = buffer[valid_count..];
var i: usize = 0;
for (json.properties.slice()) |prop| {
if (prop.value.?.data != .e_string) continue;
keys[i] = prop.key.?.data.e_string.string(allocator) catch unreachable;
values[i] = prop.value.?.data.e_string.string(allocator) catch unreachable;
i += 1;
}
framework.override_modules = Api.StringMap{ .keys = keys, .values = values };
}
fn loadDefineExpression(
env: *options.Env,
json: *const js_ast.E.Object,
allocator: std.mem.Allocator,
) anyerror!void {
for (json.properties.slice()) |prop| {
switch (prop.key.?.data) {
.e_string => |e_str| {
const str = e_str.string(allocator) catch "";
if (strings.eqlComptime(str, "defaults")) {
switch (prop.value.?.data) {
.e_object => |obj| {
try loadDefineDefaults(env, obj, allocator);
},
else => {
env.defaults.shrinkRetainingCapacity(0);
},
}
} else if (strings.eqlComptime(str, ".env")) {
switch (prop.value.?.data) {
.e_string => |value_str| {
env.setBehaviorFromPrefix(value_str.string(allocator) catch "");
},
else => {
env.behavior = .disable;
env.prefix = "";
},
}
}
},
else => continue,
}
}
}
fn loadFrameworkExpression(
framework: *options.Framework,
json: js_ast.Expr,
allocator: std.mem.Allocator,
comptime read_define: bool,
) bool {
if (json.asProperty("client")) |client| {
if (client.expr.asString(allocator)) |str| {
if (str.len > 0) {
framework.client.path = str;
framework.client.kind = .client;
}
}
}
if (json.asProperty("fallback")) |client| {
if (client.expr.asString(allocator)) |str| {
if (str.len > 0) {
framework.fallback.path = str;
framework.fallback.kind = .fallback;
}
}
}
if (json.asProperty("css")) |css_prop| {
if (css_prop.expr.asString(allocator)) |str| {
if (strings.eqlComptime(str, "onimportcss")) {
framework.client_css_in_js = .facade_onimportcss;
} else {
framework.client_css_in_js = .facade;
}
}
}
if (json.asProperty("override")) |override| {
if (override.expr.data == .e_object) {
loadOverrides(framework, override.expr.data.e_object, allocator);
}
}
if (comptime read_define) {
if (json.asProperty("define")) |defines| {
var skip_fallback = false;
if (defines.expr.asProperty("client")) |client| {
if (client.expr.data == .e_object) {
const object = client.expr.data.e_object;
framework.client.env = options.Env.init(
allocator,
);
loadDefineExpression(&framework.client.env, object, allocator) catch {};
framework.fallback.env = framework.client.env;
skip_fallback = true;
}
}
if (!skip_fallback) {
if (defines.expr.asProperty("fallback")) |client| {
if (client.expr.data == .e_object) {
const object = client.expr.data.e_object;
framework.fallback.env = options.Env.init(
allocator,
);
loadDefineExpression(&framework.fallback.env, object, allocator) catch {};
}
}
}
if (defines.expr.asProperty("server")) |server| {
if (server.expr.data == .e_object) {
const object = server.expr.data.e_object;
framework.server.env = options.Env.init(
allocator,
);
loadDefineExpression(&framework.server.env, object, allocator) catch {};
}
}
}
}
if (json.asProperty("server")) |server| {
if (server.expr.asString(allocator)) |str| {
if (str.len > 0) {
framework.server.path = str;
framework.server.kind = .server;
}
}
}
return framework.client.isEnabled() or framework.server.isEnabled() or framework.fallback.isEnabled();
}
pub fn loadFrameworkWithPreference(
package_json: *const PackageJSON,
pair: *FrameworkRouterPair,
json: js_ast.Expr,
allocator: std.mem.Allocator,
comptime read_defines: bool,
comptime load_framework: LoadFramework,
) void {
const framework_object = json.asProperty("framework") orelse return;
if (framework_object.expr.asProperty("displayName")) |name| {
if (name.expr.asString(allocator)) |str| {
if (str.len > 0) {
pair.framework.display_name = str;
}
}
}
if (json.get("version")) |version| {
if (version.asString(allocator)) |str| {
if (str.len > 0) {
pair.framework.version = str;
}
}
}
if (framework_object.expr.asProperty("static")) |static_prop| {
if (static_prop.expr.asString(allocator)) |str| {
if (str.len > 0) {
pair.router.static_dir = str;
pair.router.static_dir_enabled = true;
}
}
}
if (framework_object.expr.asProperty("assetPrefix")) |asset_prefix| {
if (asset_prefix.expr.asString(allocator)) |_str| {
const str = std.mem.trimRight(u8, _str, " ");
if (str.len > 0) {
pair.router.asset_prefix_path = str;
}
}
}
if (!pair.router.routes_enabled) {
if (framework_object.expr.asProperty("router")) |router| {
if (router.expr.asProperty("dir")) |route_dir| {
switch (route_dir.expr.data) {
.e_string => |estr| {
const str = estr.string(allocator) catch unreachable;
if (str.len > 0) {
pair.router.dir = str;
pair.router.possible_dirs = &[_]string{};
pair.loaded_routes = true;
}
},
.e_array => |array| {
var count: usize = 0;
const items = array.items.slice();
for (items) |item| {
count += @intFromBool(item.data == .e_string and item.data.e_string.data.len > 0);
}
switch (count) {
0 => {},
1 => {
const str = items[0].data.e_string.string(allocator) catch unreachable;
if (str.len > 0) {
pair.router.dir = str;
pair.router.possible_dirs = &[_]string{};
pair.loaded_routes = true;
}
},
else => {
const list = allocator.alloc(string, count) catch unreachable;
var list_i: usize = 0;
for (items) |item| {
if (item.data == .e_string and item.data.e_string.data.len > 0) {
list[list_i] = item.data.e_string.string(allocator) catch unreachable;
list_i += 1;
}
}
pair.router.dir = list[0];
pair.router.possible_dirs = list;
pair.loaded_routes = true;
},
}
},
else => {},
}
}
if (router.expr.asProperty("extensions")) |extensions_expr| {
if (extensions_expr.expr.asArray()) |array_const| {
var array = array_const;
var valid_count: usize = 0;
while (array.next()) |expr| {
if (expr.data != .e_string) continue;
const e_str: *const js_ast.E.String = expr.data.e_string;
if (e_str.data.len == 0 or e_str.data[0] != '.') continue;
valid_count += 1;
}
if (valid_count > 0) {
var extensions = allocator.alloc(string, valid_count) catch unreachable;
array.index = 0;
var i: usize = 0;
// We don't need to allocate the strings because we keep the package.json source string in memory
while (array.next()) |expr| {
if (expr.data != .e_string) continue;
const e_str: *const js_ast.E.String = expr.data.e_string;
if (e_str.data.len == 0 or e_str.data[0] != '.') continue;
extensions[i] = e_str.data;
i += 1;
}
}
}
}
}
}
switch (comptime load_framework) {
.development => {
if (framework_object.expr.asProperty("development")) |env| {
if (loadFrameworkExpression(pair.framework, env.expr, allocator, read_defines)) {
pair.framework.package = package_json.nameForImport(allocator) catch unreachable;
pair.framework.development = true;
if (env.expr.asProperty("static")) |static_prop| {
if (static_prop.expr.asString(allocator)) |str| {
if (str.len > 0) {
pair.router.static_dir = str;
pair.router.static_dir_enabled = true;
}
}
}
return;
}
}
},
.production => {
if (framework_object.expr.asProperty("production")) |env| {
if (loadFrameworkExpression(pair.framework, env.expr, allocator, read_defines)) {
pair.framework.package = package_json.nameForImport(allocator) catch unreachable;
pair.framework.development = false;
if (env.expr.asProperty("static")) |static_prop| {
if (static_prop.expr.asString(allocator)) |str| {
if (str.len > 0) {
pair.router.static_dir = str;
pair.router.static_dir_enabled = true;
}
}
}
return;
}
}
},
else => @compileError("unreachable"),
}
if (loadFrameworkExpression(pair.framework, framework_object.expr, allocator, read_defines)) {
pair.framework.package = package_json.nameForImport(allocator) catch unreachable;
pair.framework.development = false;
}
}
pub fn parseMacrosJSON(
allocator: std.mem.Allocator,
macros: js_ast.Expr,
log: *logger.Log,
json_source: *const logger.Source,
) MacroMap {
var macro_map = MacroMap{};
if (macros.data != .e_object) return macro_map;
const properties = macros.data.e_object.properties.slice();
for (properties) |property| {
const key = property.key.?.asString(allocator) orelse continue;
if (!resolver.isPackagePath(key)) {
log.addRangeWarningFmt(
json_source,
json_source.rangeOfString(property.key.?.loc),
allocator,
"\"{s}\" is not a package path. \"macros\" remaps package paths to macros. Skipping.",
.{key},
) catch unreachable;
continue;
}
const value = property.value.?;
if (value.data != .e_object) {
log.addWarningFmt(
json_source,
value.loc,
allocator,
"Invalid macro remapping in \"{s}\": expected object where the keys are import names and the value is a string path to replace",
.{key},
) catch unreachable;
continue;
}
const remap_properties = value.data.e_object.properties.slice();
if (remap_properties.len == 0) continue;
var map = MacroImportReplacementMap.init(allocator);
map.ensureUnusedCapacity(remap_properties.len) catch unreachable;
for (remap_properties) |remap| {
const import_name = remap.key.?.asString(allocator) orelse continue;
const remap_value = remap.value.?;
if (remap_value.data != .e_string or remap_value.data.e_string.data.len == 0) {
log.addWarningFmt(
json_source,
remap_value.loc,
allocator,
"Invalid macro remapping for import \"{s}\": expected string to remap to. e.g. \"graphql\": \"bun-macro-relay\" ",
.{import_name},
) catch unreachable;
continue;
}
const remap_value_str = remap_value.data.e_string.data;
map.putAssumeCapacityNoClobber(import_name, remap_value_str);
}
if (map.count() > 0) {
macro_map.put(allocator, key, map) catch unreachable;
}
}
return macro_map;
}
pub fn parse(
r: *resolver.Resolver,
input_path: string,
dirname_fd: StoredFileDescriptorType,
package_id: ?Install.PackageID,
comptime include_scripts_: @Type(.EnumLiteral),
comptime include_dependencies: @Type(.EnumLiteral),
comptime generate_hash_: @Type(.EnumLiteral),
) ?PackageJSON {
const generate_hash = generate_hash_ == .generate_hash;
const include_scripts = include_scripts_ == .include_scripts;
// TODO: remove this extra copy
const parts = [_]string{ input_path, "package.json" };
const package_json_path_ = r.fs.abs(&parts);
const package_json_path = r.fs.dirname_store.append(@TypeOf(package_json_path_), package_json_path_) catch unreachable;
// DirInfo cache is reused globally
// So we cannot free these
const allocator = bun.fs_allocator;
var entry = r.caches.fs.readFileWithAllocator(
allocator,
r.fs,
package_json_path,
dirname_fd,
false,
null,
) catch |err| {
if (err != error.IsDir) {
r.log.addErrorFmt(null, logger.Loc.Empty, allocator, "Cannot read file \"{s}\": {s}", .{ r.prettyPath(fs.Path.init(input_path)), @errorName(err) }) catch unreachable;
}
return null;
};
defer _ = entry.closeFD();
if (r.debug_logs) |*debug| {
debug.addNoteFmt("The file \"{s}\" exists", .{package_json_path});
}
const key_path = fs.Path.init(package_json_path);
var json_source = logger.Source.initPathString(key_path.text, entry.contents);
json_source.path.pretty = r.prettyPath(json_source.path);
const json: js_ast.Expr = (r.caches.json.parsePackageJSON(r.log, json_source, allocator, true) catch |err| {
if (Environment.isDebug) {
Output.printError("{s}: JSON parse error: {s}", .{ package_json_path, @errorName(err) });
}
return null;
} orelse return null);
if (json.data != .e_object) {
// Invalid package.json in node_modules is noisy.
// Let's just ignore it.
allocator.free(entry.contents);
return null;
}
var package_json = PackageJSON{
.name = "",
.version = "",
.hash = 0xDEADBEEF,
.source = json_source,
.module_type = .unknown,
.browser_map = BrowserMap.init(allocator, false),
.main_fields = MainFieldMap.init(allocator, false),
};
// Note: we tried rewriting this to be fewer loops over all the properties (asProperty loops over each)
// The end result was: it's not faster! Sometimes, it's slower.
// It's hard to say why.
// Feels like a codegen issue.
// or that looping over every property doesn't really matter because most package.jsons are < 20 properties
if (json.asProperty("version")) |version_json| {
if (version_json.expr.asString(allocator)) |version_str| {
if (version_str.len > 0) {
package_json.version = allocator.dupe(u8, version_str) catch unreachable;
}
}
}
if (json.asProperty("name")) |version_json| {
if (version_json.expr.asString(allocator)) |version_str| {
if (version_str.len > 0) {
package_json.name = allocator.dupe(u8, version_str) catch unreachable;
}
}
}
// If we're coming from `bun run`
// We do not need to parse all this stuff.
if (comptime !include_scripts) {
if (json.asProperty("type")) |type_json| {
if (type_json.expr.asString(allocator)) |type_str| {
switch (options.ModuleType.List.get(type_str) orelse options.ModuleType.unknown) {
.cjs => {
package_json.module_type = .cjs;
},
.esm => {
package_json.module_type = .esm;
},
.unknown => {
r.log.addRangeWarningFmt(
&json_source,
json_source.rangeOfString(type_json.loc),
allocator,
"\"{s}\" is not a valid value for \"type\" field (must be either \"commonjs\" or \"module\")",
.{type_str},
) catch unreachable;
},
}
} else {
r.log.addWarning(&json_source, type_json.loc, "The value for \"type\" must be a string") catch unreachable;
}
}
// Read the "main" fields
for (r.opts.main_fields) |main| {
if (json.asProperty(main)) |main_json| {
const expr: js_ast.Expr = main_json.expr;
if ((expr.asString(allocator))) |str| {
if (str.len > 0) {
package_json.main_fields.put(main, str) catch unreachable;
}
}
}
}
// Read the "browser" property, but only when targeting the browser
if (r.opts.target == .browser) {
// We both want the ability to have the option of CJS vs. ESM and the
// option of having node vs. browser. The way to do this is to use the
// object literal form of the "browser" field like this:
//
// "main": "dist/index.node.cjs.js",
// "module": "dist/index.node.esm.js",
// "browser": {
// "./dist/index.node.cjs.js": "./dist/index.browser.cjs.js",
// "./dist/index.node.esm.js": "./dist/index.browser.esm.js"
// },
//
if (json.asProperty("browser")) |browser_prop| {
switch (browser_prop.expr.data) {
.e_object => |obj| {
// The value is an object
// Remap all files in the browser field
for (obj.properties.slice()) |*prop| {
const _key_str = (prop.key orelse continue).asString(allocator) orelse continue;
const value: js_ast.Expr = prop.value orelse continue;
// Normalize the path so we can compare against it without getting
// confused by "./". There is no distinction between package paths and
// relative paths for these values because some tools (i.e. Browserify)
// don't make such a distinction.
//
// This leads to weird things like a mapping for "./foo" matching an
// import of "foo", but that's actually not a bug. Or arguably it's a
// bug in Browserify but we have to replicate this bug because packages
// do this in the wild.
const key = allocator.dupe(u8, r.fs.normalize(_key_str)) catch unreachable;
switch (value.data) {
.e_string => |str| {
// If this is a string, it's a replacement package
package_json.browser_map.put(key, str.string(allocator) catch unreachable) catch unreachable;
},
.e_boolean => |boolean| {
if (!boolean.value) {
package_json.browser_map.put(key, "") catch unreachable;
}
},
else => {
r.log.addWarning(&json_source, value.loc, "Each \"browser\" mapping must be a string or boolean") catch unreachable;
},
}
}
},
else => {},
}
}
}
if (json.asProperty("exports")) |exports_prop| {
if (ExportsMap.parse(bun.default_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(bun.default_allocator, &json_source, r.log, imports_prop.expr, imports_prop.loc)) |imports_map| {
package_json.imports = imports_map;
}
}
if (json.get("sideEffects")) |side_effects_field| outer: {
if (side_effects_field.asBool()) |boolean| {
if (!boolean)
package_json.side_effects = .{ .false = {} };
} else if (side_effects_field.asArray()) |array_| {
var array = array_;
// TODO: switch to only storing hashes
var map = SideEffects.Map{};
map.ensureTotalCapacity(allocator, array.array.items.len) catch unreachable;
while (array.next()) |item| {
if (item.asString(allocator)) |name| {
// TODO: support RegExp using JavaScriptCore <> C++ bindings
if (strings.containsChar(name, '*')) {
// https://sourcegraph.com/search?q=context:global+file:package.json+sideEffects%22:+%5B&patternType=standard&sm=1&groupBy=repo
// a lot of these seem to be css files which we don't care about for now anyway
// so we can just skip them in here
if (strings.eqlComptime(std.fs.path.extension(name), ".css"))
continue;
r.log.addWarning(
&json_source,
item.loc,
"wildcard sideEffects are not supported yet, which means this package will be deoptimized",
) catch unreachable;
map.deinit(allocator);
package_json.side_effects = .{ .unspecified = {} };
break :outer;
}
var joined = [_]string{
json_source.path.name.dirWithTrailingSlash(),
name,
};
_ = map.getOrPutAssumeCapacity(
bun.StringHashMapUnowned.Key.init(r.fs.join(&joined)),
);
}
}
package_json.side_effects = .{ .map = map };
}
}
}
if (comptime include_dependencies == .main or include_dependencies == .local) {
update_dependencies: {
if (package_id) |pkg| {
package_json.package_manager_package_id = pkg;
break :update_dependencies;
}
// // if there is a name & version, check if the lockfile has the package
if (package_json.name.len > 0 and package_json.version.len > 0) {
if (r.package_manager) |pm| {
const tag = Dependency.Version.Tag.infer(package_json.version);
if (tag == .npm) {
const sliced = Semver.SlicedString.init(package_json.version, package_json.version);
if (Dependency.parseWithTag(
allocator,
String.init(package_json.name, package_json.name),
String.Builder.stringHash(package_json.name),
package_json.version,
.npm,
&sliced,
r.log,
pm,
)) |dependency_version| {
if (dependency_version.value.npm.version.isExact()) {
if (pm.lockfile.resolve(package_json.name, dependency_version)) |resolved| {
package_json.package_manager_package_id = resolved;
if (resolved > 0) {
break :update_dependencies;
}
}
}
}
}
}
}
if (json.get("cpu")) |os_field| {
if (os_field.asArray()) |array_const| {
var array = array_const;
var arch = Architecture.none.negatable();
while (array.next()) |item| {
if (item.asString(bun.default_allocator)) |str| {
arch.apply(str);
}
}
package_json.arch = arch.combine();
}
}
if (json.get("os")) |os_field| {
var tmp = os_field.asArray();
if (tmp) |*array| {
var os = OperatingSystem.none.negatable();
while (array.next()) |item| {
if (item.asString(bun.default_allocator)) |str| {
os.apply(str);
}
}
package_json.os = os.combine();
}
}
const DependencyGroup = Install.Lockfile.Package.DependencyGroup;
const features = .{
.dependencies = true,
.dev_dependencies = include_dependencies == .main,
.optional_dependencies = true,
.peer_dependencies = false,
};
const dependency_groups = comptime brk: {
var out_groups: [
@as(usize, @intFromBool(features.dependencies)) +
@as(usize, @intFromBool(features.dev_dependencies)) +
@as(usize, @intFromBool(features.optional_dependencies)) +
@as(usize, @intFromBool(features.peer_dependencies))
]DependencyGroup = undefined;
var out_group_i: usize = 0;
if (features.dependencies) {
out_groups[out_group_i] = DependencyGroup.dependencies;
out_group_i += 1;
}
if (features.dev_dependencies) {
out_groups[out_group_i] = DependencyGroup.dev;
out_group_i += 1;
}
if (features.optional_dependencies) {
out_groups[out_group_i] = DependencyGroup.optional;
out_group_i += 1;
}
if (features.peer_dependencies) {
out_groups[out_group_i] = DependencyGroup.peer;
out_group_i += 1;
}
break :brk out_groups;
};
var total_dependency_count: usize = 0;
inline for (dependency_groups) |group| {
if (json.get(group.field)) |group_json| {
if (group_json.data == .e_object) {
total_dependency_count += group_json.data.e_object.properties.len;
}
}
}
if (total_dependency_count > 0) {
package_json.dependencies.map = DependencyMap.HashMap{};
package_json.dependencies.source_buf = json_source.contents;
const ctx = String.ArrayHashContext{
.a_buf = json_source.contents,
.b_buf = json_source.contents,
};
package_json.dependencies.map.ensureTotalCapacityContext(
allocator,
total_dependency_count,
ctx,
) catch unreachable;
inline for (dependency_groups) |group| {
if (json.get(group.field)) |group_json| {
if (group_json.data == .e_object) {
var group_obj = group_json.data.e_object;
for (group_obj.properties.slice()) |*prop| {
const name_prop = prop.key orelse continue;
const name_str = name_prop.asString(allocator) orelse continue;
const name_hash = String.Builder.stringHash(name_str);
const name = String.init(name_str, name_str);
const version_value = prop.value orelse continue;
const version_str = version_value.asString(allocator) orelse continue;
const sliced_str = Semver.SlicedString.init(version_str, version_str);
if (Dependency.parse(
allocator,
name,
name_hash,
version_str,
&sliced_str,
r.log,
r.package_manager,
)) |dependency_version| {
const dependency = Dependency{
.name = name,
.version = dependency_version,
.name_hash = name_hash,
.behavior = group.behavior,
};
package_json.dependencies.map.putAssumeCapacityContext(
dependency.name,
dependency,
ctx,
);
}
}
}
}
}
}
}
}
// used by `bun run`
if (include_scripts) {
if (json.asPropertyStringMap("scripts", allocator)) |scripts| {
package_json.scripts = scripts;
}
if (json.asPropertyStringMap("config", allocator)) |config| {
package_json.config = config;
}
}
if (generate_hash) {
if (package_json.name.len > 0 and package_json.version.len > 0) {
package_json.generateHash();
}
}
return package_json;
}
pub fn hashModule(this: *const PackageJSON, module: string) u32 {
var hasher = bun.Wyhash.init(0);
hasher.update(std.mem.asBytes(&this.hash));
hasher.update(module);
return @as(u32, @truncate(hasher.final()));
}
};
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, property_key_loc: logger.Loc) ?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 = source.rangeOfString(json.loc),
.property_key_loc = property_key_loc,
};
}
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 = {} } };
},
.e_string => |str| {
return Entry{
.data = .{
.string = str.slice(this.allocator),
},
.first_token = this.source.rangeOfString(expr.loc),
};
},
.e_array => |e_array| {
const array = this.allocator.alloc(Entry, e_array.items.len) catch unreachable;
for (e_array.items.slice(), array) |item, *dest| {
dest.* = 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;
map_data.len = e_obj.*.properties.len;
var expansion_keys = this.allocator.alloc(Entry.Data.Map.MapEntry, 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.slice(), 0..) |prop, i| {
const key: string = prop.key.?.data.e_string.slice(this.allocator);
const key_range: logger.Range = this.source.rangeOfString(prop.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.
const 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 = {} },
.first_token = first_token,
};
}
map_data_keys[i] = key;
map_data_ranges[i] = key_range;
map_data_entries[i] = this.visit(prop.value.?);
// safe to use "/" on windows. exports in package.json does not use "\\"
if (strings.endsWithComptime(key, "/") or strings.containsChar(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 a lil, but it's fine.
expansion_keys = expansion_keys[0..expansion_key_i];
// 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");
const sorter = GlobLengthSorter{};
std.sort.pdq(Entry.Data.Map.MapEntry, expansion_keys, sorter, GlobLengthSorter.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 = {} },
.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 {
invalid,
null,
boolean,
string,
array,
map,
};
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 keysStartWithDot(this: *const Entry) bool {
return this.data == .map and this.data.map.list.len > 0 and strings.startsWithChar(this.data.map.list.items(.key)[0], '.');
}
pub fn valueForKey(this: *const Entry, key_: string) ?Entry {
switch (this.data) {
.map => {
var slice = this.data.map.list.slice();
const keys = slice.items(.key);
for (keys, 0..) |key, i| {
if (strings.eql(key, key_)) {
return slice.items(.value)[i];
}
}
return null;
},
else => {
return null;
},
}
}
};
};
pub const ESModule = struct {
pub const ConditionsMap = bun.StringArrayHashMap(void);
debug_logs: ?*resolver.DebugLogs = null,
conditions: ConditionsMap,
allocator: std.mem.Allocator,
module_type: *options.ModuleType = undefined,
pub const Resolution = struct {
status: Status = Status.Undefined,
path: string = "",
debug: Debug = Debug{},
pub const Debug = struct {
// This is the range of the token to use for error messages
token: logger.Range = logger.Range.None,
// If the status is "UndefinedNoConditionsMatch", this is the set of
// conditions that didn't match. This information is used for error messages.
unmatched_conditions: []string = &[_]string{},
};
};
pub const Status = enum {
Undefined,
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.
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 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,
// The internal #import specifier was not found
PackageImportNotDefined,
PackageResolve,
pub inline fn isUndefined(this: Status) bool {
return switch (this) {
.Undefined, .UndefinedNoConditionsMatch => true,
else => false,
};
}
};
pub const Package = struct {
name: string,
version: string = "",
subpath: string,
pub const External = struct {
name: Semver.String = .{},
version: Semver.String = .{},
subpath: Semver.String = .{},
};
pub fn count(this: Package, builder: *Semver.String.Builder) void {
builder.count(this.name);
builder.count(this.version);
builder.count(this.subpath);
}
pub fn clone(this: Package, builder: *Semver.String.Builder) External {
return .{
.name = builder.appendUTF8WithoutPool(Semver.String, this.name, 0),
.version = builder.appendUTF8WithoutPool(Semver.String, this.version, 0),
.subpath = builder.appendUTF8WithoutPool(Semver.String, this.subpath, 0),
};
}
pub fn toExternal(this: Package, buffer: []const u8) External {
return .{
.name = Semver.String.init(buffer, this.name),
.version = Semver.String.init(buffer, this.version),
.subpath = Semver.String.init(buffer, this.subpath),
};
}
pub fn withAutoVersion(this: Package) Package {
if (this.version.len == 0) {
return .{
.name = this.name,
.subpath = this.subpath,
.version = "latest",
};
}
return this;
}
pub fn parseName(specifier: string) ?string {
var slash = strings.indexOfCharNeg(specifier, '/');
if (!strings.startsWithChar(specifier, '@')) {
slash = if (slash == -1) @as(i32, @intCast(specifier.len)) else slash;
return specifier[0..@as(usize, @intCast(slash))];
} else {
if (slash == -1) return null;
const slash2 = strings.indexOfChar(specifier[@as(usize, @intCast(slash)) + 1 ..], '/') orelse
specifier[@as(u32, @intCast(slash + 1))..].len;
return specifier[0 .. @as(usize, @intCast(slash + 1)) + slash2];
}
}
pub fn parseVersion(specifier_after_name: string) ?string {
if (strings.indexOfChar(specifier_after_name, '/')) |slash| {
// "foo@/bar" is not a valid specifier\
// "foo@/" is not a valid specifier
// "foo/@/bar" is not a valid specifier
// "foo@1/bar" is a valid specifier
// "foo@^123.2.3+ba-ab/bar" is a valid specifier
// ^^^^^^^^^^^^^^
// this is the version
const remainder = specifier_after_name[0..slash];
if (remainder.len > 0 and remainder[0] == '@') {
return remainder[1..];
}
return remainder;
}
return null;
}
pub fn parse(specifier: string, subpath_buf: []u8) ?Package {
if (specifier.len == 0) return null;
var package = Package{ .name = parseName(specifier) orelse return null, .subpath = "" };
if (strings.startsWith(package.name, ".") or strings.indexAnyComptime(package.name, "\\%") != null)
return null;
const offset: usize = if (package.name.len == 0 or package.name[0] != '@') 0 else 1;
if (strings.indexOfChar(specifier[offset..], '@')) |at| {
package.version = parseVersion(specifier[offset..][at..]) orelse "";
if (package.version.len == 0) {
package.version = specifier[offset..][at..];
if (package.version.len > 0 and package.version[0] == '@') {
package.version = package.version[1..];
}
}
package.name = specifier[0 .. at + offset];
parseSubpath(&package.subpath, specifier[@min(package.name.len + package.version.len + 1, specifier.len)..], subpath_buf);
} else {
parseSubpath(&package.subpath, specifier[package.name.len..], subpath_buf);
}
return package;
}
pub fn parseSubpath(subpath: *[]const u8, specifier: string, subpath_buf: []u8) void {
subpath_buf[0] = '.';
bun.copy(u8, subpath_buf[1..], specifier);
subpath.* = subpath_buf[0 .. specifier.len + 1];
}
};
const ReverseKind = enum { exact, pattern, prefix };
pub const ReverseResolution = struct {
subpath: string = "",
token: logger.Range = logger.Range.None,
};
const invalid_percent_chars = [_]string{
"%2f",
"%2F",
"%5c",
"%5C",
};
threadlocal var resolved_path_buf_percent: bun.PathBuffer = undefined;
pub fn resolve(r: *const ESModule, package_url: string, subpath: string, exports: ExportsMap.Entry) Resolution {
return finalize(
r.resolveExports(package_url, subpath, exports),
);
}
pub fn resolveImports(r: *const ESModule, specifier: string, imports: ExportsMap.Entry) Resolution {
if (imports.data != .map) {
return .{
.status = .InvalidPackageConfiguration,
.debug = .{
.token = logger.Range.None,
},
};
}
const result = r.resolveImportsExports(
specifier,
imports,
true,
"/",
);
switch (result.status) {
.Undefined, .Null => {
return .{ .status = .PackageImportNotDefined, .debug = .{ .token = result.debug.token } };
},
else => {
return finalize(result);
},
}
}
pub fn finalize(result_: Resolution) Resolution {
var result = result_;
if (result.status != .Exact and result.status != .ExactEndsWithStar and result.status != .Inexact) {
return result;
}
// If resolved contains any percent encodings of "/" or "\" ("%2f" and "%5C"
// respectively), then throw an Invalid Module Specifier error.
const PercentEncoding = @import("../url.zig").PercentEncoding;
var fbs = std.io.fixedBufferStream(&resolved_path_buf_percent);
var writer = fbs.writer();
const len = PercentEncoding.decode(@TypeOf(&writer), &writer, result.path) catch return Resolution{
.status = .InvalidModuleSpecifier,
.path = result.path,
.debug = result.debug,
};
const resolved_path = resolved_path_buf_percent[0..len];
var found: string = "";
if (strings.contains(resolved_path, invalid_percent_chars[0])) {
found = invalid_percent_chars[0];
} else if (strings.contains(resolved_path, invalid_percent_chars[1])) {
found = invalid_percent_chars[1];
} else if (strings.contains(resolved_path, invalid_percent_chars[2])) {
found = invalid_percent_chars[2];
} else if (strings.contains(resolved_path, invalid_percent_chars[3])) {
found = invalid_percent_chars[3];
}
if (found.len != 0) {
return Resolution{ .status = .InvalidModuleSpecifier, .path = result.path, .debug = result.debug };
}
// If resolved is a directory, throw an Unsupported Directory Import error.
if (strings.endsWithAnyComptime(resolved_path, "/\\")) {
return Resolution{ .status = .UnsupportedDirectoryImport, .path = result.path, .debug = result.debug };
}
result.path = resolved_path;
return result;
}
fn resolveExports(
r: *const ESModule,
package_url: string,
subpath: string,
exports: ExportsMap.Entry,
) Resolution {
if (exports.data == .invalid) {
if (r.debug_logs) |logs| {
logs.addNote("Invalid package configuration");
}
return Resolution{ .status = .InvalidPackageConfiguration, .debug = .{ .token = exports.first_token } };
}
if (strings.eqlComptime(subpath, ".")) {
var main_export = ExportsMap.Entry{ .data = .{ .null = {} }, .first_token = logger.Range.None };
if (switch (exports.data) {
.string,
.array,
=> true,
.map => !exports.keysStartWithDot(),
else => false,
}) {
main_export = exports;
} else if (exports.data == .map) {
if (exports.valueForKey(".")) |value| {
main_export = value;
}
}
if (main_export.data != .null) {
const result = r.resolveTarget(package_url, main_export, "", false, false);
if (result.status != .Null and result.status != .Undefined) {
return result;
}
}
} else if (exports.data == .map and exports.keysStartWithDot()) {
const result = r.resolveImportsExports(subpath, exports, false, package_url);
if (result.status != .Null and result.status != .Undefined) {
return result;
}
if (result.status == .Null) {
return Resolution{ .status = .PackagePathDisabled, .debug = .{ .token = exports.first_token } };
}
}
if (r.debug_logs) |logs| {
logs.addNoteFmt("The path \"{s}\" was not exported", .{subpath});
}
return Resolution{ .status = .PackagePathNotExported, .debug = .{ .token = exports.first_token } };
}
fn resolveImportsExports(
r: *const ESModule,
match_key: string,
match_obj: ExportsMap.Entry,
is_imports: bool,
package_url: string,
) Resolution {
if (r.debug_logs) |logs| {
logs.addNoteFmt("Checking object path map for \"{s}\"", .{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});
}
return r.resolveTarget(package_url, target, "", is_imports, false);
}
}
if (match_obj.data == .map) {
const expansion_keys = match_obj.data.map.expansion_keys;
for (expansion_keys) |expansion| {
// 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..];
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;
}
}
if (r.debug_logs) |log| {
log.addNoteFmt("The key \"{s}\" did not match", .{expansion.key});
}
}
}
if (r.debug_logs) |log| {
log.addNoteFmt("No keys matched \"{s}\"", .{match_key});
}
return Resolution{
.status = .Null,
.debug = .{ .token = match_obj.first_token },
};
}
threadlocal var resolve_target_buf: bun.PathBuffer = undefined;
threadlocal var resolve_target_buf2: bun.PathBuffer = undefined;
fn resolveTarget(
r: *const ESModule,
package_url: string,
target: ExportsMap.Entry,
subpath: string,
internal: bool,
comptime pattern: bool,
) Resolution {
switch (target.data) {
.string => |str| {
if (r.debug_logs) |log| {
log.addNoteFmt("Checking path \"{s}\" against target \"{s}\"", .{ subpath, str });
log.increaseIndent();
}
defer {
if (r.debug_logs) |log| {
log.decreaseIndent();
}
}
// If pattern is false, subpath has non-zero length and target
// does not end with "/", throw an Invalid Module Specifier error.
if (comptime !pattern) {
if (subpath.len > 0 and !strings.endsWithChar(str, '/')) {
if (r.debug_logs) |log| {
log.addNoteFmt("The target \"{s}\" is invalid because it doesn't end with a \"/\"", .{str});
}
return Resolution{ .path = str, .status = .InvalidModuleSpecifier, .debug = .{ .token = target.first_token } };
}
}
// If target does not start with "./", then...
if (!strings.startsWith(str, "./")) {
if (r.debug_logs) |log| {
log.addNoteFmt("The target \"{s}\" is invalid because it doesn't start with a \"./\"", .{str});
}
if (internal and !strings.hasPrefixComptime(str, "../") and !strings.hasPrefix(str, "/")) {
if (comptime pattern) {
// Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
const len = std.mem.replacementSize(u8, str, "*", subpath);
_ = std.mem.replace(u8, str, "*", 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, str, result });
}
return Resolution{ .path = result, .status = .PackageResolve, .debug = .{ .token = target.first_token } };
} else {
const parts2 = [_]string{ str, subpath };
const result = resolve_path.joinStringBuf(&resolve_target_buf2, parts2, .auto);
if (r.debug_logs) |log| {
log.addNoteFmt("Resolved \".{s}\" to \".{s}\"", .{ str, result });
}
return Resolution{ .path = result, .status = .PackageResolve, .debug = .{ .token = target.first_token } };
}
}
return Resolution{ .path = str, .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };
}
// If target split on "/" or "\" contains any ".", ".." or "node_modules"
// segments after the first segment, throw an Invalid Package Target error.
if (findInvalidSegment(str)) |invalid| {
if (r.debug_logs) |log| {
log.addNoteFmt("The target \"{s}\" is invalid because it contains an invalid segment \"{s}\"", .{ str, invalid });
}
return Resolution{ .path = str, .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };
}
// Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
const parts = [_]string{ package_url, str };
const resolved_target = resolve_path.joinStringBuf(&resolve_target_buf, parts, .auto);
// If target split on "/" or "\" contains any ".", ".." or "node_modules"
// segments after the first segment, throw an Invalid Package Target error.
if (findInvalidSegment(resolved_target)) |invalid| {
if (r.debug_logs) |log| {
log.addNoteFmt("The target \"{s}\" is invalid because it contains an invalid segment \"{s}\"", .{ str, invalid });
}
return Resolution{ .path = str, .status = .InvalidModuleSpecifier, .debug = .{ .token = target.first_token } };
}
if (comptime pattern) {
// Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
const len = std.mem.replacementSize(u8, resolved_target, "*", subpath);
_ = std.mem.replace(u8, resolved_target, "*", subpath, &resolve_target_buf2);
const result = resolve_target_buf2[0..len];
if (r.debug_logs) |log| {
log.addNoteFmt("Substituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, resolved_target, result });
}
const status: Status = if (strings.endsWithCharOrIsZeroLength(result, '*') and strings.indexOfChar(result, '*').? == result.len - 1)
.ExactEndsWithStar
else
.Exact;
return Resolution{ .path = result, .status = status, .debug = .{ .token = target.first_token } };
} else {
const parts2 = [_]string{ package_url, str, subpath };
const result = resolve_path.joinStringBuf(&resolve_target_buf2, parts2, .auto);
if (r.debug_logs) |log| {
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 } };
}
},
.map => |object| {
var did_find_map_entry = false;
var last_map_entry_i: usize = 0;
const slice = object.list.slice();
const keys = slice.items(.key);
for (keys, 0..) |key, i| {
if (r.conditions.contains(key)) {
if (r.debug_logs) |log| {
log.addNoteFmt("The key \"{s}\" matched", .{key});
}
const prev_module_type = r.module_type.*;
var result = r.resolveTarget(package_url, slice.items(.value)[i], subpath, internal, pattern);
if (result.status.isUndefined()) {
did_find_map_entry = true;
last_map_entry_i = i;
r.module_type.* = prev_module_type;
continue;
}
if (strings.eqlComptime(key, "import")) {
r.module_type.* = .esm;
}
if (strings.eqlComptime(key, "require")) {
r.module_type.* = .cjs;
}
return result;
}
if (r.debug_logs) |log| {
log.addNoteFmt("The key \"{s}\" did not match", .{key});
}
}
if (r.debug_logs) |log| {
log.addNoteFmt("No keys matched", .{});
}
var return_target = target;
// ALGORITHM DEVIATION: Provide a friendly error message if no conditions matched
if (keys.len > 0 and !target.keysStartWithDot()) {
var last_map_entry = ExportsMap.Entry.Data.Map.MapEntry{
.key = keys[last_map_entry_i],
.value = slice.items(.value)[last_map_entry_i],
// key_range is unused, so we don't need to pull up the array for it.
.key_range = logger.Range.None,
};
if (did_find_map_entry and
last_map_entry.value.data == .map and
last_map_entry.value.data.map.list.len > 0 and
!last_map_entry.value.keysStartWithDot())
{
// If a top-level condition did match but no sub-condition matched,
// complain about the sub-condition instead of the top-level condition.
// This leads to a less confusing error message. For example:
//
// "exports": {
// "node": {
// "require": "./dist/bwip-js-node.js"
// }
// },
//
// We want the warning to say this:
//
// note: None of the conditions provided ("require") match any of the
// currently active conditions ("default", "import", "node")
// 14 | "node": {
// | ^
//
// We don't want the warning to say this:
//
// note: None of the conditions provided ("browser", "electron", "node")
// match any of the currently active conditions ("default", "import", "node")
// 7 | "exports": {
// | ^
//
// More information: https://github.com/evanw/esbuild/issues/1484
return_target = last_map_entry.value;
}
return Resolution{
.path = "",
.status = .UndefinedNoConditionsMatch,
.debug = .{
.token = target.first_token,
.unmatched_conditions = return_target.data.map.list.items(.key),
},
};
}
return Resolution{
.path = "",
.status = .UndefinedNoConditionsMatch,
.debug = .{ .token = target.first_token },
};
},
.array => |array| {
if (array.len == 0) {
if (r.debug_logs) |log| {
log.addNoteFmt("The path \"{s}\" is an empty array", .{subpath});
}
return Resolution{ .path = "", .status = .Null, .debug = .{ .token = target.first_token } };
}
var last_exception = Status.Undefined;
var last_debug = Resolution.Debug{ .token = target.first_token };
for (array) |targetValue| {
// Let resolved be the result, continuing the loop on any Invalid Package Target error.
const prev_module_type = r.module_type.*;
const result = r.resolveTarget(package_url, targetValue, subpath, internal, pattern);
if (result.status == .InvalidPackageTarget or result.status == .Null) {
last_debug = result.debug;
last_exception = result.status;
}
if (result.status.isUndefined()) {
r.module_type.* = prev_module_type;
continue;
}
return result;
}
return Resolution{ .path = "", .status = last_exception, .debug = last_debug };
},
.null => {
if (r.debug_logs) |log| {
log.addNoteFmt("The path \"{s}\" is null", .{subpath});
}
return Resolution{ .path = "", .status = .Null, .debug = .{ .token = target.first_token } };
},
else => {},
}
if (r.debug_logs) |logs| {
logs.addNoteFmt("Invalid package target for path \"{s}\"", .{subpath});
}
return Resolution{ .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };
}
fn resolveExportsReverse(
r: *const ESModule,
query: string,
root: ExportsMap.Entry,
) ?ReverseResolution {
if (root.data == .map and root.keysStartWithDot()) {
if (r.resolveImportsExportsReverse(query, root)) |res| {
return res;
}
}
return null;
}
fn resolveImportsExportsReverse(
r: *const ESModule,
query: string,
match_obj: ExportsMap.Entry,
) ?ReverseResolution {
if (match_obj.data != .map) return null;
const map = match_obj.data.map;
if (!strings.endsWithCharOrIsZeroLength(query, "*")) {
var slices = map.list.slice();
const keys = slices.items(.key);
const values = slices.items(.value);
for (keys, 0..) |key, i| {
if (r.resolveTargetReverse(query, key, values[i], .exact)) |result| {
return result;
}
}
}
for (map.expansion_keys) |expansion| {
if (strings.endsWithCharOrIsZeroLength(expansion.key, '*')) {
if (r.resolveTargetReverse(query, expansion.key, expansion.value, .pattern)) |result| {
return result;
}
}
if (r.resolveTargetReverse(query, expansion.key, expansion.value, .reverse)) |result| {
return result;
}
}
}
threadlocal var resolve_target_reverse_prefix_buf: bun.PathBuffer = undefined;
threadlocal var resolve_target_reverse_prefix_buf2: bun.PathBuffer = undefined;
fn resolveTargetReverse(
r: *const ESModule,
query: string,
key: string,
target: ExportsMap.Entry,
comptime kind: ReverseKind,
) ?ReverseResolution {
switch (target.data) {
.string => |str| {
switch (comptime kind) {
.exact => {
if (strings.eql(query, str)) {
return ReverseResolution{ .subpath = str, .token = target.first_token };
}
},
.prefix => {
if (strings.startsWith(query, str)) {
return ReverseResolution{
.subpath = std.fmt.bufPrint(&resolve_target_reverse_prefix_buf, "{s}{s}", .{ key, query[str.len..] }) catch unreachable,
.token = target.first_token,
};
}
},
.pattern => {
const key_without_trailing_star = std.mem.trimRight(u8, key, "*");
const star = strings.indexOfChar(str, '*') orelse {
// Handle the case of no "*"
if (strings.eql(query, str)) {
return ReverseResolution{ .subpath = key_without_trailing_star, .token = target.first_token };
}
return null;
};
// Only support tracing through a single "*"
const prefix = str[0..star];
const suffix = str[star + 1 ..];
if (strings.startsWith(query, prefix) and !strings.containsChar(suffix, '*')) {
const after_prefix = query[prefix.len..];
if (strings.endsWith(after_prefix, suffix)) {
const star_data = after_prefix[0 .. after_prefix.len - suffix.len];
return ReverseResolution{
.subpath = std.fmt.bufPrint(
&resolve_target_reverse_prefix_buf2,
"{s}{s}",
.{
key_without_trailing_star,
star_data,
},
) catch unreachable,
.token = target.first_token,
};
}
}
},
}
},
.map => |map| {
const slice = map.list.slice();
const keys = slice.items(.key);
for (keys, 0..) |map_key, i| {
if (r.conditions.contains(map_key)) {
if (r.resolveTargetReverse(query, key, slice.items(.value)[i], kind)) |result| {
if (strings.eqlComptime(map_key, "import")) {
r.module_type.* = .esm;
} else if (strings.eqlComptime(map_key, "require")) {
r.module_type.* = .cjs;
}
return result;
}
}
}
},
.array => |array| {
for (array) |target_value| {
if (r.resolveTargetReverse(query, key, target_value, kind)) |result| {
return result;
}
}
},
else => {},
}
return null;
}
};
fn findInvalidSegment(path_: string) ?string {
const slash = strings.indexAnyComptime(path_, "/\\") orelse return "";
var path = path_[slash + 1 ..];
while (path.len > 0) {
var segment = path;
if (strings.indexAnyComptime(path, "/\\")) |new_slash| {
segment = path[0..new_slash];
path = path[new_slash + 1 ..];
} else {
path = "";
}
switch (segment.len) {
1 => {
if (strings.eqlComptimeIgnoreLen(segment, ".")) return segment;
},
2 => {
if (strings.eqlComptimeIgnoreLen(segment, "..")) return segment;
},
"node_modules".len => {
if (strings.eqlComptimeIgnoreLen(segment, "node_modules")) return segment;
},
else => {},
}
}
return null;
}