Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/bun_core/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ impl Display for DependencyUrlFormatter<'_> {
let mut remain = self.url;
while let Some(slash) = crate::strings::index_of_char_usize(remain, b'/') {
write_bytes(f, &remain[..slash])?;
f.write_str("%2f")?;
f.write_str("%2F")?;
remain = &remain[slash + 1..];
}
write_bytes(f, remain)
Expand Down
2 changes: 1 addition & 1 deletion src/install/NetworkTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ impl NetworkTask {
// "npm" CLI requests the manifest with the encoded name.
let encoded_name_storage;
let encoded_name: &[u8] = if strings::index_of_char(name, b'/').is_some() {
encoded_name_storage = name.replace(b"/", b"%2f");
encoded_name_storage = name.replace(b"/", b"%2F");
&encoded_name_storage
} else {
name
Expand Down
4 changes: 2 additions & 2 deletions test/cli/install/bun-add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,10 +466,10 @@ it("should handle @scoped names", async () => {
env,
});
const err = await stderr.text();
expect(err.split(/\r?\n/)).toContain(`error: GET http://localhost:${port}/@bar%2fbaz - 404`);
expect(err.split(/\r?\n/)).toContain(`error: GET http://localhost:${port}/@bar%2Fbaz - 404`);
expect(await stdout.text()).toEqual(expect.stringContaining("bun add v1."));
expect(await exited).toBe(1);
expect(urls.sort()).toEqual([`${root_url}/@bar%2fbaz`]);
expect(urls.sort()).toEqual([`${root_url}/@bar%2Fbaz`]);
expect(requested).toBe(1);
try {
await access(join(package_dir, "bun.lockb"));
Expand Down
6 changes: 3 additions & 3 deletions test/cli/install/bun-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ it("should create selected template with @ prefix", async () => {

const err = await stderr.text();
expect(err.split(/\r?\n/)).toContain(
`error: GET https://registry.npmjs.org/@quick-start%2fcreate-some-template - 404`,
`error: GET https://registry.npmjs.org/@quick-start%2Fcreate-some-template - 404`,
);
});

Expand All @@ -63,7 +63,7 @@ it("should create selected template with @ prefix implicit `/create`", async ()
});

const err = await stderr.text();
expect(err.split(/\r?\n/)).toContain(`error: GET https://registry.npmjs.org/@second-quick-start%2fcreate - 404`);
expect(err.split(/\r?\n/)).toContain(`error: GET https://registry.npmjs.org/@second-quick-start%2Fcreate - 404`);
await exited;
});

Expand All @@ -78,7 +78,7 @@ it("should create selected template with @ prefix implicit `/create` with versio
});

const err = await stderr.text();
expect(err.split(/\r?\n/)).toContain(`error: GET https://registry.npmjs.org/@second-quick-start%2fcreate - 404`);
expect(err.split(/\r?\n/)).toContain(`error: GET https://registry.npmjs.org/@second-quick-start%2Fcreate - 404`);

await exited;
});
Expand Down
34 changes: 34 additions & 0 deletions test/cli/install/bun-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,40 @@ describe.concurrent("bun info", () => {
expect(output).toContain("TypeScript definitions");
});

// https://github.com/oven-sh/bun/issues/30311
// GitLab's npm registry rejects lowercase %2f; match npm CLI (uppercase %2F).
it("should use uppercase %2F in the manifest URL for scoped names", async () => {
const requestPaths: string[] = [];
using server = Bun.serve({
port: 0,
fetch(req) {
const url = new URL(req.url);
requestPaths.push(url.pathname);
const path = url.pathname.replaceAll("%2f", "/").replaceAll("%2F", "/");
if (path === "/@scoped/has-bin-entry") {
return Response.json({
name: "@scoped/has-bin-entry",
"dist-tags": { latest: "1.0.0" },
versions: { "1.0.0": { name: "@scoped/has-bin-entry", version: "1.0.0" } },
});
}
return new Response("not found", { status: 404 });
},
});

const testDir = tempDirWithFiles("pm-view-scoped", {
"package.json": JSON.stringify({ name: "x", version: "0.0.1" }),
".npmrc": `@scoped:registry=http://localhost:${server.port}/\n`,
});

const { output, error, code } = await runCommand([bunExe(), "pm", "view", "@scoped/has-bin-entry"], testDir);

expect(requestPaths).toEqual(["/@scoped%2Fhas-bin-entry"]);
expect(error).not.toContain("error:");
expect(output).toContain("@scoped/has-bin-entry@1.0.0");
expect(code).toBe(0);
});

