@changesets/get-dependents-graph
Advanced tools
+17
-0
| # @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 @@ |
| 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>; |
+5
-6
| { | ||
| "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" | ||
| } | ||
| } |
+3
-1
@@ -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; |
+14
-5
@@ -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 @@ }, |
32161
32.44%3
-25%633
56.68%18
12.5%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed