diff --git a/dev/package.json b/dev/package.json index 6117df13a..a734b6271 100644 --- a/dev/package.json +++ b/dev/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@types/cors": "^2.8.19", - "@types/express": "^4.17.21", + "@types/express": "^4.17.25", "@types/node": "^20.12.7", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", @@ -67,7 +67,7 @@ "dotenv": "^17.2.3", "esbuild": "^0.25.9", "esbuild-shim-plugin": "^1.0.3", - "express": "^4.21.2", + "express": "^4.22.1", "fast-glob": "^3.3.3", "js-yaml": "^4.1.1", "ts-graphviz": "^1.0.1", diff --git a/package-lock.json b/package-lock.json index e29a4a190..782823035 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,7 +107,7 @@ "dotenv": "^17.2.3", "esbuild": "^0.25.9", "esbuild-shim-plugin": "^1.0.3", - "express": "^4.21.2", + "express": "^4.22.1", "fast-glob": "^3.3.3", "js-yaml": "^4.1.1", "ts-graphviz": "^1.0.1", @@ -119,7 +119,7 @@ }, "devDependencies": { "@types/cors": "^2.8.19", - "@types/express": "^4.17.21", + "@types/express": "^4.17.25", "@types/node": "^20.12.7", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", diff --git a/tests/integration/workspace_manifests/dependency_alignment_test.ts b/tests/integration/workspace_manifests/dependency_alignment_test.ts new file mode 100644 index 000000000..8e7a59f32 --- /dev/null +++ b/tests/integration/workspace_manifests/dependency_alignment_test.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {readFileSync} from 'node:fs'; +import path from 'node:path'; +import {describe, expect, it} from 'vitest'; + +/** + * `peerDependencies` are excluded because a peer range is deliberately allowed + * to be wider than the dependency range that satisfies it. The root manifest is + * excluded too: it is the tooling root rather than a shipped workspace. + */ +const CHECKED_FIELDS = ['dependencies', 'devDependencies'] as const; + +interface Manifest { + workspaces?: string[]; + dependencies?: Record; + devDependencies?: Record; +} + +interface Declaration { + site: string; + range: string; +} + +/** Reads a manifest relative to the repo root, which vitest sets as the cwd. */ +function readManifest(workspaceDir: string): Manifest { + return JSON.parse( + readFileSync( + path.join(process.cwd(), workspaceDir, 'package.json'), + 'utf8', + ), + ); +} + +describe('workspace dependency ranges', () => { + it('declares one range per package across all workspaces', () => { + const workspaces = readManifest('.').workspaces ?? []; + const declarations = new Map(); + + for (const workspace of workspaces) { + const manifest = readManifest(workspace); + for (const field of CHECKED_FIELDS) { + for (const [name, range] of Object.entries(manifest[field] ?? {})) { + const sites = declarations.get(name) ?? []; + sites.push({site: `${workspace} (${field})`, range}); + declarations.set(name, sites); + } + } + } + + const violations = [...declarations] + .filter(([, sites]) => new Set(sites.map((s) => s.range)).size > 1) + .map( + ([name, sites]) => + `${name}: ${sites.map((s) => `${s.site} -> ${s.range}`).join(', ')}`, + ); + + expect(workspaces.length).toBeGreaterThan(1); + expect(violations).toEqual([]); + }); +});