Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 11 additions & 26 deletions src/install/yarn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,34 +236,19 @@ impl<'a> Entry<'a> {
ParsedNpmAlias { version: b"*" }
}

/// Registry tarball URLs look like `<registry>/<name>/-/<basename>-<version>.tgz`,
/// where `<name>` spans two path segments (`@scope/name`) for scoped packages.
pub(crate) fn get_package_name_from_resolved_url(url: &[u8]) -> Option<&[u8]> {
if let Some(dash_idx) = strings::index_of(url, b"/-/") {
let mut slash_count: usize = 0;
let mut last_slash: usize = 0;
let mut second_last_slash: usize = 0;

let mut i = dash_idx;
while i > 0 {
if url[i - 1] == b'/' {
slash_count += 1;
if slash_count == 1 {
last_slash = i - 1;
} else if slash_count == 2 {
second_last_slash = i - 1;
break;
}
}
i -= 1;
}

if last_slash < dash_idx && url[last_slash + 1] == b'@' {
return Some(&url[second_last_slash + 1..dash_idx]);
} else if last_slash < dash_idx {
return Some(&url[last_slash + 1..dash_idx]);
}
let path = &url[..strings::index_of(url, b"/-/")?];
let (prefix, name) = strings::rsplit_once_char(path, b'/')?;
if name.is_empty() {
return None;
}

None
let scope_start = strings::last_index_of_char(prefix, b'/').map_or(0, |slash| slash + 1);
if prefix[scope_start..].starts_with(b"@") {
return Some(&path[scope_start..]);
}
Some(name)
}
}

Expand Down
93 changes: 93 additions & 0 deletions test/cli/install/migration/yarn-lock-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,99 @@ undici-types@~5.26.4:
expect(bunLockContent).toContain('"my-lodash": ["lodash@4.17.21"');
});

test("yarn.lock with npm aliases of a scoped package keep the scope", async () => {
const typesNode20Integrity =
"sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==";
const typesNode18Integrity = "sha512-" + Buffer.alloc(64, 1).toString("base64");
const needsNodeTypesIntegrity = "sha512-" + Buffer.alloc(64, 2).toString("base64");

await using tmpDir = tempDir("yarn-migration-scoped-aliases", {
"package.json": JSON.stringify(
{
name: "scoped-alias-test",
version: "1.0.0",
dependencies: {
"@aliased/node-types": "npm:@types/node@20.11.5",
"@types/node": "^18.0.0",
"my-node-types": "npm:@types/node@20.11.5",
"needs-node-types": "1.0.0",
},
},
null,
2,
),
// yarn v1 lists every pattern that resolved to the same tarball on one key line,
// so the three aliases of @types/node@20.11.5 share a single entry.
"yarn.lock": `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@aliased/node-types@npm:@types/node@20.11.5", "my-node-types@npm:@types/node@20.11.5", "types-alias@npm:@types/node@20.11.5":
version "20.11.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#be10c622ca7fcaa3cf226cf80166abc31389d86e"
integrity ${typesNode20Integrity}

"@types/node@^18.0.0":
version "18.19.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.0.tgz#1111111111111111111111111111111111111111"
integrity ${typesNode18Integrity}

needs-node-types@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/needs-node-types/-/needs-node-types-1.0.0.tgz#2222222222222222222222222222222222222222"
integrity ${needsNodeTypesIntegrity}
dependencies:
types-alias "npm:@types/node@20.11.5"
`,
});

// After migrating, bun fetches the manifest of every migrated package by the name it
// recorded, so the registry sees which package each alias was resolved to.
const requestedManifests = new Set<string>();
await using registry = Bun.serve({
port: 0,
fetch(req) {
requestedManifests.add(decodeURIComponent(new URL(req.url).pathname.slice(1)));
return new Response("not found", { status: 404 });
},
});

await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "migrate", "-f"],
cwd: tmpDir,
env: {
...bunEnv,
BUN_CONFIG_REGISTRY: registry.url.href,
BUN_INSTALL_CACHE_DIR: join(tmpDir, ".bun-cache"),
},
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toBe("");
expect(stderr).toContain("migrated lockfile from yarn.lock");
expect(exitCode).toBe(0);

const lock = Bun.JSONC.parse(await Bun.file(join(tmpDir, "bun.lock")).text()) as { packages: unknown };
expect(lock.packages).toStrictEqual({
"@aliased/node-types": ["@types/node@20.11.5", "", {}, typesNode20Integrity],
"@types/node": ["@types/node@18.19.0", "", {}, typesNode18Integrity],
"my-node-types": ["@types/node@20.11.5", "", {}, typesNode20Integrity],
"needs-node-types": [
"needs-node-types@1.0.0",
"",
{ dependencies: { "types-alias": "npm:@types/node@20.11.5" } },
needsNodeTypesIntegrity,
],
"types-alias": ["@types/node@20.11.5", "", {}, typesNode20Integrity],
});

expect([...requestedManifests].sort()).toStrictEqual(["@types/node", "needs-node-types"]);
});

test("yarn.lock with resolutions", async () => {
await using tmpDir = tempDir("yarn-migration-resolutions", {
"package.json": JSON.stringify(
Expand Down