mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 22:32:06 +00:00
Compare commits
4 Commits
claude/imp
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bfca764c4 | ||
|
|
ff10adfac1 | ||
|
|
8f7b95a183 | ||
|
|
c399c48610 |
@@ -874,20 +874,16 @@ pub fn init(
|
||||
manager.lockfile = try ctx.allocator.create(Lockfile);
|
||||
JSC.MiniEventLoop.global = &manager.event_loop.mini;
|
||||
if (!manager.options.enable.cache) {
|
||||
manager.options.enable.manifest_cache = false;
|
||||
manager.options.enable.manifest_cache_control = false;
|
||||
manager.options.enable.manifest_cache = .disabled;
|
||||
}
|
||||
|
||||
if (env.get("BUN_MANIFEST_CACHE")) |manifest_cache| {
|
||||
if (strings.eqlComptime(manifest_cache, "1")) {
|
||||
manager.options.enable.manifest_cache = true;
|
||||
manager.options.enable.manifest_cache_control = false;
|
||||
manager.options.enable.manifest_cache = .write_only;
|
||||
} else if (strings.eqlComptime(manifest_cache, "2")) {
|
||||
manager.options.enable.manifest_cache = true;
|
||||
manager.options.enable.manifest_cache_control = true;
|
||||
manager.options.enable.manifest_cache = .ttl;
|
||||
} else {
|
||||
manager.options.enable.manifest_cache = false;
|
||||
manager.options.enable.manifest_cache_control = false;
|
||||
manager.options.enable.manifest_cache = .disabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,6 +894,7 @@ pub fn init(
|
||||
cli,
|
||||
ctx.install,
|
||||
subcommand,
|
||||
ctx,
|
||||
);
|
||||
|
||||
var ca: []stringZ = &.{};
|
||||
@@ -1047,20 +1044,16 @@ pub fn initWithRuntimeOnce(
|
||||
}
|
||||
|
||||
if (!manager.options.enable.cache) {
|
||||
manager.options.enable.manifest_cache = false;
|
||||
manager.options.enable.manifest_cache_control = false;
|
||||
manager.options.enable.manifest_cache = .disabled;
|
||||
}
|
||||
|
||||
if (env.get("BUN_MANIFEST_CACHE")) |manifest_cache| {
|
||||
if (strings.eqlComptime(manifest_cache, "1")) {
|
||||
manager.options.enable.manifest_cache = true;
|
||||
manager.options.enable.manifest_cache_control = false;
|
||||
manager.options.enable.manifest_cache = .write_only;
|
||||
} else if (strings.eqlComptime(manifest_cache, "2")) {
|
||||
manager.options.enable.manifest_cache = true;
|
||||
manager.options.enable.manifest_cache_control = true;
|
||||
manager.options.enable.manifest_cache = .ttl;
|
||||
} else {
|
||||
manager.options.enable.manifest_cache = false;
|
||||
manager.options.enable.manifest_cache_control = false;
|
||||
manager.options.enable.manifest_cache = .disabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,6 +1064,7 @@ pub fn initWithRuntimeOnce(
|
||||
cli,
|
||||
bun_install,
|
||||
.install,
|
||||
null,
|
||||
) catch |err| {
|
||||
switch (err) {
|
||||
error.OutOfMemory => bun.outOfMemory(),
|
||||
|
||||
@@ -48,6 +48,7 @@ const shared_params = [_]ParamType{
|
||||
clap.parseParam("--save-text-lockfile Save a text-based lockfile") catch unreachable,
|
||||
clap.parseParam("--omit <dev|optional|peer>... Exclude 'dev', 'optional', or 'peer' dependencies from install") catch unreachable,
|
||||
clap.parseParam("--lockfile-only Generate a lockfile without installing dependencies") catch unreachable,
|
||||
clap.parseParam("--prefer-offline Use local cache even if registry data has expired") catch unreachable,
|
||||
clap.parseParam("--linker <STR> Linker strategy (one of \"isolated\" or \"hoisted\")") catch unreachable,
|
||||
clap.parseParam("-h, --help Print this help menu") catch unreachable,
|
||||
};
|
||||
@@ -217,6 +218,7 @@ ca_file_name: string = "",
|
||||
save_text_lockfile: ?bool = null,
|
||||
|
||||
lockfile_only: bool = false,
|
||||
prefer_offline: bool = false,
|
||||
|
||||
node_linker: ?Options.NodeLinker = null,
|
||||
|
||||
@@ -730,6 +732,7 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
|
||||
cli.no_summary = args.flag("--no-summary");
|
||||
cli.ca = args.options("--ca");
|
||||
cli.lockfile_only = args.flag("--lockfile-only");
|
||||
cli.prefer_offline = args.flag("--prefer-offline");
|
||||
|
||||
if (args.option("--linker")) |linker| {
|
||||
cli.node_linker = .fromStr(linker);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
pub inline fn getCacheDirectory(this: *PackageManager) std.fs.Dir {
|
||||
return this.cache_directory_ orelse brk: {
|
||||
this.cache_directory_ = ensureCacheDirectory(this);
|
||||
|
||||
PackageManager.debug("cache directory: {s}", .{this.cache_directory_path});
|
||||
|
||||
break :brk this.cache_directory_.?;
|
||||
};
|
||||
}
|
||||
@@ -83,7 +86,7 @@ noinline fn ensureTemporaryDirectory(this: *PackageManager) std.fs.Dir {
|
||||
};
|
||||
|
||||
if (PackageManager.verbose_install) {
|
||||
Output.prettyErrorln("<r><yellow>warn<r>: bun is unable to access tempdir: {s}, using fallback", .{@errorName(err2)});
|
||||
Output.prettyErrorln("<r><yellow>warn<r>: bun is unable to access tempdir: {s}, using fallback. This may make bun install slower.", .{@errorName(err2)});
|
||||
}
|
||||
|
||||
continue :brk;
|
||||
@@ -104,7 +107,7 @@ noinline fn ensureTemporaryDirectory(this: *PackageManager) std.fs.Dir {
|
||||
};
|
||||
|
||||
if (PackageManager.verbose_install) {
|
||||
Output.prettyErrorln("<r><d>info<r>: cannot move files from tempdir: {s}, using fallback", .{@errorName(err)});
|
||||
Output.prettyErrorln("<r><d>info<r>: cannot move files from tempdir: {s}, using fallback. This may make bun install slower.", .{@errorName(err)});
|
||||
}
|
||||
|
||||
continue :brk;
|
||||
@@ -714,7 +717,7 @@ pub fn writeYarnLock(this: *PackageManager) !void {
|
||||
try tmpfile.promoteToCWD(tmpname, "yarn.lock");
|
||||
}
|
||||
|
||||
const CacheVersion = struct {
|
||||
pub const CacheVersion = struct {
|
||||
pub const current = 1;
|
||||
pub const Formatter = struct {
|
||||
version_number: ?usize = null,
|
||||
@@ -725,6 +728,20 @@ const CacheVersion = struct {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn unversionedName(name: string) ?string {
|
||||
const cache_version_suffix = bun.strings.lastIndexOf(name, "@@@") orelse return null;
|
||||
comptime {
|
||||
if (current != 1) {
|
||||
@compileError("Update this to the current cache version");
|
||||
}
|
||||
}
|
||||
|
||||
if (cache_version_suffix > 0 and name[cache_version_suffix + 3] != '1') {
|
||||
return null;
|
||||
}
|
||||
return name[0..cache_version_suffix];
|
||||
}
|
||||
};
|
||||
|
||||
const PatchHashFmt = struct {
|
||||
|
||||
@@ -667,50 +667,61 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
|
||||
);
|
||||
|
||||
if (!dependency.behavior.isPeer() or install_peer) {
|
||||
if (!this.hasCreatedNetworkTask(task_id, dependency.behavior.isRequired())) {
|
||||
if (this.options.enable.manifest_cache) {
|
||||
var expired = false;
|
||||
if (this.manifests.byNameHashAllowExpired(
|
||||
this,
|
||||
this.scopeForPackageName(name_str),
|
||||
name_hash,
|
||||
&expired,
|
||||
.load_from_memory_fallback_to_disk,
|
||||
)) |manifest| {
|
||||
loaded_manifest = manifest.*;
|
||||
const has_created_network_task = this.hasCreatedNetworkTask(task_id, dependency.behavior.isRequired());
|
||||
const should_read_from_cache = switch (this.options.enable.manifest_cache) {
|
||||
.prefer_offline, .ttl => !has_created_network_task,
|
||||
.write_only, .disabled => false,
|
||||
};
|
||||
|
||||
// If it's an exact package version already living in the cache
|
||||
// We can skip the network request, even if it's beyond the caching period
|
||||
if (version.tag == .npm and version.value.npm.version.isExact()) {
|
||||
if (loaded_manifest.?.findByVersion(version.value.npm.version.head.head.range.left.version)) |find_result| {
|
||||
if (getOrPutResolvedPackageWithFindResult(
|
||||
this,
|
||||
name_hash,
|
||||
name,
|
||||
dependency,
|
||||
version,
|
||||
id,
|
||||
dependency.behavior,
|
||||
&loaded_manifest.?,
|
||||
find_result,
|
||||
install_peer,
|
||||
successFn,
|
||||
) catch null) |new_resolve_result| {
|
||||
resolve_result_ = new_resolve_result;
|
||||
_ = this.network_dedupe_map.remove(task_id);
|
||||
continue :retry_with_new_resolve_result;
|
||||
}
|
||||
if (should_read_from_cache) {
|
||||
var expired = false;
|
||||
if (this.manifests.byNameHashAllowExpired(
|
||||
this,
|
||||
this.scopeForPackageName(name_str),
|
||||
name_hash,
|
||||
&expired,
|
||||
.load_from_memory_fallback_to_disk,
|
||||
)) |manifest| {
|
||||
loaded_manifest = manifest.*;
|
||||
|
||||
// If it's an exact package version already living in the cache
|
||||
// We can skip the network request, even if it's beyond the caching period
|
||||
if (version.tag == .npm and version.value.npm.version.isExact()) {
|
||||
if (loaded_manifest.?.findByVersion(version.value.npm.version.head.head.range.left.version)) |find_result| {
|
||||
if (getOrPutResolvedPackageWithFindResult(
|
||||
this,
|
||||
name_hash,
|
||||
name,
|
||||
dependency,
|
||||
version,
|
||||
id,
|
||||
dependency.behavior,
|
||||
&loaded_manifest.?,
|
||||
find_result,
|
||||
install_peer,
|
||||
successFn,
|
||||
) catch null) |new_resolve_result| {
|
||||
resolve_result_ = new_resolve_result;
|
||||
_ = this.network_dedupe_map.remove(task_id);
|
||||
continue :retry_with_new_resolve_result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Was it recent enough to just load it without the network call?
|
||||
if (this.options.enable.manifest_cache_control and !expired) {
|
||||
_ = this.network_dedupe_map.remove(task_id);
|
||||
continue :retry_from_manifests_ptr;
|
||||
}
|
||||
// Either:
|
||||
// - Was it recent enough to just load it without the network call?
|
||||
// - Did the user pass `--prefer-offline`?
|
||||
if (!expired) {
|
||||
_ = this.network_dedupe_map.remove(task_id);
|
||||
debug("manifest cache hit for {s}@{s}", .{ name_str, this.lockfile.str(&version.literal) });
|
||||
continue :retry_from_manifests_ptr;
|
||||
} else {
|
||||
debug("manifest cache expired for {s}@{s}", .{ name_str, this.lockfile.str(&version.literal) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_created_network_task) {
|
||||
if (PackageManager.verbose_install) {
|
||||
Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name_str});
|
||||
}
|
||||
@@ -1561,6 +1572,7 @@ fn getOrPutResolvedPackage(
|
||||
|
||||
// Resolve the version from the loaded NPM manifest
|
||||
const name_str = this.lockfile.str(&name);
|
||||
|
||||
const manifest = this.manifests.byNameHash(
|
||||
this,
|
||||
this.scopeForPackageName(name_str),
|
||||
|
||||
@@ -230,6 +230,7 @@ pub fn load(
|
||||
maybe_cli: ?CommandLineArguments,
|
||||
bun_install_: ?*Api.BunInstall,
|
||||
subcommand: Subcommand,
|
||||
ctx: ?Command.Context,
|
||||
) bun.OOM!void {
|
||||
var base = Api.NpmRegistry{
|
||||
.url = "",
|
||||
@@ -288,11 +289,11 @@ pub fn load(
|
||||
}
|
||||
|
||||
if (config.disable_manifest_cache orelse false) {
|
||||
this.enable.manifest_cache = false;
|
||||
this.enable.manifest_cache = .disabled;
|
||||
}
|
||||
|
||||
if (config.force orelse false) {
|
||||
this.enable.manifest_cache_control = false;
|
||||
this.enable.manifest_cache = .disabled;
|
||||
this.enable.force_install = true;
|
||||
}
|
||||
|
||||
@@ -452,8 +453,18 @@ pub fn load(
|
||||
|
||||
// Update should never read from manifest cache
|
||||
if (subcommand == .update) {
|
||||
this.enable.manifest_cache = false;
|
||||
this.enable.manifest_cache_control = false;
|
||||
this.enable.manifest_cache = .disabled;
|
||||
}
|
||||
|
||||
// Override prefer_offline from bunfig or CLI context offline_mode_setting
|
||||
if (ctx) |context| {
|
||||
if (context.debug.offline_mode_setting) |offline_mode| {
|
||||
switch (offline_mode) {
|
||||
.offline => this.enable.manifest_cache = .prefer_offline,
|
||||
.online => this.enable.manifest_cache = .ttl,
|
||||
.latest => this.enable.manifest_cache = .disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maybe_cli) |cli| {
|
||||
@@ -499,8 +510,7 @@ pub fn load(
|
||||
this.json_output = cli.json_output;
|
||||
|
||||
if (cli.no_cache) {
|
||||
this.enable.manifest_cache = false;
|
||||
this.enable.manifest_cache_control = false;
|
||||
this.enable.manifest_cache = .disabled;
|
||||
}
|
||||
|
||||
if (cli.omit) |omit| {
|
||||
@@ -584,7 +594,7 @@ pub fn load(
|
||||
}
|
||||
|
||||
if (cli.force) {
|
||||
this.enable.manifest_cache_control = false;
|
||||
this.enable.manifest_cache = .disabled;
|
||||
this.enable.force_install = true;
|
||||
this.enable.force_save_lockfile = true;
|
||||
}
|
||||
@@ -641,6 +651,10 @@ pub fn load(
|
||||
// `bun pm why` command options
|
||||
this.top_only = cli.top_only;
|
||||
this.depth = cli.depth;
|
||||
|
||||
if (cli.prefer_offline) {
|
||||
this.enable.manifest_cache = .prefer_offline;
|
||||
}
|
||||
} else {
|
||||
this.log_level = if (default_disable_progress_bar) LogLevel.default_no_progress else LogLevel.default;
|
||||
PackageManager.verbose_install = false;
|
||||
@@ -669,9 +683,42 @@ pub const Do = packed struct(u16) {
|
||||
_: u4 = 0,
|
||||
};
|
||||
|
||||
pub const ManifestCacheControl = enum(u2) {
|
||||
disabled,
|
||||
|
||||
/// Populate the cache without reading from the cache.
|
||||
write_only,
|
||||
|
||||
ttl,
|
||||
|
||||
/// No TTL on package manifest cache
|
||||
///
|
||||
/// This lets us resolve package versions without network requests if they
|
||||
/// already exist in the cache.
|
||||
prefer_offline,
|
||||
|
||||
pub const default: ManifestCacheControl = .ttl;
|
||||
|
||||
pub fn isExpired(this: ManifestCacheControl, public_max_age: u32, timestamp_for_manifest_cache_control: u32) bool {
|
||||
return switch (this) {
|
||||
.disabled => true,
|
||||
.write_only => false,
|
||||
.ttl => public_max_age > timestamp_for_manifest_cache_control,
|
||||
.prefer_offline => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn shouldReadFromCache(this: ManifestCacheControl) bool {
|
||||
return switch (this) {
|
||||
.disabled => false,
|
||||
.write_only => false,
|
||||
.ttl, .prefer_offline => true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Enable = packed struct(u16) {
|
||||
manifest_cache: bool = true,
|
||||
manifest_cache_control: bool = true,
|
||||
manifest_cache: ManifestCacheControl = .default,
|
||||
cache: bool = true,
|
||||
fail_early: bool = false,
|
||||
frozen_lockfile: bool = false,
|
||||
@@ -714,3 +761,4 @@ const CommandLineArguments = @import("./CommandLineArguments.zig");
|
||||
const Subcommand = bun.install.PackageManager.Subcommand;
|
||||
const PackageManager = bun.install.PackageManager;
|
||||
const PackageInstall = bun.install.PackageInstall;
|
||||
const Command = bun.CLI.Command;
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn scopeForPackageName(this: *const PackageManager, name: string) *const Npm
|
||||
) orelse &this.options.scope;
|
||||
}
|
||||
|
||||
pub fn getInstalledVersionsFromDiskCache(this: *PackageManager, tags_buf: *std.ArrayList(u8), package_name: []const u8, allocator: std.mem.Allocator) !std.ArrayList(Semver.Version) {
|
||||
pub fn getInstalledVersionsFromDiskCache(this: *PackageManager, package_name: []const u8, tags_buf: *std.ArrayList(u8), allocator: std.mem.Allocator) !std.ArrayList(Semver.Version) {
|
||||
var list = std.ArrayList(Semver.Version).init(allocator);
|
||||
var dir = this.getCacheDirectory().openDir(package_name, .{
|
||||
.iterate = true,
|
||||
@@ -54,7 +54,7 @@ pub fn getInstalledVersionsFromDiskCache(this: *PackageManager, tags_buf: *std.A
|
||||
|
||||
while (try iter.next()) |entry| {
|
||||
if (entry.kind != .directory and entry.kind != .sym_link) continue;
|
||||
const name = entry.name;
|
||||
const name = PackageManagerDirectories.CacheVersion.unversionedName(entry.name) orelse continue;
|
||||
const sliced = SlicedString.init(name, name);
|
||||
const parsed = Semver.Version.parse(sliced);
|
||||
if (!parsed.valid or parsed.wildcard != .none) continue;
|
||||
@@ -63,7 +63,7 @@ pub fn getInstalledVersionsFromDiskCache(this: *PackageManager, tags_buf: *std.A
|
||||
var version = parsed.version.min();
|
||||
const total = version.tag.build.len() + version.tag.pre.len();
|
||||
if (total > 0) {
|
||||
tags_buf.ensureUnusedCapacity(total) catch unreachable;
|
||||
try tags_buf.ensureUnusedCapacity(total);
|
||||
var available = tags_buf.items.ptr[tags_buf.items.len..tags_buf.capacity];
|
||||
const new_version = version.cloneInto(name, &available);
|
||||
tags_buf.items.len += total;
|
||||
@@ -76,7 +76,10 @@ pub fn getInstalledVersionsFromDiskCache(this: *PackageManager, tags_buf: *std.A
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn resolveFromDiskCache(this: *PackageManager, package_name: []const u8, version: Dependency.Version) ?PackageID {
|
||||
pub fn resolveFromDiskCache(this: *PackageManager, package_name: []const u8, version: Dependency.Version) ?struct {
|
||||
package_id: PackageID,
|
||||
is_first_time: bool,
|
||||
} {
|
||||
if (version.tag != .npm) {
|
||||
// only npm supported right now
|
||||
// tags are more ambiguous
|
||||
@@ -89,7 +92,7 @@ pub fn resolveFromDiskCache(this: *PackageManager, package_name: []const u8, ver
|
||||
var stack_fallback = std.heap.stackFallback(4096, arena_alloc);
|
||||
const allocator = stack_fallback.get();
|
||||
var tags_buf = std.ArrayList(u8).init(allocator);
|
||||
const installed_versions = this.getInstalledVersionsFromDiskCache(&tags_buf, package_name, allocator) catch |err| {
|
||||
const installed_versions = this.getInstalledVersionsFromDiskCache(package_name, &tags_buf, allocator) catch |err| {
|
||||
Output.debug("error getting installed versions from disk cache: {s}", .{bun.span(@errorName(err))});
|
||||
return null;
|
||||
};
|
||||
@@ -101,30 +104,32 @@ pub fn resolveFromDiskCache(this: *PackageManager, package_name: []const u8, ver
|
||||
@as([]const u8, tags_buf.items),
|
||||
Semver.Version.sortGt,
|
||||
);
|
||||
const npm_version = &version.value.npm.version;
|
||||
for (installed_versions.items) |installed_version| {
|
||||
if (version.value.npm.version.satisfies(installed_version, this.lockfile.buffers.string_bytes.items, tags_buf.items)) {
|
||||
var buf: bun.PathBuffer = undefined;
|
||||
const npm_package_path = this.pathForCachedNPMPath(&buf, package_name, installed_version) catch |err| {
|
||||
const satisfies = npm_version.satisfies(installed_version, this.lockfile.buffers.string_bytes.items, tags_buf.items);
|
||||
|
||||
if (satisfies) {
|
||||
const buf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(buf);
|
||||
const npm_package_path = this.pathForCachedNPMPath(buf, package_name, installed_version) catch |err| {
|
||||
Output.debug("error getting path for cached npm path: {s}", .{bun.span(@errorName(err))});
|
||||
return null;
|
||||
};
|
||||
var npm_info = version.npm().?;
|
||||
npm_info.version = Semver.Query.Group.from(installed_version);
|
||||
const dependency = Dependency.Version{
|
||||
.tag = .npm,
|
||||
.literal = version.literal,
|
||||
.value = .{
|
||||
.npm = .{
|
||||
.name = String.init(package_name, package_name),
|
||||
.version = Semver.Query.Group.from(installed_version),
|
||||
},
|
||||
.npm = npm_info,
|
||||
},
|
||||
};
|
||||
switch (FolderResolution.getOrPut(.{ .cache_folder = npm_package_path }, dependency, ".", this)) {
|
||||
.new_package_id => |id| {
|
||||
this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id]);
|
||||
return id;
|
||||
return .{ .package_id = id, .is_first_time = true };
|
||||
},
|
||||
.package_id => |id| {
|
||||
this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id]);
|
||||
return id;
|
||||
return .{ .package_id = id, .is_first_time = false };
|
||||
},
|
||||
.err => |err| {
|
||||
Output.debug("error getting or putting folder resolution: {s}", .{bun.span(@errorName(err))});
|
||||
@@ -218,6 +223,7 @@ pub fn verifyResolutions(this: *PackageManager, log_level: PackageManager.Option
|
||||
|
||||
// @sortImports
|
||||
|
||||
const PackageManagerDirectories = @import("./PackageManagerDirectories.zig");
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
@@ -229,7 +235,6 @@ const strings = bun.strings;
|
||||
|
||||
const Semver = bun.Semver;
|
||||
const SlicedString = Semver.SlicedString;
|
||||
const String = Semver.String;
|
||||
|
||||
const Dependency = bun.install.Dependency;
|
||||
const DependencyID = bun.install.DependencyID;
|
||||
|
||||
@@ -266,7 +266,7 @@ pub fn runTasks(
|
||||
|
||||
entry.value_ptr.manifest.pkg.public_max_age = timestamp_this_tick.?;
|
||||
|
||||
if (manager.options.enable.manifest_cache) {
|
||||
if (manager.options.enable.manifest_cache != .disabled) {
|
||||
Npm.PackageManifest.Serializer.saveAsync(
|
||||
&entry.value_ptr.manifest,
|
||||
manager.scopeForPackageName(name.slice()),
|
||||
|
||||
@@ -66,14 +66,14 @@ pub fn byNameHashAllowExpired(
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pm.options.enable.manifest_cache) {
|
||||
if (pm.options.enable.manifest_cache != .disabled) {
|
||||
if (Npm.PackageManifest.Serializer.loadByFileID(
|
||||
pm.allocator,
|
||||
scope,
|
||||
pm.getCacheDirectory(),
|
||||
name_hash,
|
||||
) catch null) |manifest| {
|
||||
if (pm.options.enable.manifest_cache_control and manifest.pkg.public_max_age > pm.timestamp_for_manifest_cache_control) {
|
||||
if (manifest.pkg.public_max_age > pm.timestamp_for_manifest_cache_control or pm.options.enable.manifest_cache == .prefer_offline) {
|
||||
entry.value_ptr.* = .{ .manifest = manifest };
|
||||
return &entry.value_ptr.manifest;
|
||||
} else {
|
||||
|
||||
@@ -1047,6 +1047,7 @@ pub const Printer = struct {
|
||||
null,
|
||||
null,
|
||||
.install,
|
||||
null,
|
||||
);
|
||||
|
||||
var printer = Printer{
|
||||
|
||||
@@ -489,7 +489,7 @@ pub const Registry = struct {
|
||||
new_etag,
|
||||
@as(u32, @truncate(@as(u64, @intCast(@max(0, std.time.timestamp()))))) + 300,
|
||||
)) |package| {
|
||||
if (package_manager.options.enable.manifest_cache) {
|
||||
if (package_manager.options.enable.manifest_cache != .disabled) {
|
||||
PackageManifest.Serializer.saveAsync(
|
||||
&package,
|
||||
scope,
|
||||
|
||||
@@ -94,6 +94,7 @@ pub const FolderResolution = union(Tag) {
|
||||
.value = .{
|
||||
.npm = .{
|
||||
.version = this.version,
|
||||
|
||||
.url = String.from(""),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2364,9 +2364,9 @@ pub const Resolver = struct {
|
||||
}
|
||||
|
||||
if (r.opts.prefer_offline_install) {
|
||||
if (pm.resolveFromDiskCache(esm.name, version)) |package_id| {
|
||||
input_package_id_.* = package_id;
|
||||
return .{ .resolution = pm.lockfile.packages.items(.resolution)[package_id] };
|
||||
if (pm.resolveFromDiskCache(esm.name, version)) |result| {
|
||||
input_package_id_.* = result.package_id;
|
||||
return .{ .resolution = pm.lockfile.packages.items(.resolution)[result.package_id] };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
289
test/cli/install/bun-prefer-offline.test.ts
Normal file
289
test/cli/install/bun-prefer-offline.test.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { spawn } from "bun";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { bunEnv as bunEnv_, bunExe } from "harness";
|
||||
import { join } from "path";
|
||||
import {
|
||||
dummyAfterAll,
|
||||
dummyAfterEach,
|
||||
dummyBeforeAll,
|
||||
dummyBeforeEach,
|
||||
package_dir,
|
||||
requested,
|
||||
root_url,
|
||||
setHandler,
|
||||
} from "./dummy.registry";
|
||||
|
||||
beforeAll(dummyBeforeAll);
|
||||
afterAll(dummyAfterAll);
|
||||
beforeEach(dummyBeforeEach);
|
||||
afterEach(dummyAfterEach);
|
||||
|
||||
let bunEnv: Record<string, string>;
|
||||
beforeEach(() => {
|
||||
bunEnv = {
|
||||
...bunEnv_,
|
||||
};
|
||||
|
||||
console.log(package_dir);
|
||||
});
|
||||
|
||||
it("should use cache when --prefer-offline is passed even with expired data", async () => {
|
||||
const urls: string[] = [];
|
||||
|
||||
// Create a registry handler that sets cache control headers with expiry in the past
|
||||
setHandler(async request => {
|
||||
urls.push(request.url);
|
||||
|
||||
expect(request.method).toBe("GET");
|
||||
if (request.url.endsWith(".tgz")) {
|
||||
// For .tgz files, return the test package from dummy registry
|
||||
const { file } = await import("bun");
|
||||
const { basename, join } = await import("path");
|
||||
return new Response(file(join(import.meta.dir, basename(request.url).toLowerCase())), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/octet-stream",
|
||||
// Set cache control to expire in the past
|
||||
"cache-control": "max-age=3600",
|
||||
"last-modified": new Date(Date.now() - 7200000).toUTCString(), // 2 hours ago
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// For package metadata requests
|
||||
const name = request.url.slice(request.url.indexOf("/", root_url.length) + 1);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name,
|
||||
versions: {
|
||||
"0.0.2": {
|
||||
name,
|
||||
version: "0.0.2",
|
||||
dist: {
|
||||
tarball: `${request.url}-0.0.2.tgz`,
|
||||
},
|
||||
},
|
||||
"0.1.0": {
|
||||
name,
|
||||
version: "0.1.0",
|
||||
dist: {
|
||||
tarball: `${request.url}-0.1.0.tgz`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"dist-tags": {
|
||||
latest: name === "moo" ? "0.1.0" : "0.0.2",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
// Set cache control to expire in the past
|
||||
"cache-control": "max-age=3600",
|
||||
"last-modified": new Date(Date.now() - 7200000).toUTCString(), // 2 hours ago
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Create package.json with a dependency
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "test-prefer-offline",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"bar": "^0.0.2",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// First install - this should populate the cache
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: package_dir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
},
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
const stderrText = await new Response(stderr).text();
|
||||
expect(await exited).toBe(0);
|
||||
expect(stderrText).not.toContain("error:");
|
||||
}
|
||||
|
||||
// Save the URLs from the first install
|
||||
const firstInstallUrls = [...urls];
|
||||
expect(firstInstallUrls.length).toBeGreaterThan(0);
|
||||
|
||||
// Clear the URLs array and requested counter
|
||||
urls.length = 0;
|
||||
const firstRequestCount = requested;
|
||||
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "test-prefer-offline",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
// Update from exact to inexact to force a registry lookup.
|
||||
"bar": ">=0.0.2",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Second install with --prefer-offline - this should NOT make network requests
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install", "--prefer-offline", "--verbose"],
|
||||
cwd: package_dir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
BUN_CONFIG_MANIFEST_CACHE_CONTROL_TIMESTAMP: (Date.now() + 7200000).toString(),
|
||||
BUN_DEBUG: "/tmp/all.log",
|
||||
BUN_DEBUG_ALL: "1",
|
||||
BUN_DEBUG_QUIET_LOGS: undefined,
|
||||
},
|
||||
stdio: ["ignore", "inherit", "pipe"],
|
||||
});
|
||||
|
||||
const stderrText = await new Response(stderr).text();
|
||||
const exitCode = await exited;
|
||||
|
||||
// With --prefer-offline and no cached manifest, the install should NOT fail
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderrText).not.toContain("failed to resolve");
|
||||
console.log(stderrText);
|
||||
}
|
||||
|
||||
// Verify no new network requests were made (the key behavior we want)
|
||||
expect(urls).toEqual([]);
|
||||
expect(requested).toBe(firstRequestCount);
|
||||
|
||||
// Since the install failed, the lockfile should remain from the first install
|
||||
const lockfileExists = await Bun.file(join(package_dir, "bun.lock")).exists();
|
||||
expect(lockfileExists).toBe(true);
|
||||
});
|
||||
|
||||
it("should make network requests without --prefer-offline even with expired cache", async () => {
|
||||
const urls: string[] = [];
|
||||
|
||||
// Create a registry handler that sets cache control headers with expiry in the past
|
||||
setHandler(async request => {
|
||||
urls.push(request.url);
|
||||
|
||||
expect(request.method).toBe("GET");
|
||||
if (request.url.endsWith(".tgz")) {
|
||||
const { file } = await import("bun");
|
||||
const { basename, join } = await import("path");
|
||||
return new Response(file(join(import.meta.dir, basename(request.url).toLowerCase())), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/octet-stream",
|
||||
"cache-control": "max-age=3600",
|
||||
"last-modified": new Date(Date.now() - 7200000).toUTCString(), // 2 hours ago
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const name = request.url.slice(request.url.indexOf("/", root_url.length) + 1);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name,
|
||||
versions: {
|
||||
"0.0.2": {
|
||||
name,
|
||||
version: "0.0.2",
|
||||
dist: {
|
||||
tarball: `${request.url}-0.0.2.tgz`,
|
||||
},
|
||||
},
|
||||
"0.1.0": {
|
||||
name,
|
||||
version: "0.1.0",
|
||||
dist: {
|
||||
tarball: `${request.url}-0.1.0.tgz`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"dist-tags": {
|
||||
latest: name === "moo" ? "0.1.0" : "0.0.2",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"cache-control": "max-age=3600",
|
||||
"last-modified": new Date(Date.now() - 7200000).toUTCString(), // 2 hours ago
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Create package.json with a dependency
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "test-normal-install",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"bar": "0.0.2",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// First install to populate cache
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: package_dir,
|
||||
env: bunEnv,
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
|
||||
// Clear URLs and add a new dependency to force registry lookup
|
||||
urls.length = 0;
|
||||
|
||||
// Add a new dependency to package.json to force a registry lookup
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "test-normal-install",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"bar": "0.0.2",
|
||||
"moo": "0.1.0", // This will force a registry lookup
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Second install WITHOUT --prefer-offline - this SHOULD make network requests
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: package_dir,
|
||||
env: bunEnv,
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
const stderrText = await new Response(stderr).text();
|
||||
if ((await exited) !== 0) {
|
||||
console.log("Normal install STDERR:", stderrText);
|
||||
}
|
||||
expect(await exited).toBe(0);
|
||||
expect(stderrText).not.toContain("error:");
|
||||
}
|
||||
|
||||
// Verify network requests were made because cache was expired
|
||||
expect(urls.length).toBeGreaterThan(0);
|
||||
});
|
||||
Reference in New Issue
Block a user