Skip to content
Open
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
36 changes: 36 additions & 0 deletions .yarn/versions/b4b98142.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
releases:
"@yarnpkg/cli": minor
"@yarnpkg/core": minor
"@yarnpkg/plugin-essentials": minor

declined:
- "@yarnpkg/plugin-catalog"
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-exec"
- "@yarnpkg/plugin-file"
- "@yarnpkg/plugin-git"
- "@yarnpkg/plugin-github"
- "@yarnpkg/plugin-http"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-jsr"
- "@yarnpkg/plugin-link"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-npm"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/doctor"
- "@yarnpkg/extensions"
- "@yarnpkg/nm"
- "@yarnpkg/pnpify"
- "@yarnpkg/sdks"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = require(`./package.json`);

for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) {
for (const dep of Object.keys(module.exports[key] || {})) {
module.exports[key][dep] = require(dep);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "release-date",
"version": "2.0.0",
"dependencies": {
"release-date-transitive": "^2.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = require(`./package.json`);

for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) {
for (const dep of Object.keys(module.exports[key] || {})) {
module.exports[key][dep] = require(dep);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "release-date-transitive",
"version": "2.0.0"
}
164 changes: 164 additions & 0 deletions packages/acceptance-tests/pkg-tests-specs/sources/commands/why.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,169 @@ describe(`Commands`, () => {
}]);
}),
);

describe(`with a specified version`, () => {
test(
`it should list workspaces using a specific version range`,
makeTemporaryEnv({
workspaces: [`packages/*`],
}, async ({path, run, source}) => {
await fs.writeJson(ppath.join(path, `packages/a/package.json`), {
name: `a`,
dependencies: {
[`b`]: `workspace:^`,
[`c`]: `workspace:^`,
[`no-deps`]: `1.0.0`,
},
});

await fs.writeJson(ppath.join(path, `packages/b/package.json`), {
name: `b`,
dependencies: {
[`no-deps`]: `1.1.0`,
},
});

await fs.writeJson(ppath.join(path, `packages/c/package.json`), {
name: `c`,
dependencies: {
[`no-deps`]: `2.0.0`,
},
});

await run(`install`);

const {stdout} = await run(`why`, `no-deps@^1.1.0`, `--json`);

// Don't list v1.0.0 (package A) nor v2.0.0 (package C)
expect(misc.parseJsonStream(stdout)).toEqual([{
value: `b@workspace:packages/b`,
children: {
[`no-deps@npm:1.1.0`]: {
descriptor: `no-deps@npm:1.1.0`,
locator: `no-deps@npm:1.1.0`,
},
},
}]);
}));

test(
`it should list workspaces transitively using a specific version range`,
makeTemporaryEnv({
workspaces: [`packages/*`],
}, async ({path, run, source}) => {
await fs.writeJson(ppath.join(path, `packages/a/package.json`), {
name: `a`,
dependencies: {
[`b`]: `workspace:^`,
[`release-date`]: `1.0.0`,
},
});

await fs.writeJson(ppath.join(path, `packages/b/package.json`), {
name: `b`,
dependencies: {
[`c`]: `workspace:^`,
[`release-date`]: `1.1.0`,
},
});

await fs.writeJson(ppath.join(path, `packages/c/package.json`), {
name: `c`,
dependencies: {
[`d`]: `workspace:^`,
[`release-date`]: `1.1.1`,
},
});

await fs.writeJson(ppath.join(path, `packages/d/package.json`), {
name: `d`,
dependencies: {
[`release-date`]: `2.0.0`,
},
});

await run(`install`);

const {stdout} = await run(`why`, `-R`, `release-date-transitive@^1.1.0`, `--json`);

expect(stdout).not.toContain(`release-date-transitive@npm:1.0.0`);
expect(stdout).not.toContain(`release-date-transitive@npm:2.0.0`);

// Don't list v1.0.0 (package A) nor v2.0.0 (package D)
expect(misc.parseJsonStream(stdout)).toEqual([{
value: `a@workspace:packages/a`,
children: {
[`b@workspace:packages/b`]: {
children: {},
value: {
descriptor: `b@workspace:^`,
locator: `b@workspace:packages/b`,
},
},
[`release-date@npm:1.0.0`]: {
value: {
descriptor: `release-date@npm:1.0.0`,
locator: `release-date@npm:1.0.0`,
},
children: {
[`release-date-transitive@npm:1.1.1`]: {
children: {},
value: {
descriptor: `release-date-transitive@npm:^1.0.0`,
locator: `release-date-transitive@npm:1.1.1`,
},
},
},
},
},
}, {
value: `b@workspace:packages/b`,
children: {
[`c@workspace:packages/c`]: {
children: {},
value: {
descriptor: `c@workspace:^`,
locator: `c@workspace:packages/c`,
},
},
"release-date@npm:1.1.0": {
children: {
"release-date-transitive@npm:1.1.1": {
children: {},
value: {
descriptor: `release-date-transitive@npm:^1.0.0`,
locator: `release-date-transitive@npm:1.1.1`,
},
},
},
value: {
descriptor: `release-date@npm:1.1.0`,
locator: `release-date@npm:1.1.0`,
},
},
},
}, {
value: `c@workspace:packages/c`,
children: {
[`release-date@npm:1.1.1`]: {
value: {
descriptor: `release-date@npm:1.1.1`,
locator: `release-date@npm:1.1.1`,
},
children: {
[`release-date-transitive@npm:1.1.1`]: {
children: {},
value: {
descriptor: `release-date-transitive@npm:^1.0.0`,
locator: `release-date-transitive@npm:1.1.1`,
},
},
},
},
},
}]);
}));
});
});
});
43 changes: 31 additions & 12 deletions packages/plugin-essentials/sources/commands/why.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
import {Configuration, LocatorHash, Package, formatUtils, Descriptor} from '@yarnpkg/core';
import {IdentHash, Project} from '@yarnpkg/core';
import {Ident, Project} from '@yarnpkg/core';
import {miscUtils, structUtils, treeUtils} from '@yarnpkg/core';
import {Command, Option, Usage} from 'clipanion';

Expand All @@ -13,13 +13,19 @@ export default class WhyCommand extends BaseCommand {
static usage: Usage = Command.Usage({
description: `display the reason why a package is needed`,
details: `
This command prints the exact reasons why a package appears in the dependency tree.
This command prints the exact reasons why a package appears in the dependency tree. Specify a version or range to determine why the dependency tree contains a specific version of a package. This is particularly useful when trying to find out why your project depends on lower versions.

If \`-R,--recursive\` is set, the listing will go in depth and will list, for each workspaces, what are all the paths that lead to the dependency. Note that the display is somewhat optimized in that it will not print the package listing twice for a single package, so if you see a leaf named "Foo" when looking for "Bar", it means that "Foo" already got printed higher in the tree.
`,
examples: [[
`Explain why lodash is used in your project`,
`$0 why lodash`,
], [
`Explain why version 3.3.1 of lodash is in your project`,
`$0 why lodash@3.3.1`,
], [
`or why version 3.X of lodash is in your project`,
`$0 why lodash@^3`,
]],
});

Expand All @@ -46,11 +52,11 @@ export default class WhyCommand extends BaseCommand {

await project.restoreInstallState();

const identHash = structUtils.parseIdent(this.package).identHash;
const descriptor = structUtils.parseDescriptor(this.package, false);

const whyTree = this.recursive
? whyRecursive(project, identHash, {configuration, peers: this.peers})
: whySimple(project, identHash, {configuration, peers: this.peers});
? whyRecursive(project, descriptor, {configuration, peers: this.peers})
: whySimple(project, descriptor, {configuration, peers: this.peers});

treeUtils.emitTree(whyTree, {
configuration,
Expand All @@ -61,7 +67,11 @@ export default class WhyCommand extends BaseCommand {
}
}

function whySimple(project: Project, identHash: IdentHash, {configuration, peers}: {configuration: Configuration, peers: boolean}) {
function isSameIdent(pkg: Ident, targetPkg: Ident) {
return pkg.identHash === targetPkg.identHash;
}

function whySimple(project: Project, targetPkg: Descriptor, {configuration, peers}: {configuration: Configuration, peers: boolean}) {
const sortedPackages = miscUtils.sortMap(project.storedPackages.values(), pkg => {
return structUtils.stringifyLocator(pkg);
});
Expand All @@ -85,7 +95,11 @@ function whySimple(project: Project, identHash: IdentHash, {configuration, peers
if (!nextPkg)
throw new Error(`Assertion failed: The package should have been registered`);

if (nextPkg.identHash !== identHash)

if (!isSameIdent(nextPkg, targetPkg))
continue;

if (!structUtils.isPackageInRange(nextPkg, targetPkg.range))
continue;

if (node === null) {
Expand All @@ -104,7 +118,7 @@ function whySimple(project: Project, identHash: IdentHash, {configuration, peers
return root;
}

function whyRecursive(project: Project, identHash: IdentHash, {configuration, peers}: {configuration: Configuration, peers: boolean}) {
function whyRecursive(project: Project, targetPkg: Descriptor, {configuration, peers}: {configuration: Configuration, peers: boolean}) {
const sortedWorkspaces = miscUtils.sortMap(project.workspaces, workspace => {
return structUtils.stringifyLocator(workspace.anchoredLocator);
});
Expand All @@ -118,16 +132,13 @@ function whyRecursive(project: Project, identHash: IdentHash, {configuration, pe

seen.add(pkg.locatorHash);

if (pkg.identHash === identHash) {
if (isSameIdent(pkg, targetPkg)) {
dependents.add(pkg.locatorHash);
return true;
}

let depends = false;

if (pkg.identHash === identHash)
depends = true;

for (const dependency of pkg.dependencies.values()) {
if (!peers && pkg.peerDependencies.has(dependency.identHash))
continue;
Expand All @@ -140,6 +151,10 @@ function whyRecursive(project: Project, identHash: IdentHash, {configuration, pe
if (!nextPkg)
throw new Error(`Assertion failed: The package should have been registered`);

if (isSameIdent(nextPkg, targetPkg) && !structUtils.isPackageInRange(nextPkg, targetPkg.range))
continue;


if (markAllDependents(nextPkg)) {
depends = true;
}
Expand Down Expand Up @@ -181,6 +196,10 @@ function whyRecursive(project: Project, identHash: IdentHash, {configuration, pe
if (dependency !== null && project.tryWorkspaceByLocator(pkg))
return;

// We don't want to print the full path if it doesn't transitively depend on targetPkg.range
if (isSameIdent(pkg, targetPkg) && !structUtils.isPackageInRange(pkg, targetPkg.range))
return;

// We don't want to reprint the children for a package that already got
// printed as part of another branch
if (printed.has(pkg.locatorHash))
Expand Down
Loading