it("should handle .", async () => {
const testDir = await setupTest();
const { output, error, code } = await runCommand([bunExe(), "pm", "view", "."], testDir, false);
Expand Down
63 changes: 63 additions & 0 deletions test/cli/install/bun-install-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4935,6 +4935,69 @@ test("name from manifest is scoped and url encoded", async () => {
]);
});

// https://github.com/oven-sh/bun/issues/30311
// GitLab's npm registry rejects lowercase %2f; match npm CLI (uppercase %2F).
test("scoped package manifest url uses uppercase %2F", async () => {
const requestPaths: string[] = [];
using server = Bun.serve({
port: 0,
fetch(req) {
const url = new URL(req.url);
requestPaths.push(url.pathname);
const path = url.pathname.replaceAll("%2f", "/").replaceAll("%2F", "/");
if (path === "/@scoped/has-bin-entry") {
return Response.json({
name: "@scoped/has-bin-entry",
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
name: "@scoped/has-bin-entry",
version: "1.0.0",
dist: {
shasum: "611b2b566718e05e8643fe872923ed91342b039a",
tarball: `http://localhost:${server.port}/@scoped/has-bin-entry/-/has-bin-entry-1.0.0.tgz`,
},
},
},
});
}
if (path.endsWith("/has-bin-entry-1.0.0.tgz")) {
return new Response(
Bun.file(
join(import.meta.dir, "registry", "packages", "@scoped", "has-bin-entry", "has-bin-entry-1.0.0.tgz"),
),
);
}
return new Response("not found", { status: 404 });
},
});

await Promise.all([
write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: { "@scoped/has-bin-entry": "1.0.0" },
}),
),
write(join(packageDir, ".npmrc"), `@scoped:registry=http://localhost:${server.port}/\n`),
]);

const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env,
});
const [out, err, code] = await Promise.all([stdout.text(), stderr.text(), exited]);

expect(requestPaths).toEqual(["/@scoped%2Fhas-bin-entry", "/@scoped/has-bin-entry/-/has-bin-entry-1.0.0.tgz"]);
expect(err).not.toContain("error:");
expect(out).toContain("+ @scoped/has-bin-entry@1.0.0");
expect(code).toBe(0);
});

describe("update", () => {
test("duplicate peer dependency (one package is invalid_package_id)", async () => {
await write(
Expand Down
2 changes: 1 addition & 1 deletion test/cli/install/bun-install-security-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ describe("Large payload via ipc pipe", () => {
customRegistry: (urls, ctx) => {
return async (request: Request) => {
urls.push(request.url);
const url = request.url.replaceAll("%2f", "/");
const url = request.url.replaceAll("%2f", "/").replaceAll("%2F", "/");
expect(request.method).toBe("GET");
if (url.endsWith(".tgz")) {
return new Response(barTarballBytes);
Expand Down
2 changes: 1 addition & 1 deletion test/cli/install/bun-install-tarball-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("tarball download fai
const urls: string[] = [];
let tarballStatus = 200;
setContextHandler(ctx, async request => {
const url = request.url.replaceAll("%2f", "/");
const url = request.url.replaceAll("%2f", "/").replaceAll("%2F", "/");
urls.push(url);
if (url.endsWith(".tgz")) {
if (tarballStatus !== 200) {
Expand Down
6 changes: 3 additions & 3 deletions test/cli/install/bun-install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ describe.concurrent("bun-install", () => {
it("should handle @scoped authentication", async () => {
await withContext(defaultOpts, async ctx => {
let seen_token = false;
const url = `${ctx.registry_url}@foo%2fbar`;
const url = `${ctx.registry_url}@foo%2Fbar`;
const urls: string[] = [];
setContextHandler(ctx, async request => {
expect(request.method).toBe("GET");
Expand Down Expand Up @@ -3310,7 +3310,7 @@ describe.concurrent("bun-install", () => {
"1 package installed",
]);
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([`${ctx.registry_url}@barn%2fmoo`, `${ctx.registry_url}@barn/moo-0.1.0.tgz`]);
expect(urls.sort()).toEqual([`${ctx.registry_url}@barn%2Fmoo`, `${ctx.registry_url}@barn/moo-0.1.0.tgz`]);
expect(ctx.requested).toBe(2);
expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "@barn", "moo"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]);
Expand Down Expand Up @@ -3445,7 +3445,7 @@ describe.concurrent("bun-install", () => {
]);
expect(await exited1).toBe(0);
expect(urls.sort()).toEqual([
`${ctx.registry_url}@barn%2fmoo`,
`${ctx.registry_url}@barn%2Fmoo`,
`${ctx.registry_url}@barn/moo-0.1.0.tgz`,
`${ctx.registry_url}bar`,
`${ctx.registry_url}bar-0.0.2.tgz`,
Expand Down
4 changes: 2 additions & 2 deletions test/cli/install/bun-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ for (const { input } of [{ input: { baz: "~0.0.3", moo: "~0.1.0" } }]) {
]);
expect(await exited1).toBe(0);
expect(urls.sort()).toEqual([
`${root_url}/@barn%2fmoo`,
`${root_url}/@barn%2Fmoo`,
`${root_url}/@barn/moo-0.1.0.tgz`,
`${root_url}/baz`,
`${root_url}/baz-0.0.3.tgz`,
Expand Down Expand Up @@ -264,7 +264,7 @@ for (const { input } of [{ input: { baz: "~0.0.3", moo: "~0.1.0" } }]) {
}
expect(await exited2).toBe(0);
expect(urls.sort()).toEqual([
`${root_url}/@barn%2fmoo`,
`${root_url}/@barn%2Fmoo`,
`${root_url}/@barn/moo-0.1.0.tgz`,
`${root_url}/baz`,
tilde ? `${root_url}/baz-0.0.5.tgz` : `${root_url}/baz-0.0.3.tgz`,
Expand Down
2 changes: 1 addition & 1 deletion test/cli/install/bunx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,7 @@ describe("package name aliases", () => {
const paths = urls.map(u => new URL(u).pathname);
// The manifest request must be for the real package, and must never hit
// the squatter package name.
expect(paths).toContain("/@anthropic-ai%2fclaude-code");
expect(paths).toContain("/@anthropic-ai%2Fclaude-code");
expect(paths).not.toContain("/claude");
// Install fails because the mock registry 404s; that's fine, we only care
// about which manifest was requested.
Expand Down
6 changes: 3 additions & 3 deletions test/cli/install/dummy.registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function defaultHandler(): Response {

/**
* Extract the test ID prefix from a URL path.
* URL format: /test-123/package-name or /test-123/@scope%2fpackage-name
* URL format: /test-123/package-name or /test-123/@scope%2Fpackage-name
*/
function extractTestPrefix(url: string): { prefix: string; remainingPath: string } | null {
const urlObj = new URL(url);
Expand Down Expand Up @@ -171,7 +171,7 @@ export function dummyRegistryForContext(
let retryCountsByURL = new Map<string, number>();
const _handler: Handler = async request => {
urls.push(request.url);
const url = request.url.replaceAll("%2f", "/");
const url = request.url.replaceAll("%2f", "/").replaceAll("%2F", "/");

let status = 200;

Expand Down Expand Up @@ -247,7 +247,7 @@ export function dummyRegistry(
let retryCountsByURL = new Map<string, number>();
const _handler: Handler = async request => {
urls.push(request.url);
const url = request.url.replaceAll("%2f", "/");
const url = request.url.replaceAll("%2f", "/").replaceAll("%2F", "/");

let status = 200;

Expand Down
Loading