aboutsummaryrefslogtreecommitdiff
path: root/src/resolver/package_json.zig
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2022-11-06 21:42:05 -0800
committerGravatar GitHub <noreply@github.com> 2022-11-06 21:42:05 -0800
commite45f72e8e422191adeb4fd1bad896dc6a47c76b3 (patch)
tree3a76da8b343c081dba84e0ac95f3c2cc2423106a /src/resolver/package_json.zig
parent645cf903350a7fe5f5076100b7c4a6bc8cd1b431 (diff)
downloadbun-e45f72e8e422191adeb4fd1bad896dc6a47c76b3.tar.gz
bun-e45f72e8e422191adeb4fd1bad896dc6a47c76b3.tar.zst
bun-e45f72e8e422191adeb4fd1bad896dc6a47c76b3.zip
Automatically install npm packages when running a script in Bun's runtime (#1459)
* Update bundler.zig * WIP * Update README.md * Update README.md * wip * Support running scripts without package.json * Add `--no-auto-install` and `--prefer-offline` flags * WIP * wip * Update headers-handwritten.h * WIP * Build fixes * Fix UAF * Update install.zig * Must call .allocate() * Micro-optimization: only call .timestamp() once per tick when installing packages * Support progress bar * Extend the timestamp for package staleness checks to 1 day * Add `--prefer-latest`, `-i` CLI Flags * Fix crash * Support line text manually being set on an Error instance * Add a few more fields for error messages * Fix bug when counting 8 character strings in string builder * Implement error handling for automatic package installs! * Fix crash * Make it say module when there's a slash * Update module_loader.zig * Ban dependency versions in import specifiers when a package.json is present * Remove unused field * Update README.md * Update README.md * Update README.md * Update README.md Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Diffstat (limited to 'src/resolver/package_json.zig')
-rw-r--r--src/resolver/package_json.zig326
1 files changed, 294 insertions, 32 deletions
diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig
index e33c5ac60..034debe59 100644
--- a/src/resolver/package_json.zig
+++ b/src/resolver/package_json.zig
@@ -28,6 +28,26 @@ pub const MacroImportReplacementMap = std.StringArrayHashMap(string);
pub const MacroMap = std.StringArrayHashMapUnmanaged(MacroImportReplacementMap);
const ScriptsMap = std.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 {
@@ -85,6 +105,12 @@ pub const PackageJSON = struct {
scripts: ?*ScriptsMap = null,
+ arch: Architecture = Architecture.all,
+ os: OperatingSystem = OperatingSystem.all,
+
+ package_manager_package_id: Install.PackageID = Install.invalid_package_id,
+ dependencies: DependencyMap = .{},
+
// 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
@@ -538,12 +564,13 @@ pub const PackageJSON = struct {
}
pub fn parse(
- comptime ResolverType: type,
- r: *ResolverType,
+ r: *resolver.Resolver,
input_path: string,
dirname_fd: StoredFileDescriptorType,
- comptime generate_hash: bool,
+ package_id: ?Install.PackageID,
comptime include_scripts: bool,
+ comptime include_dependencies: @Type(.EnumLiteral),
+ comptime generate_hash: bool,
) ?PackageJSON {
// TODO: remove this extra copy
@@ -566,7 +593,7 @@ pub const PackageJSON = struct {
};
if (r.debug_logs) |*debug| {
- debug.addNoteFmt("The file \"{s}\" exists", .{package_json_path}) catch unreachable;
+ debug.addNoteFmt("The file \"{s}\" exists", .{package_json_path});
}
const key_path = fs.Path.init(package_json_path);
@@ -716,6 +743,160 @@ pub const PackageJSON = struct {
}
}
+ 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(r.allocator, package_json.version, .npm, &sliced, r.log)) |dependency_version| {
+ if (dependency_version.value.npm.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| {
+ var first = true;
+ if (os_field.asArray()) |*array| {
+ while (array.next()) |item| {
+ if (item.asString(bun.default_allocator)) |str| {
+ if (first) {
+ package_json.arch = Architecture.none;
+ first = false;
+ }
+ package_json.arch = package_json.arch.apply(str);
+ }
+ }
+ }
+ }
+
+ if (json.get("os")) |os_field| {
+ var first = true;
+ if (os_field.asArray()) |*array| {
+ while (array.next()) |item| {
+ if (item.asString(bun.default_allocator)) |str| {
+ if (first) {
+ package_json.os = OperatingSystem.none;
+ first = false;
+ }
+ package_json.os = package_json.os.apply(str);
+ }
+ }
+ }
+ }
+
+ const DependencyGroup = Install.Lockfile.Package.DependencyGroup;
+ const features = .{
+ .dependencies = true,
+ .dev_dependencies = include_dependencies == .main,
+ .optional_dependencies = false,
+ .peer_dependencies = false,
+ };
+
+ const dependency_groups = comptime brk: {
+ var out_groups: [
+ @as(usize, @boolToInt(features.dependencies)) +
+ @as(usize, @boolToInt(features.dev_dependencies)) +
+ @as(usize, @boolToInt(features.optional_dependencies)) +
+ @as(usize, @boolToInt(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(
+ r.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.key orelse continue;
+ const name_str = name.asString(r.allocator) orelse continue;
+ const version_value = prop.value orelse continue;
+ const version_str = version_value.asString(r.allocator) orelse continue;
+ const sliced_str = Semver.SlicedString.init(version_str, version_str);
+
+ if (Dependency.parse(
+ r.allocator,
+ version_str,
+ &sliced_str,
+ r.log,
+ )) |dependency_version| {
+ const dependency = Dependency{
+ .name = String.init(name_str, name_str),
+ .version = dependency_version,
+ .name_hash = bun.hash(name_str),
+ .behavior = group.behavior,
+ };
+ package_json.dependencies.map.putAssumeCapacityContext(
+ dependency.name,
+ dependency,
+ ctx,
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
// used by `bun run`
if (include_scripts) {
read_scripts: {
@@ -1043,8 +1224,49 @@ pub const ESModule = struct {
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 = ">=0.0.0",
+ };
+ }
+
+ return this;
+ }
+
pub fn parseName(specifier: string) ?string {
var slash = strings.indexOfCharNeg(specifier, '/');
if (!strings.startsWithChar(specifier, '@')) {
@@ -1059,6 +1281,27 @@ pub const ESModule = struct {
}
}
+ 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 = "" };
@@ -1066,11 +1309,30 @@ pub const ESModule = struct {
if (strings.startsWith(package.name, ".") or strings.indexAnyComptime(package.name, "\\%") != null)
return null;
- std.mem.copy(u8, subpath_buf[1..], specifier[package.name.len..]);
- subpath_buf[0] = '.';
- package.subpath = subpath_buf[0 .. specifier[package.name.len..].len + 1];
+ 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[@minimum(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 {
+ std.mem.copy(u8, subpath_buf[1..], specifier);
+ subpath_buf[0] = '.';
+ subpath.* = subpath_buf[0 .. specifier.len + 1];
+ }
};
const ReverseKind = enum { exact, pattern, prefix };
@@ -1170,7 +1432,7 @@ pub const ESModule = struct {
) Resolution {
if (exports.data == .invalid) {
if (r.debug_logs) |logs| {
- logs.addNote("Invalid package configuration") catch unreachable;
+ logs.addNote("Invalid package configuration");
}
return Resolution{ .status = .InvalidPackageConfiguration, .debug = .{ .token = exports.first_token } };
@@ -1210,7 +1472,7 @@ pub const ESModule = struct {
}
if (r.debug_logs) |logs| {
- logs.addNoteFmt("The path \"{s}\" was not exported", .{subpath}) catch unreachable;
+ logs.addNoteFmt("The path \"{s}\" was not exported", .{subpath});
}
return Resolution{ .status = .PackagePathNotExported, .debug = .{ .token = exports.first_token } };
@@ -1224,13 +1486,13 @@ pub const ESModule = struct {
package_url: string,
) Resolution {
if (r.debug_logs) |logs| {
- logs.addNoteFmt("Checking object path map for \"{s}\"", .{match_key}) catch unreachable;
+ logs.addNoteFmt("Checking object path map for \"{s}\"", .{match_key});
}
if (!strings.endsWithChar(match_key, '.')) {
if (match_obj.valueForKey(match_key)) |target| {
if (r.debug_logs) |log| {
- log.addNoteFmt("Found \"{s}\"", .{match_key}) catch unreachable;
+ log.addNoteFmt("Found \"{s}\"", .{match_key});
}
return r.resolveTarget(package_url, target, "", is_imports, false);
@@ -1248,7 +1510,7 @@ pub const ESModule = struct {
const target = expansion.value;
const subpath = match_key[expansion.key.len - 1 ..];
if (r.debug_logs) |log| {
- log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath }) catch unreachable;
+ log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath });
}
return r.resolveTarget(package_url, target, subpath, is_imports, true);
@@ -1259,7 +1521,7 @@ pub const ESModule = struct {
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 }) catch unreachable;
+ log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath });
}
var result = r.resolveTarget(package_url, target, subpath, is_imports, false);
@@ -1273,13 +1535,13 @@ pub const ESModule = struct {
}
if (r.debug_logs) |log| {
- log.addNoteFmt("The key \"{s}\" did not match", .{expansion.key}) catch unreachable;
+ log.addNoteFmt("The key \"{s}\" did not match", .{expansion.key});
}
}
}
if (r.debug_logs) |log| {
- log.addNoteFmt("No keys matched \"{s}\"", .{match_key}) catch unreachable;
+ log.addNoteFmt("No keys matched \"{s}\"", .{match_key});
}
return Resolution{
@@ -1301,12 +1563,12 @@ pub const ESModule = struct {
switch (target.data) {
.string => |str| {
if (r.debug_logs) |log| {
- log.addNoteFmt("Checking path \"{s}\" against target \"{s}\"", .{ subpath, str }) catch unreachable;
- log.increaseIndent() catch unreachable;
+ log.addNoteFmt("Checking path \"{s}\" against target \"{s}\"", .{ subpath, str });
+ log.increaseIndent();
}
defer {
if (r.debug_logs) |log| {
- log.decreaseIndent() catch unreachable;
+ log.decreaseIndent();
}
}
@@ -1315,7 +1577,7 @@ pub const ESModule = struct {
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}) catch unreachable;
+ 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 } };
@@ -1325,7 +1587,7 @@ pub const ESModule = struct {
// 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}) catch unreachable;
+ 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, "/")) {
@@ -1335,7 +1597,7 @@ pub const ESModule = struct {
_ = 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 }) catch unreachable;
+ log.addNoteFmt("Subsituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, str, result });
}
return Resolution{ .path = result, .status = .PackageResolve, .debug = .{ .token = target.first_token } };
@@ -1343,7 +1605,7 @@ pub const ESModule = struct {
var 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 }) catch unreachable;
+ log.addNoteFmt("Resolved \".{s}\" to \".{s}\"", .{ str, result });
}
return Resolution{ .path = result, .status = .PackageResolve, .debug = .{ .token = target.first_token } };
@@ -1357,7 +1619,7 @@ pub const ESModule = struct {
// 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 }) catch unreachable;
+ 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 } };
@@ -1371,7 +1633,7 @@ pub const ESModule = struct {
// 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 }) catch unreachable;
+ 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 } };
@@ -1383,7 +1645,7 @@ 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 }) catch unreachable;
+ log.addNoteFmt("Subsituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, resolved_target, result });
}
return Resolution{ .path = result, .status = .Exact, .debug = .{ .token = target.first_token } };
@@ -1391,7 +1653,7 @@ pub const ESModule = struct {
var 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 }) catch unreachable;
+ 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 } };
@@ -1406,7 +1668,7 @@ pub const ESModule = struct {
for (keys) |key, i| {
if (strings.eqlComptime(key, "default") or r.conditions.contains(key)) {
if (r.debug_logs) |log| {
- log.addNoteFmt("The key \"{s}\" matched", .{key}) catch unreachable;
+ log.addNoteFmt("The key \"{s}\" matched", .{key});
}
var result = r.resolveTarget(package_url, slice.items(.value)[i], subpath, internal, pattern);
@@ -1420,12 +1682,12 @@ pub const ESModule = struct {
}
if (r.debug_logs) |log| {
- log.addNoteFmt("The key \"{s}\" did not match", .{key}) catch unreachable;
+ log.addNoteFmt("The key \"{s}\" did not match", .{key});
}
}
if (r.debug_logs) |log| {
- log.addNoteFmt("No keys matched", .{}) catch unreachable;
+ log.addNoteFmt("No keys matched", .{});
}
var return_target = target;
@@ -1489,7 +1751,7 @@ pub const ESModule = struct {
.array => |array| {
if (array.len == 0) {
if (r.debug_logs) |log| {
- log.addNoteFmt("The path \"{s}\" is an empty array", .{subpath}) catch unreachable;
+ log.addNoteFmt("The path \"{s}\" is an empty array", .{subpath});
}
return Resolution{ .path = "", .status = .Null, .debug = .{ .token = target.first_token } };
@@ -1517,7 +1779,7 @@ pub const ESModule = struct {
},
.@"null" => {
if (r.debug_logs) |log| {
- log.addNoteFmt("The path \"{s}\" is null", .{subpath}) catch unreachable;
+ log.addNoteFmt("The path \"{s}\" is null", .{subpath});
}
return Resolution{ .path = "", .status = .Null, .debug = .{ .token = target.first_token } };
@@ -1526,7 +1788,7 @@ pub const ESModule = struct {
}
if (r.debug_logs) |logs| {
- logs.addNoteFmt("Invalid package target for path \"{s}\"", .{subpath}) catch unreachable;
+ logs.addNoteFmt("Invalid package target for path \"{s}\"", .{subpath});
}
return Resolution{ .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };