🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@changesets/get-dependents-graph

Package Overview
Dependencies
Maintainers
4
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@changesets/get-dependents-graph - npm Package Compare versions

Comparing version
3.0.0-next.1
to
3.0.0-next.2
+17
-0
CHANGELOG.md
# @changesets/get-dependents-graph
## 3.0.0-next.2
### Major Changes
- [#1655](https://github.com/changesets/changesets/pull/1655) [`db46911`](https://github.com/changesets/changesets/commit/db46911e57603f20a158a47bbbebd112272c84e2) Thanks [@bluwy](https://github.com/bluwy)! - Update `@manypkg/get-packages` which drops support for detecting packages in Bolt monorepos and adds support for npm monorepos
### Patch Changes
- Updated dependencies [[`c19b112`](https://github.com/changesets/changesets/commit/c19b1123d27986da0e14e99d65b0f9a408def35c)]:
- @changesets/types@7.0.0-next.2
## 2.1.4
### Patch Changes
- [#1888](https://github.com/changesets/changesets/pull/1888) [`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464) Thanks [@mixelburg](https://github.com/mixelburg)! - Fix dependency graph validation for workspace path references. Valid `workspace:packages/pkg` specifiers are now treated as local dependencies instead of being rejected as invalid ranges.
## 3.0.0-next.1

@@ -4,0 +21,0 @@

+26
-7
import Range from 'semver/classes/range.js';
import pc from 'picocolors';
import path from 'node:path';

@@ -32,3 +33,3 @@ // This is a modified version of the graph-getting in bolt

};
function getDependencyGraph(packages, {
function getDependencyGraph(packages, rootPackage, {
ignoreDevDependencies = false,

@@ -40,8 +41,12 @@ bumpVersionsWithWorkspaceProtocolOnly = false

const packagesByName = {
[packages.root.packageJson.name]: packages.root
[rootPackage.packageJson.name]: rootPackage
};
const queue = [packages.root];
const relativePathsByName = {
[rootPackage.packageJson.name]: "."
};
const queue = [rootPackage];
for (const pkg of packages.packages) {
queue.push(pkg);
packagesByName[pkg.packageJson.name] = pkg;
relativePathsByName[pkg.packageJson.name] = path.relative(rootPackage.dir, pkg.dir).replace(/\\/g, "/");
}

@@ -58,2 +63,3 @@ for (const pkg of queue) {

const expected = match.packageJson.version;
const rawDepRange = depRange;
const usesWorkspaceRange = depRange.startsWith("workspace:");

@@ -66,2 +72,11 @@ if (usesWorkspaceRange) {

}
if (path.posix.normalize(depRange) === relativePathsByName[depName]) {
dependencies.push(depName);
continue;
}
if (!getValidRange(depRange)) {
valid = false;
console.error(`Package ${pc.cyan(`"${name}"`)} must depend on the current version of ${pc.cyan(`"${depName}"`)}: ${pc.green(`"${expected}"`)} vs ${pc.red(`"${rawDepRange}"`)}`);
continue;
}
} else if (bumpVersionsWithWorkspaceProtocolOnly) {

@@ -73,3 +88,3 @@ continue;

valid = false;
console.error(`Package ${pc.cyan(`"${name}"`)} must depend on the current version of ${pc.cyan(`"${depName}"`)}: ${pc.green(`"${expected}"`)} vs ${pc.red(`"${depRange}"`)}`);
console.error(`Package ${pc.cyan(`"${name}"`)} must depend on the current version of ${pc.cyan(`"${depName}"`)}: ${pc.green(`"${expected}"`)} vs ${pc.red(`"${rawDepRange}"`)}`);
continue;

@@ -98,8 +113,12 @@ }

const graph = new Map();
const rootPackage = packages.rootPackage;
if (rootPackage == null) {
return new Map();
}
const {
graph: dependencyGraph
} = getDependencyGraph(packages, opts);
} = getDependencyGraph(packages, rootPackage, opts);
const dependentsLookup = {
[packages.root.packageJson.name]: {
pkg: packages.root,
[rootPackage.packageJson.name]: {
pkg: rootPackage,
dependents: []

@@ -106,0 +125,0 @@ }

@@ -1,5 +0,5 @@

import type { Packages } from "@manypkg/get-packages";
import type { Packages } from "@changesets/types";
export declare function getDependentsGraph(packages: Packages, opts?: {
ignoreDevDependencies?: boolean;
bumpVersionsWithWorkspaceProtocolOnly?: boolean;
}): Map<string, string[]>;
}): Map<any, any>;
{
"name": "@changesets/get-dependents-graph",
"version": "3.0.0-next.1",
"version": "3.0.0-next.2",
"description": "Get the graph of dependents in a monorepo",

@@ -13,4 +13,3 @@ "type": "module",

"dependencies": {
"@changesets/types": "^7.0.0-next.1",
"@manypkg/get-packages": "^1.1.3",
"@changesets/types": "^7.0.0-next.2",
"picocolors": "^1.1.0",

@@ -20,8 +19,8 @@ "semver": "^7.5.3"

"devDependencies": {
"@changesets/test-utils": "0.0.9-next.1",
"vitest": "^3.2.4"
"@changesets/test-utils": "0.0.9-next.2",
"vitest": "^4.1.2"
},
"engines": {
"node": ">=20.0.0"
"node": ">=20.19.0"
}
}

@@ -12,5 +12,7 @@ # Get Dependents Graph

let { graph, valid } = getDependentsGraph(await getPackages(cwd));
const packages = await getPackages(cwd);
let { graph, valid } = getDependentsGraph(packages);
```
Mostly published for use in [changesets](https://www.npmjs.com/package/@changesets/cli)

@@ -5,2 +5,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import getDependencyGraph from "./get-dependency-graph.ts";
import { Package } from "@changesets/types";
import path from "node:path";

@@ -19,28 +21,33 @@ const consoleError = console.error;

it("should skip dependencies specified through the link protocol", function () {
const { graph, valid } = getDependencyGraph({
root: {
dir: ".",
packageJson: { name: "root", version: "1.0.0" },
},
packages: [
{
dir: "foo",
packageJson: {
name: "foo",
version: "1.0.0",
devDependencies: {
bar: "link:../bar",
const rootPackage: Package = {
dir: path.resolve(),
packageJson: { name: "root", version: "1.0.0" },
};
const { graph, valid } = getDependencyGraph(
{
tool: { type: "yarn" },
rootDir: rootPackage.dir,
rootPackage,
packages: [
{
dir: "foo",
packageJson: {
name: "foo",
version: "1.0.0",
devDependencies: {
bar: "link:../bar",
},
},
},
},
{
dir: "bar",
packageJson: {
name: "bar",
version: "1.0.0",
{
dir: "bar",
packageJson: {
name: "bar",
version: "1.0.0",
},
},
},
],
tool: "pnpm",
});
],
},
rootPackage,
);
expect(graph.get("foo")!.dependencies).toStrictEqual([]);

@@ -52,28 +59,33 @@ expect(valid).toBeTruthy();

it("should skip dependencies specified using a tag", function () {
const { graph, valid } = getDependencyGraph({
root: {
dir: ".",
packageJson: { name: "root", version: "1.0.0" },
},
packages: [
{
dir: "examples/foo",
packageJson: {
name: "foo-example",
version: "1.0.0",
dependencies: {
bar: "latest",
const rootPackage: Package = {
dir: path.resolve(),
packageJson: { name: "root", version: "1.0.0" },
};
const { graph, valid } = getDependencyGraph(
{
tool: { type: "yarn" },
rootDir: rootPackage.dir,
rootPackage,
packages: [
{
dir: "examples/foo",
packageJson: {
name: "foo-example",
version: "1.0.0",
dependencies: {
bar: "latest",
},
},
},
},
{
dir: "packages/bar",
packageJson: {
name: "bar",
version: "1.0.0",
{
dir: "packages/bar",
packageJson: {
name: "bar",
version: "1.0.0",
},
},
},
],
tool: "pnpm",
});
],
},
rootPackage,
);
expect(graph.get("foo-example")!.dependencies).toStrictEqual([]);

@@ -87,10 +99,141 @@ expect(valid).toBeTruthy();

temporarilySilenceLogs(() => {
const { valid } = getDependencyGraph({
root: {
dir: ".",
packageJson: { name: "root", version: "1.0.0" },
const rootPackage: Package = {
dir: path.resolve(),
packageJson: { name: "root", version: "1.0.0" },
};
const { valid } = getDependencyGraph(
{
tool: { type: "yarn" },
rootDir: rootPackage.dir,
rootPackage,
packages: [
{
dir: "foo",
packageJson: {
name: "foo",
version: "1.0.0",
dependencies: {
bar: "link:../bar",
},
},
},
{
dir: "bar",
packageJson: {
name: "bar",
version: "1.0.0",
},
},
],
},
rootPackage,
);
expect(valid).toBeFalsy();
expect((console.error as any).mock.calls).toHaveLength(1);
expect(
stripVTControlCharacters((console.error as any).mock.calls[0][0]),
).toBe(
`Package "foo" must depend on the current version of "bar": "1.0.0" vs "link:../bar"`,
);
}),
);
it(
"should error on dependencies not specified using workspace protocol when bumpVersionsWithWorkspaceProtocolOnly is false",
temporarilySilenceLogs(() => {
const rootPackage: Package = {
dir: path.resolve(),
packageJson: { name: "root", version: "1.0.0" },
};
const { valid } = getDependencyGraph(
{
tool: { type: "yarn" },
rootDir: rootPackage.dir,
rootPackage,
packages: [
{
dir: "foo",
packageJson: {
name: "foo",
version: "1.0.0",
dependencies: {
bar: "0.9.0",
},
},
},
{
dir: "bar",
packageJson: {
name: "bar",
version: "1.0.0",
},
},
],
},
rootPackage,
);
expect(valid).toBe(false);
expect(
stripVTControlCharacters((console.error as any).mock.calls[0][0]),
).toMatchInlineSnapshot(
`"Package "foo" must depend on the current version of "bar": "1.0.0" vs "0.9.0""`,
);
}),
);
it(
"should skip dependencies not specified using workspace protocol when bumpVersionsWithWorkspaceProtocolOnly is true",
temporarilySilenceLogs(() => {
const rootPackage: Package = {
dir: path.resolve(),
packageJson: { name: "root", version: "1.0.0" },
};
const { valid } = getDependencyGraph(
{
tool: { type: "yarn" },
rootDir: rootPackage.dir,
rootPackage,
packages: [
{
dir: "foo",
packageJson: {
name: "foo",
version: "1.0.0",
dependencies: {
bar: "0.9.0",
},
},
},
{
dir: "bar",
packageJson: {
name: "bar",
version: "1.0.0",
},
},
],
},
rootPackage,
{
bumpVersionsWithWorkspaceProtocolOnly: true,
},
);
expect(valid).toBe(true);
expect((console.error as any).mock.calls).toMatchInlineSnapshot(`[]`);
}),
);
it("should treat workspace path dependencies as valid local dependencies", () => {
const rootPackage: Package = {
dir: path.resolve(),
packageJson: { name: "root", version: "1.0.0" },
};
const { graph, valid } = getDependencyGraph(
{
tool: { type: "pnpm" },
rootDir: rootPackage.dir,
rootPackage,
packages: [
{
dir: "foo",
dir: "packages/foo",
packageJson: {

@@ -100,3 +243,3 @@ name: "foo",

dependencies: {
bar: "link:../bar",
bar: "workspace:packages/bar",
},

@@ -106,3 +249,3 @@ },

{
dir: "bar",
dir: "packages/bar",
packageJson: {

@@ -114,13 +257,55 @@ name: "bar",

],
tool: "pnpm",
});
expect(valid).toBeFalsy();
expect((console.error as any).mock.calls).toHaveLength(1);
},
rootPackage,
);
expect(graph.get("foo")!.dependencies).toStrictEqual(["bar"]);
expect(valid).toBeTruthy();
expect((console.error as any).mock.calls).toMatchInlineSnapshot(`[]`);
});
it(
"should error on mismatched workspace path dependencies",
temporarilySilenceLogs(() => {
const rootPackage: Package = {
dir: path.resolve(),
packageJson: { name: "root", version: "1.0.0" },
};
const { graph, valid } = getDependencyGraph(
{
tool: { type: "pnpm" },
rootDir: rootPackage.dir,
rootPackage,
packages: [
{
dir: "packages/foo",
packageJson: {
name: "foo",
version: "1.0.0",
dependencies: {
bar: "workspace:packages/not-bar",
},
},
},
{
dir: "packages/bar",
packageJson: {
name: "bar",
version: "1.0.0",
},
},
],
},
rootPackage,
);
expect(graph.get("foo")!.dependencies).toStrictEqual([]);
expect(valid).toBe(false);
expect(
stripVTControlCharacters((console.error as any).mock.calls[0][0])
).toBe(
`Package "foo" must depend on the current version of "bar": "1.0.0" vs "link:../bar"`
stripVTControlCharacters((console.error as any).mock.calls[0][0]),
).toMatchInlineSnapshot(
`"Package "foo" must depend on the current version of "bar": "1.0.0" vs "workspace:packages/not-bar""`,
);
})
}),
);
});
// This is a modified version of the graph-getting in bolt
import Range from "semver/classes/range.js";
import pc from "picocolors";
import type { Packages, Package } from "@manypkg/get-packages";
import type { PackageJSON } from "@changesets/types";
import type { Package, Packages, PackageJSON } from "@changesets/types";
import path from "node:path";

@@ -16,3 +16,3 @@ const DEPENDENCY_TYPES = [

config: PackageJSON,
ignoreDevDependencies: boolean
ignoreDevDependencies: boolean,
) => {

@@ -59,2 +59,3 @@ const allDependencies = new Map<string, string>();

packages: Packages,
rootPackage: Package,
{

@@ -66,3 +67,3 @@ ignoreDevDependencies = false,

bumpVersionsWithWorkspaceProtocolOnly?: boolean;
} = {}
} = {},
): {

@@ -79,6 +80,9 @@ graph: Map<string, { pkg: Package; dependencies: Array<string> }>;

const packagesByName: { [key: string]: Package } = {
[packages.root.packageJson.name]: packages.root,
[rootPackage.packageJson.name]: rootPackage,
};
const relativePathsByName: { [key: string]: string } = {
[rootPackage.packageJson.name]: ".",
};
const queue = [packages.root];
const queue = [rootPackage];

@@ -88,10 +92,13 @@ for (const pkg of packages.packages) {

packagesByName[pkg.packageJson.name] = pkg;
relativePathsByName[pkg.packageJson.name] = path
.relative(rootPackage.dir, pkg.dir)
.replace(/\\/g, "/");
}
for (const pkg of queue) {
const { name } = pkg.packageJson;
const { name } = pkg!.packageJson;
const dependencies = [];
const allDependencies = getAllDependencies(
pkg.packageJson,
ignoreDevDependencies
ignoreDevDependencies,
);

@@ -104,2 +111,3 @@

const expected = match.packageJson.version;
const rawDepRange = depRange;
const usesWorkspaceRange = depRange.startsWith("workspace:");

@@ -114,2 +122,19 @@

}
if (path.posix.normalize(depRange) === relativePathsByName[depName]) {
dependencies.push(depName);
continue;
}
if (!getValidRange(depRange)) {
valid = false;
console.error(
`Package ${pc.cyan(
`"${name}"`,
)} must depend on the current version of ${pc.cyan(
`"${depName}"`,
)}: ${pc.green(`"${expected}"`)} vs ${pc.red(`"${rawDepRange}"`)}`,
);
continue;
}
} else if (bumpVersionsWithWorkspaceProtocolOnly) {

@@ -125,6 +150,6 @@ continue;

`Package ${pc.cyan(
`"${name}"`
`"${name}"`,
)} must depend on the current version of ${pc.cyan(
`"${depName}"`
)}: ${pc.green(`"${expected}"`)} vs ${pc.red(`"${depRange}"`)}`
`"${depName}"`,
)}: ${pc.green(`"${expected}"`)} vs ${pc.red(`"${rawDepRange}"`)}`,
);

@@ -131,0 +156,0 @@ continue;

@@ -1,2 +0,2 @@

import type { Packages, Package } from "@manypkg/get-packages";
import type { Package, Packages } from "@changesets/types";
import getDependencyGraph from "./get-dependency-graph.ts";

@@ -9,13 +9,22 @@

bumpVersionsWithWorkspaceProtocolOnly?: boolean;
}
},
) {
const graph: Map<string, { pkg: Package; dependents: string[] }> = new Map();
const { graph: dependencyGraph } = getDependencyGraph(packages, opts);
const rootPackage = packages.rootPackage;
if (rootPackage == null) {
return new Map();
}
const { graph: dependencyGraph } = getDependencyGraph(
packages,
rootPackage,
opts,
);
const dependentsLookup: {
[key: string]: { pkg: Package; dependents: Array<string> };
} = {
[packages.root.packageJson.name]: {
pkg: packages.root,
[rootPackage.packageJson.name]: {
pkg: rootPackage,
dependents: [],

@@ -22,0 +31,0 @@ },