mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
4 Commits
dylan/pyth
...
dylan/dev-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0da7377f7 | ||
|
|
b31bee1e48 | ||
|
|
a4e9a31b94 | ||
|
|
01c97bee80 |
@@ -951,14 +951,22 @@ endif()
|
||||
|
||||
if(APPLE)
|
||||
target_link_options(${bun} PUBLIC
|
||||
-dead_strip
|
||||
-dead_strip_dylibs
|
||||
-Wl,-ld_new
|
||||
-Wl,-no_compact_unwind
|
||||
-Wl,-stack_size,0x1200000
|
||||
-fno-keep-static-consts
|
||||
-Wl,-map,${bun}.linker-map
|
||||
)
|
||||
|
||||
# don't strip in debug, this seems to be needed so that the Zig std library
|
||||
# `*dbHelper` DWARF symbols (used by LLDB for pretty printing) are in the
|
||||
# output executable
|
||||
if(NOT DEBUG)
|
||||
target_link_options(${bun} PUBLIC
|
||||
-dead_strip
|
||||
-dead_strip_dylibs
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(LINUX)
|
||||
@@ -995,7 +1003,6 @@ if(LINUX)
|
||||
-Wl,-no-pie
|
||||
-Wl,-icf=safe
|
||||
-Wl,--as-needed
|
||||
-Wl,--gc-sections
|
||||
-Wl,-z,stack-size=12800000
|
||||
-Wl,--compress-debug-sections=zlib
|
||||
-Wl,-z,lazy
|
||||
@@ -1011,6 +1018,15 @@ if(LINUX)
|
||||
-Wl,--build-id=sha1 # Better for debugging than default
|
||||
-Wl,-Map=${bun}.linker-map
|
||||
)
|
||||
|
||||
# don't strip in debug, this seems to be needed so that the Zig std library
|
||||
# `*dbHelper` DWARF symbols (used by LLDB for pretty printing) are in the
|
||||
# output executable
|
||||
if(NOT DEBUG)
|
||||
target_link_options(${bun} PUBLIC
|
||||
-Wl,--gc-sections
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- Symbols list ---
|
||||
|
||||
@@ -167,6 +167,7 @@ pub const Features = struct {
|
||||
};
|
||||
|
||||
pub const workspace = Features{
|
||||
.check_for_duplicate_dependencies = true,
|
||||
.dev_dependencies = true,
|
||||
.optional_dependencies = true,
|
||||
.trusted_dependencies = true,
|
||||
|
||||
@@ -1064,11 +1064,11 @@ pub const Package = extern struct {
|
||||
else => external_alias.hash,
|
||||
};
|
||||
|
||||
var workspace_path: ?String = null;
|
||||
var workspace_version = workspace_ver;
|
||||
var has_workspace_path: ?String = null;
|
||||
var has_workspace_version = workspace_ver;
|
||||
if (comptime tag == null) {
|
||||
workspace_path = lockfile.workspace_paths.get(name_hash);
|
||||
workspace_version = lockfile.workspace_versions.get(name_hash);
|
||||
has_workspace_path = lockfile.workspace_paths.get(name_hash);
|
||||
has_workspace_version = lockfile.workspace_versions.get(name_hash);
|
||||
}
|
||||
|
||||
if (comptime tag != null) {
|
||||
@@ -1093,9 +1093,9 @@ pub const Package = extern struct {
|
||||
},
|
||||
.npm => {
|
||||
const npm = dependency_version.value.npm;
|
||||
if (workspace_version != null) {
|
||||
if (pm.options.link_workspace_packages and npm.version.satisfies(workspace_version.?, buf, buf)) {
|
||||
const path = workspace_path.?.sliced(buf);
|
||||
if (has_workspace_version) |workspace_version| {
|
||||
if (pm.options.link_workspace_packages and npm.version.satisfies(workspace_version, buf, buf)) {
|
||||
const path = has_workspace_path.?.sliced(buf);
|
||||
if (Dependency.parseWithTag(
|
||||
allocator,
|
||||
external_alias.value,
|
||||
@@ -1112,7 +1112,7 @@ pub const Package = extern struct {
|
||||
} else {
|
||||
// It doesn't satisfy, but a workspace shares the same name. Override the workspace with the other dependency
|
||||
for (package_dependencies[0..dependencies_count]) |*dep| {
|
||||
if (dep.name_hash == name_hash and dep.version.tag == .workspace) {
|
||||
if (dep.name_hash == name_hash and dep.behavior.isWorkspaceOnly()) {
|
||||
dep.* = .{
|
||||
.behavior = if (in_workspace) group.behavior.add(.workspace) else group.behavior,
|
||||
.name = external_alias.value,
|
||||
@@ -1126,11 +1126,11 @@ pub const Package = extern struct {
|
||||
}
|
||||
},
|
||||
.workspace => workspace: {
|
||||
if (workspace_path) |path| {
|
||||
if (has_workspace_path) |workspace_path| {
|
||||
if (workspace_range) |range| {
|
||||
if (workspace_version) |ver| {
|
||||
if (range.satisfies(ver, buf, buf)) {
|
||||
dependency_version.value.workspace = path;
|
||||
if (has_workspace_version) |workspace_version| {
|
||||
if (range.satisfies(workspace_version, buf, buf)) {
|
||||
dependency_version.value.workspace = workspace_path;
|
||||
break :workspace;
|
||||
}
|
||||
}
|
||||
@@ -1138,7 +1138,7 @@ pub const Package = extern struct {
|
||||
// important to trim before len == 0 check. `workspace:foo@ ` should install successfully
|
||||
const version_literal = strings.trim(range.input, &strings.whitespace_chars);
|
||||
if (version_literal.len == 0 or range.@"is *"() or Semver.Version.isTaggedVersionOnly(version_literal)) {
|
||||
dependency_version.value.workspace = path;
|
||||
dependency_version.value.workspace = workspace_path;
|
||||
break :workspace;
|
||||
}
|
||||
|
||||
@@ -1157,7 +1157,7 @@ pub const Package = extern struct {
|
||||
return error.InstallFailed;
|
||||
}
|
||||
|
||||
dependency_version.value.workspace = path;
|
||||
dependency_version.value.workspace = workspace_path;
|
||||
} else {
|
||||
const workspace = dependency_version.value.workspace.slice(buf);
|
||||
const path = string_builder.append(String, if (strings.eqlComptime(workspace, "*")) "*" else brk: {
|
||||
@@ -1190,13 +1190,13 @@ pub const Package = extern struct {
|
||||
const workspace_entry = try lockfile.workspace_paths.getOrPut(allocator, name_hash);
|
||||
const found_matching_workspace = workspace_entry.found_existing;
|
||||
|
||||
if (workspace_version) |ver| {
|
||||
try lockfile.workspace_versions.put(allocator, name_hash, ver);
|
||||
if (has_workspace_version) |workspace_version| {
|
||||
try lockfile.workspace_versions.put(allocator, name_hash, workspace_version);
|
||||
for (package_dependencies[0..dependencies_count]) |*package_dep| {
|
||||
if (switch (package_dep.version.tag) {
|
||||
// `dependencies` & `workspaces` defined within the same `package.json`
|
||||
.npm => String.Builder.stringHash(package_dep.realname().slice(buf)) == name_hash and
|
||||
package_dep.version.value.npm.version.satisfies(ver, buf, buf),
|
||||
package_dep.version.value.npm.version.satisfies(workspace_version, buf, buf),
|
||||
// `workspace:*`
|
||||
.workspace => found_matching_workspace and
|
||||
String.Builder.stringHash(package_dep.realname().slice(buf)) == name_hash,
|
||||
@@ -1234,19 +1234,25 @@ pub const Package = extern struct {
|
||||
|
||||
// `peerDependencies` may be specified on existing dependencies. Packages in `workspaces` are deduplicated when
|
||||
// the array is processed
|
||||
if (comptime features.check_for_duplicate_dependencies and !group.behavior.isPeer() and !group.behavior.isWorkspace()) {
|
||||
const entry = lockfile.scratch.duplicate_checker_map.getOrPutAssumeCapacity(external_alias.hash);
|
||||
if (entry.found_existing) {
|
||||
// duplicate dependencies are allowed in optionalDependencies
|
||||
if (comptime group.behavior.isOptional()) {
|
||||
if (comptime features.check_for_duplicate_dependencies) {
|
||||
if (!this_dep.behavior.isWorkspaceOnly()) {
|
||||
const entry = lockfile.scratch.duplicate_checker_map.getOrPutAssumeCapacity(external_alias.hash);
|
||||
if (entry.found_existing) {
|
||||
// duplicate dependencies are allowed in optionalDependencies and devDependencies. choose dev over others
|
||||
for (package_dependencies[0..dependencies_count]) |*package_dep| {
|
||||
if (package_dep.name_hash == this_dep.name_hash) {
|
||||
package_dep.* = this_dep;
|
||||
break;
|
||||
if (comptime group.behavior.isOptional() or group.behavior.isDev()) {
|
||||
package_dep.* = this_dep;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (package_dep.behavior.isDev()) {
|
||||
// choose the existing one.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
|
||||
var notes = try allocator.alloc(logger.Data, 1);
|
||||
|
||||
notes[0] = .{
|
||||
@@ -1263,9 +1269,9 @@ pub const Package = extern struct {
|
||||
.{external_alias.slice(buf)},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
entry.value_ptr.* = value_loc;
|
||||
entry.value_ptr.* = value_loc;
|
||||
}
|
||||
}
|
||||
|
||||
return this_dep;
|
||||
|
||||
276
test/cli/install/test-dev-peer-dependency-priority.test.ts
Normal file
276
test/cli/install/test-dev-peer-dependency-priority.test.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
test("workspace devDependencies should take priority over peerDependencies for resolution", async () => {
|
||||
const dir = tempDirWithFiles("dev-peer-priority", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-monorepo",
|
||||
version: "1.0.0",
|
||||
workspaces: {
|
||||
packages: ["packages/*"],
|
||||
nodeLinker: "isolated",
|
||||
},
|
||||
}),
|
||||
"packages/lib/package.json": JSON.stringify({
|
||||
name: "lib",
|
||||
version: "1.0.0",
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
"my-dep": "workspace:*", // Use workspace protocol for dev
|
||||
},
|
||||
peerDependencies: {
|
||||
"my-dep": "^1.0.0", // Range that wants 1.x
|
||||
},
|
||||
}),
|
||||
"packages/lib/test.js": `const dep = require("my-dep"); console.log(dep.version);`,
|
||||
// Only provide workspace package with version 2.0.0
|
||||
"packages/my-dep/package.json": JSON.stringify({
|
||||
name: "my-dep",
|
||||
version: "2.0.0",
|
||||
main: "index.js",
|
||||
}),
|
||||
"packages/my-dep/index.js": `module.exports = { version: "2.0.0" };`,
|
||||
});
|
||||
|
||||
// Run bun install with a dead registry to ensure no network requests
|
||||
const { stdout, stderr, exitCode } = await new Promise<{ stdout: string; stderr: string; exitCode: number }>(
|
||||
resolve => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--no-progress", "--no-summary"],
|
||||
cwd: dir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
NPM_CONFIG_REGISTRY: "http://localhost:9999/", // Dead URL - will fail if used
|
||||
},
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
proc.exited.then(exitCode => {
|
||||
Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]).then(([stdout, stderr]) => {
|
||||
resolve({ stdout, stderr, exitCode });
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.error("Install failed with exit code:", exitCode);
|
||||
console.error("stdout:", stdout);
|
||||
console.error("stderr:", stderr);
|
||||
}
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check that no network requests were made for packages that should be resolved locally
|
||||
expect(stderr).not.toContain("GET");
|
||||
expect(stderr).not.toContain("http");
|
||||
|
||||
// Check that the lockfile was created correctly
|
||||
const lockfilePath = join(dir, "bun.lock");
|
||||
expect(await Bun.file(lockfilePath).exists()).toBe(true);
|
||||
|
||||
// Verify that version 2.0.0 (devDependency) was linked
|
||||
// If peerDependency range ^1.0.0 was used, it would try to fetch from npm and fail
|
||||
const testResult = await new Promise<string>(resolve => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "packages/lib/test.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
new Response(proc.stdout).text().then(resolve);
|
||||
});
|
||||
|
||||
expect(testResult.trim()).toBe("2.0.0");
|
||||
});
|
||||
|
||||
test("devDependencies and peerDependencies with different versions should coexist", async () => {
|
||||
const dir = tempDirWithFiles("dev-peer-different-versions", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-monorepo",
|
||||
version: "1.0.0",
|
||||
workspaces: {
|
||||
packages: ["packages/*"],
|
||||
nodeLinker: "isolated",
|
||||
},
|
||||
}),
|
||||
"packages/lib/package.json": JSON.stringify({
|
||||
name: "lib",
|
||||
version: "1.0.0",
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
"utils": "1.0.0",
|
||||
},
|
||||
peerDependencies: {
|
||||
"utils": "^1.0.0",
|
||||
},
|
||||
}),
|
||||
"packages/lib/index.js": `console.log("lib");`,
|
||||
"packages/utils/package.json": JSON.stringify({
|
||||
name: "utils",
|
||||
version: "1.0.0",
|
||||
main: "index.js",
|
||||
}),
|
||||
"packages/utils/index.js": `console.log("utils");`,
|
||||
});
|
||||
|
||||
// Run bun install in the monorepo
|
||||
const { stdout, stderr, exitCode } = await new Promise<{ stdout: string; stderr: string; exitCode: number }>(
|
||||
resolve => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--no-progress", "--no-summary"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
proc.exited.then(exitCode => {
|
||||
Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]).then(([stdout, stderr]) => {
|
||||
resolve({ stdout, stderr, exitCode });
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.error("Install failed with exit code:", exitCode);
|
||||
console.error("stdout:", stdout);
|
||||
console.error("stderr:", stderr);
|
||||
}
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check that the lockfile was created correctly
|
||||
const lockfilePath = join(dir, "bun.lock");
|
||||
expect(await Bun.file(lockfilePath).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("dependency behavior comparison prioritizes devDependencies", async () => {
|
||||
const dir = tempDirWithFiles("behavior-comparison", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-app",
|
||||
version: "1.0.0",
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
peerDependencies: {
|
||||
"typescript": "^4.0.0 || ^5.0.0",
|
||||
},
|
||||
}),
|
||||
"index.js": `console.log("app");`,
|
||||
});
|
||||
|
||||
// Run bun install
|
||||
const { stdout, stderr, exitCode } = await new Promise<{ stdout: string; stderr: string; exitCode: number }>(
|
||||
resolve => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--no-progress", "--no-summary"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
proc.exited.then(exitCode => {
|
||||
Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]).then(([stdout, stderr]) => {
|
||||
resolve({ stdout, stderr, exitCode });
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.error("Install failed with exit code:", exitCode);
|
||||
console.error("stdout:", stdout);
|
||||
console.error("stderr:", stderr);
|
||||
}
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check that the lockfile was created correctly
|
||||
const lockfilePath = join(dir, "bun.lock");
|
||||
expect(await Bun.file(lockfilePath).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("Next.js monorepo scenario should not make unnecessary network requests", async () => {
|
||||
const dir = tempDirWithFiles("nextjs-monorepo", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "nextjs-monorepo",
|
||||
version: "1.0.0",
|
||||
workspaces: {
|
||||
packages: ["packages/*"],
|
||||
nodeLinker: "isolated",
|
||||
},
|
||||
}),
|
||||
"packages/web/package.json": JSON.stringify({
|
||||
name: "web",
|
||||
version: "1.0.0",
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
"next": "15.0.0-canary.119", // Specific canary version for dev
|
||||
},
|
||||
peerDependencies: {
|
||||
"next": "^14.0.0 || ^15.0.0", // Range that would accept 14.x or 15.x stable
|
||||
},
|
||||
}),
|
||||
"packages/web/test.js": `const next = require("next/package.json"); console.log(next.version);`,
|
||||
// Only provide the canary version that matches devDependencies
|
||||
"packages/next/package.json": JSON.stringify({
|
||||
name: "next",
|
||||
version: "15.0.0-canary.119",
|
||||
main: "index.js",
|
||||
}),
|
||||
"packages/next/index.js": `console.log("next workspace");`,
|
||||
});
|
||||
|
||||
// Run bun install with dead registry
|
||||
const { stdout, stderr, exitCode } = await new Promise<{ stdout: string; stderr: string; exitCode: number }>(
|
||||
resolve => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--no-progress", "--no-summary"],
|
||||
cwd: dir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
NPM_CONFIG_REGISTRY: "http://localhost:9999/", // Dead URL
|
||||
},
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
proc.exited.then(exitCode => {
|
||||
Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]).then(([stdout, stderr]) => {
|
||||
resolve({ stdout, stderr, exitCode });
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// The key test: should not make network requests for packages that exist in workspace
|
||||
// When devDependencies are prioritized over peerDependencies, the workspace version should be used
|
||||
expect(stderr).not.toContain("GET");
|
||||
expect(stderr).not.toContain("404");
|
||||
expect(stderr).not.toContain("http");
|
||||
|
||||
// Check that the lockfile was created correctly
|
||||
const lockfilePath = join(dir, "bun.lock");
|
||||
expect(await Bun.file(lockfilePath).exists()).toBe(true);
|
||||
|
||||
// Verify that version 15.0.0-canary.119 (devDependency) was used
|
||||
// If peer range was used, it would try to fetch a stable version from npm and fail
|
||||
const testResult = await new Promise<string>(resolve => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "packages/web/test.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
new Response(proc.stdout).text().then(resolve);
|
||||
});
|
||||
|
||||
expect(testResult.trim()).toBe("15.0.0-canary.119");
|
||||
});
|
||||
Reference in New Issue
Block a user