mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 08:58:52 +00:00
Compare commits
1 Commits
dylan/pyth
...
claude/off
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cef2f57fdf |
@@ -86,6 +86,11 @@ pub fn forManifest(
|
||||
is_optional: bool,
|
||||
needs_extended: bool,
|
||||
) ForManifestError!void {
|
||||
// Debug assertion - should not be called in offline mode
|
||||
if (comptime bun.Environment.allow_assert) {
|
||||
bun.assert(!this.package_manager.options.enable.offline);
|
||||
}
|
||||
|
||||
this.url_buf = blk: {
|
||||
|
||||
// Not all registries support scoped package names when fetching the manifest.
|
||||
@@ -250,6 +255,7 @@ pub fn schedule(this: *NetworkTask, batch: *ThreadPool.Batch) void {
|
||||
|
||||
pub const ForTarballError = OOM || error{
|
||||
InvalidURL,
|
||||
OfflineModePackageNotCached,
|
||||
};
|
||||
|
||||
pub fn forTarball(
|
||||
@@ -259,6 +265,11 @@ pub fn forTarball(
|
||||
scope: *const Npm.Registry.Scope,
|
||||
authorization: NetworkTask.Authorization,
|
||||
) ForTarballError!void {
|
||||
// Debug assertion - should not be called in offline mode
|
||||
if (comptime bun.Environment.allow_assert) {
|
||||
bun.assert(!this.package_manager.options.enable.offline);
|
||||
}
|
||||
|
||||
this.callback = .{ .extract = tarball_.* };
|
||||
const tarball = &this.callback.extract;
|
||||
const tarball_url = tarball.url.slice();
|
||||
|
||||
@@ -934,6 +934,7 @@ pub const PackageInstaller = struct {
|
||||
is_pending_package_install,
|
||||
log_level,
|
||||
),
|
||||
error.OfflineModePackageNotCached => {},
|
||||
};
|
||||
},
|
||||
.local_tarball => {
|
||||
@@ -957,6 +958,7 @@ pub const PackageInstaller = struct {
|
||||
is_pending_package_install,
|
||||
log_level,
|
||||
),
|
||||
error.OfflineModePackageNotCached => {},
|
||||
};
|
||||
},
|
||||
.npm => {
|
||||
@@ -985,6 +987,7 @@ pub const PackageInstaller = struct {
|
||||
is_pending_package_install,
|
||||
log_level,
|
||||
),
|
||||
error.OfflineModePackageNotCached => {},
|
||||
};
|
||||
},
|
||||
else => {
|
||||
|
||||
@@ -29,6 +29,7 @@ const shared_params = [_]ParamType{
|
||||
clap.parseParam("--cafile <STR> The same as `--ca`, but is a file path to the certificate") catch unreachable,
|
||||
clap.parseParam("--dry-run Don't install anything") catch unreachable,
|
||||
clap.parseParam("--frozen-lockfile Disallow changes to lockfile") catch unreachable,
|
||||
clap.parseParam("--offline Install packages using only the local cache") catch unreachable,
|
||||
clap.parseParam("-f, --force Always request the latest versions from the registry & reinstall all dependencies") catch unreachable,
|
||||
clap.parseParam("--cache-dir <PATH> Store & load cached data from a specific directory path") catch unreachable,
|
||||
clap.parseParam("--no-cache Ignore manifest cache entirely") catch unreachable,
|
||||
@@ -184,6 +185,7 @@ positionals: []const string = &[_]string{},
|
||||
yarn: bool = false,
|
||||
production: bool = false,
|
||||
frozen_lockfile: bool = false,
|
||||
offline: bool = false,
|
||||
no_save: bool = false,
|
||||
dry_run: bool = false,
|
||||
force: bool = false,
|
||||
@@ -795,6 +797,7 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
|
||||
cli.yarn = args.flag("--yarn");
|
||||
cli.production = args.flag("--production") or args.flag("--prod");
|
||||
cli.frozen_lockfile = args.flag("--frozen-lockfile") or (cli.positionals.len > 0 and strings.eqlComptime(cli.positionals[0], "ci"));
|
||||
cli.offline = args.flag("--offline");
|
||||
cli.no_progress = args.flag("--no-progress");
|
||||
cli.dry_run = args.flag("--dry-run");
|
||||
cli.global = args.flag("--global");
|
||||
|
||||
@@ -107,6 +107,21 @@ pub fn enqueueTarballForDownload(
|
||||
|
||||
if (task_queue.found_existing) return;
|
||||
|
||||
// In offline mode, check if tarball is in cache
|
||||
if (this.options.enable.offline) {
|
||||
const dep = this.lockfile.buffers.dependencies.items[dependency_id];
|
||||
if (!dep.behavior.isOptional()) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"Package tarball not found in cache (offline mode): {s}",
|
||||
.{url},
|
||||
) catch {};
|
||||
}
|
||||
return error.OfflineModePackageNotCached;
|
||||
}
|
||||
|
||||
if (try this.generateNetworkTaskForTarball(
|
||||
task_id,
|
||||
url,
|
||||
@@ -162,6 +177,21 @@ pub fn enqueueGitForCheckout(
|
||||
task_context: TaskCallbackContext,
|
||||
patch_name_and_version_hash: ?u64,
|
||||
) void {
|
||||
// Git dependencies not supported in offline mode
|
||||
if (this.options.enable.offline) {
|
||||
const dep = this.lockfile.buffers.dependencies.items[dependency_id];
|
||||
if (!dep.behavior.isOptional()) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"Git dependencies not supported in offline mode: {s}",
|
||||
.{alias},
|
||||
) catch {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = &resolution.value.git;
|
||||
const url = this.lockfile.str(&repository.repo);
|
||||
const clone_id = Task.Id.forGitClone(url);
|
||||
@@ -760,6 +790,20 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
|
||||
}
|
||||
}
|
||||
|
||||
// In offline mode, check if package is in cache
|
||||
if (this.options.enable.offline) {
|
||||
if (!dependency.behavior.isOptional()) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"Package \"{s}\" not found in cache (offline mode)",
|
||||
.{name_str},
|
||||
) catch {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (PackageManager.verbose_install) {
|
||||
Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name_str});
|
||||
}
|
||||
@@ -815,6 +859,20 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
|
||||
return;
|
||||
}
|
||||
|
||||
// Git dependencies not supported in offline mode
|
||||
if (this.options.enable.offline) {
|
||||
if (dependency.behavior.isRequired()) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"Git dependencies not supported in offline mode",
|
||||
.{},
|
||||
) catch {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const alias = this.lockfile.str(&dependency.name);
|
||||
const url = this.lockfile.str(&dep.repo);
|
||||
const clone_id = Task.Id.forGitClone(url);
|
||||
|
||||
@@ -624,6 +624,12 @@ pub fn load(
|
||||
this.enable.frozen_lockfile = true;
|
||||
}
|
||||
|
||||
if (cli.offline) {
|
||||
this.enable.offline = true;
|
||||
// In offline mode, use stale cache (don't check expiry)
|
||||
this.enable.manifest_cache_control = false;
|
||||
}
|
||||
|
||||
if (cli.force) {
|
||||
this.enable.manifest_cache_control = false;
|
||||
this.enable.force_install = true;
|
||||
@@ -728,7 +734,8 @@ pub const Enable = packed struct(u16) {
|
||||
|
||||
exact_versions: bool = false,
|
||||
only_missing: bool = false,
|
||||
_: u7 = 0,
|
||||
offline: bool = false,
|
||||
_: u6 = 0,
|
||||
};
|
||||
|
||||
const string = []const u8;
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
const StartManifestTaskError = bun.OOM || error{InvalidURL};
|
||||
const StartManifestTaskError = bun.OOM || error{ InvalidURL, OfflineModePackageNotCached };
|
||||
fn startManifestTask(manager: *PackageManager, pkg_name: []const u8, dep: *const Dependency, needs_extended_manifest: bool) StartManifestTaskError!void {
|
||||
const task_id = Task.Id.forManifest(pkg_name);
|
||||
if (manager.hasCreatedNetworkTask(task_id, dep.behavior.optional)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In offline mode, error if we need to fetch a manifest
|
||||
if (manager.options.enable.offline) {
|
||||
if (dep.behavior.isOptional()) {
|
||||
// Optional dependencies can fail silently in offline mode
|
||||
return;
|
||||
}
|
||||
// Required dependency not in cache - fail
|
||||
manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
manager.allocator,
|
||||
"Package \"{s}\" not found in cache (offline mode)",
|
||||
.{pkg_name},
|
||||
) catch {};
|
||||
return error.OfflineModePackageNotCached;
|
||||
}
|
||||
|
||||
manager.startProgressBarIfNone();
|
||||
var task = manager.getNetworkTask();
|
||||
task.* = .{
|
||||
@@ -153,6 +171,7 @@ const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const Output = bun.Output;
|
||||
const logger = bun.logger;
|
||||
|
||||
const Dependency = bun.install.Dependency;
|
||||
const DependencyID = bun.install.DependencyID;
|
||||
|
||||
@@ -6,6 +6,14 @@ pub fn installWithManager(
|
||||
) !void {
|
||||
const log_level = manager.options.log_level;
|
||||
|
||||
// Display offline mode message
|
||||
if (manager.options.enable.offline) {
|
||||
if (log_level != .silent) {
|
||||
Output.prettyErrorln("<d>Installing in offline mode (using cache only)<r>", .{});
|
||||
Output.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Start resolving DNS for the default registry immediately.
|
||||
// Unless you're behind a proxy.
|
||||
if (!manager.env.hasHTTPProxy()) {
|
||||
|
||||
@@ -907,6 +907,14 @@ pub inline fn decrementPendingTasks(manager: *PackageManager) void {
|
||||
}
|
||||
|
||||
pub fn flushNetworkQueue(this: *PackageManager) void {
|
||||
// In offline mode, network queue should be empty
|
||||
if (this.options.enable.offline) {
|
||||
if (this.network_task_fifo.readableLength() > 0) {
|
||||
Output.debug("Warning: network tasks queued in offline mode (this is a bug)", .{});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var network = &this.network_task_fifo;
|
||||
|
||||
while (network.readItem()) |network_task| {
|
||||
|
||||
@@ -87,6 +87,16 @@ pub fn byNameHashAllowExpired(
|
||||
return null;
|
||||
}
|
||||
|
||||
// In offline mode, always use cached manifest regardless of expiry
|
||||
if (pm.options.enable.offline) {
|
||||
entry.value_ptr.* = .{ .manifest = manifest };
|
||||
if (is_expired) |expiry| {
|
||||
expiry.* = false;
|
||||
}
|
||||
return &entry.value_ptr.manifest;
|
||||
}
|
||||
|
||||
// Online mode: check expiry
|
||||
if (pm.options.enable.manifest_cache_control and manifest.pkg.public_max_age > pm.timestamp_for_manifest_cache_control) {
|
||||
entry.value_ptr.* = .{ .manifest = manifest };
|
||||
return &entry.value_ptr.manifest;
|
||||
|
||||
@@ -964,7 +964,7 @@ pub fn installIsolatedPackages(
|
||||
patch_info.nameAndVersionHash(),
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => |oom| return oom,
|
||||
error.InvalidURL => {
|
||||
error.InvalidURL, error.OfflineModePackageNotCached => {
|
||||
Output.err(err, "failed to enqueue package for download: {s}@{}", .{
|
||||
pkg_name.slice(string_buf),
|
||||
pkg_res.fmt(string_buf, .auto),
|
||||
@@ -1001,7 +1001,7 @@ pub fn installIsolatedPackages(
|
||||
patch_info.nameAndVersionHash(),
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => bun.outOfMemory(),
|
||||
error.InvalidURL => {
|
||||
error.InvalidURL, error.OfflineModePackageNotCached => {
|
||||
Output.err(err, "failed to enqueue github package for download: {s}@{}", .{
|
||||
pkg_name.slice(string_buf),
|
||||
pkg_res.fmt(string_buf, .auto),
|
||||
@@ -1035,7 +1035,7 @@ pub fn installIsolatedPackages(
|
||||
patch_info.nameAndVersionHash(),
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => bun.outOfMemory(),
|
||||
error.InvalidURL => {
|
||||
error.InvalidURL, error.OfflineModePackageNotCached => {
|
||||
Output.err(err, "failed to enqueue tarball for download: {s}@{}", .{
|
||||
pkg_name.slice(string_buf),
|
||||
pkg_res.fmt(string_buf, .auto),
|
||||
|
||||
403
test/cli/install/bun-install-offline.test.ts
Normal file
403
test/cli/install/bun-install-offline.test.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import { file } from "bun";
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
test("offline mode: successful install with cached packages", async () => {
|
||||
using tmpdir = tempDir("offline-success", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-success",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"is-odd": "3.0.1",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install to populate cache
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
|
||||
|
||||
expect(exitCode1).toBe(0);
|
||||
|
||||
// Remove node_modules and lockfile
|
||||
await using rmProc = Bun.spawn({
|
||||
cmd: ["rm", "-rf", "node_modules", "bun.lockb"],
|
||||
cwd: String(tmpdir),
|
||||
});
|
||||
await rmProc.exited;
|
||||
|
||||
// Install again in offline mode
|
||||
await using proc2 = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
|
||||
|
||||
expect(exitCode2).toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr2, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
|
||||
// Verify package was installed
|
||||
const isOddPath = join(String(tmpdir), "node_modules", "is-odd", "package.json");
|
||||
const isOddPkg = await file(isOddPath).json();
|
||||
expect(isOddPkg.name).toBe("is-odd");
|
||||
expect(isOddPkg.version).toBe("3.0.1");
|
||||
});
|
||||
|
||||
test("offline mode: fails when package not in cache", async () => {
|
||||
using tmpdir = tempDir("offline-fail-not-cached", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-fail",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
// Using a package that's very unlikely to be in cache
|
||||
"some-nonexistent-package-that-should-never-exist": "1.0.0",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
expect(normalizeBunSnapshot(stderr, tmpdir)).toContain("not found in cache (offline mode)");
|
||||
});
|
||||
|
||||
test("offline mode: works without existing lockfile", async () => {
|
||||
using tmpdir = tempDir("offline-no-lockfile", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-no-lockfile",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"is-even": "1.0.0",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install to populate cache
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await proc1.exited;
|
||||
|
||||
// Remove node_modules and lockfile
|
||||
await using rmProc = Bun.spawn({
|
||||
cmd: ["rm", "-rf", "node_modules", "bun.lockb"],
|
||||
cwd: String(tmpdir),
|
||||
});
|
||||
await rmProc.exited;
|
||||
|
||||
// Install in offline mode without lockfile
|
||||
await using proc2 = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
|
||||
|
||||
expect(exitCode2).toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr2, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
|
||||
// Verify package was installed
|
||||
const isEvenPath = join(String(tmpdir), "node_modules", "is-even", "package.json");
|
||||
const isEvenPkg = await file(isEvenPath).json();
|
||||
expect(isEvenPkg.name).toBe("is-even");
|
||||
});
|
||||
|
||||
test("offline mode: skips optional dependencies not in cache", async () => {
|
||||
using tmpdir = tempDir("offline-optional-deps", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-optional",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"is-number": "7.0.0",
|
||||
},
|
||||
optionalDependencies: {
|
||||
// This package doesn't exist, but should be skipped
|
||||
"some-nonexistent-optional-package": "1.0.0",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install is-number to cache it
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--no-optional"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await proc1.exited;
|
||||
|
||||
// Remove node_modules and lockfile
|
||||
await using rmProc = Bun.spawn({
|
||||
cmd: ["rm", "-rf", "node_modules", "bun.lockb"],
|
||||
cwd: String(tmpdir),
|
||||
});
|
||||
await rmProc.exited;
|
||||
|
||||
// Install in offline mode - should succeed despite optional dep missing
|
||||
await using proc2 = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
|
||||
|
||||
expect(exitCode2).toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr2, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
|
||||
// Verify is-number was installed
|
||||
const isNumberPath = join(String(tmpdir), "node_modules", "is-number", "package.json");
|
||||
const isNumberPkg = await file(isNumberPath).json();
|
||||
expect(isNumberPkg.name).toBe("is-number");
|
||||
});
|
||||
|
||||
test.skip("offline mode: rejects git dependencies", async () => {
|
||||
using tmpdir = tempDir("offline-git-deps", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-git",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"is-odd": "3.0.1",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install to populate cache
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await proc1.exited;
|
||||
|
||||
// Change to a git dependency (which can't be cached)
|
||||
await Bun.write(
|
||||
join(String(tmpdir), "package.json"),
|
||||
JSON.stringify({
|
||||
name: "test-offline-git",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"some-git-package": "git+https://github.com/user/repo.git#main",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Remove node_modules and lockfile
|
||||
await using rmProc = Bun.spawn({
|
||||
cmd: ["rm", "-rf", "node_modules", "bun.lockb"],
|
||||
cwd: String(tmpdir),
|
||||
});
|
||||
await rmProc.exited;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
});
|
||||
|
||||
test("offline mode: works with workspace dependencies", async () => {
|
||||
using tmpdir = tempDir("offline-workspace", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-workspace",
|
||||
version: "1.0.0",
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"packages/pkg-a/package.json": JSON.stringify({
|
||||
name: "pkg-a",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"is-odd": "3.0.1",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install to populate cache
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await proc1.exited;
|
||||
|
||||
// Remove node_modules and lockfile
|
||||
await using rmProc = Bun.spawn({
|
||||
cmd: ["rm", "-rf", "node_modules", "packages/pkg-a/node_modules", "bun.lockb"],
|
||||
cwd: String(tmpdir),
|
||||
});
|
||||
await rmProc.exited;
|
||||
|
||||
// Install in offline mode
|
||||
await using proc2 = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
|
||||
|
||||
expect(exitCode2).toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr2, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
|
||||
// Verify package was installed (in workspace node_modules)
|
||||
const pkgAPath = join(String(tmpdir), "packages", "pkg-a", "node_modules", "is-odd", "package.json");
|
||||
const isOddPkg = await file(pkgAPath).json();
|
||||
expect(isOddPkg.name).toBe("is-odd");
|
||||
});
|
||||
|
||||
test("offline mode: uses stale manifests (ignores expiry)", async () => {
|
||||
using tmpdir = tempDir("offline-stale-manifest", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-stale",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"lodash": "^4.17.0",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install to populate cache
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await proc1.exited;
|
||||
|
||||
// Remove node_modules and lockfile
|
||||
await using rmProc = Bun.spawn({
|
||||
cmd: ["rm", "-rf", "node_modules", "bun.lockb"],
|
||||
cwd: String(tmpdir),
|
||||
});
|
||||
await rmProc.exited;
|
||||
|
||||
// Install in offline mode - should use cached manifest even if stale
|
||||
await using proc2 = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
|
||||
|
||||
expect(exitCode2).toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr2, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
|
||||
// Verify lodash was installed (some version satisfying ^4.17.0)
|
||||
const lodashPath = join(String(tmpdir), "node_modules", "lodash", "package.json");
|
||||
const lodashPkg = await file(lodashPath).json();
|
||||
expect(lodashPkg.name).toBe("lodash");
|
||||
expect(lodashPkg.version).toMatch(/^4\.17\./);
|
||||
});
|
||||
|
||||
test("offline mode: combines with other install flags", async () => {
|
||||
using tmpdir = tempDir("offline-combined-flags", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-offline-combined",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"ms": "2.1.3",
|
||||
},
|
||||
devDependencies: {
|
||||
"is-odd": "3.0.1",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install to populate cache
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await proc1.exited;
|
||||
|
||||
// Remove node_modules and lockfile
|
||||
await using rmProc = Bun.spawn({
|
||||
cmd: ["rm", "-rf", "node_modules", "bun.lockb"],
|
||||
cwd: String(tmpdir),
|
||||
});
|
||||
await rmProc.exited;
|
||||
|
||||
// Install in offline mode with --production
|
||||
await using proc2 = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--offline", "--production"],
|
||||
cwd: String(tmpdir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
|
||||
|
||||
expect(exitCode2).toBe(0);
|
||||
expect(normalizeBunSnapshot(stderr2, tmpdir)).toContain("Installing in offline mode (using cache only)");
|
||||
|
||||
// Verify only production dependency was installed
|
||||
const msPath = join(String(tmpdir), "node_modules", "ms", "package.json");
|
||||
const msPkg = await file(msPath).json();
|
||||
expect(msPkg.name).toBe("ms");
|
||||
|
||||
// Dev dependency should not be installed
|
||||
const isOddPath = join(String(tmpdir), "node_modules", "is-odd");
|
||||
const isOddExists = await file(join(isOddPath, "package.json"))
|
||||
.exists()
|
||||
.catch(() => false);
|
||||
expect(isOddExists).toBe(false);
|
||||
});
|
||||
Reference in New Issue
Block a user