From 155d30aca9e84db2e0ba22a7413dc35909855ac0 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Sat, 17 Jan 2026 04:24:16 -0800 Subject: [PATCH] Remove duplicate file operations in hasher code --- ...-f33f8d82-2bfd-40ba-bdd1-401ef24507f0.json | 11 ++ packages/hasher/src/PackageTree.ts | 8 +- packages/hasher/src/TargetHasher.ts | 110 +++++------ .../__tests__/resolveDependenciesHelper.ts | 47 ----- .../resolveExternalDependencies.test.ts | 178 +++++++++--------- .../resolveInternalDependencies.test.ts | 132 ------------- packages/hasher/src/nameAtVersion.ts | 3 - .../hasher/src/resolveExternalDependencies.ts | 72 +++---- .../hasher/src/resolveInternalDependencies.ts | 14 -- packages/monorepo-fixture/src/monorepo.ts | 2 +- 10 files changed, 179 insertions(+), 398 deletions(-) create mode 100644 change/change-f33f8d82-2bfd-40ba-bdd1-401ef24507f0.json delete mode 100644 packages/hasher/src/__tests__/resolveDependenciesHelper.ts delete mode 100644 packages/hasher/src/__tests__/resolveInternalDependencies.test.ts delete mode 100644 packages/hasher/src/nameAtVersion.ts delete mode 100644 packages/hasher/src/resolveInternalDependencies.ts diff --git a/change/change-f33f8d82-2bfd-40ba-bdd1-401ef24507f0.json b/change/change-f33f8d82-2bfd-40ba-bdd1-401ef24507f0.json new file mode 100644 index 000000000..ccd3eb96d --- /dev/null +++ b/change/change-f33f8d82-2bfd-40ba-bdd1-401ef24507f0.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "patch", + "comment": "Clean up TargetHasher internals", + "packageName": "@lage-run/hasher", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} diff --git a/packages/hasher/src/PackageTree.ts b/packages/hasher/src/PackageTree.ts index 2dc719931..fc050e6ac 100644 --- a/packages/hasher/src/PackageTree.ts +++ b/packages/hasher/src/PackageTree.ts @@ -31,14 +31,14 @@ export class PackageTree { constructor(private options: PackageTreeOptions) {} - reset(): void { + private reset(): void { // reset the internal state this.#tree = { children: {}, isPackage: true }; this.#packageFiles = {}; this.#memoizedPackageFiles = {}; } - async initialize(): Promise { + public async initialize(): Promise { const { root, includeUntracked, packageInfos } = this.options; this.reset(); @@ -77,7 +77,7 @@ export class PackageTree { } } - addToPackageTree(filePaths: string[]): void { + private addToPackageTree(filePaths: string[]): void { // key: path/to/package (packageRoot), value: array of a tuple of [file, hash] const packageFiles = this.#packageFiles; @@ -108,7 +108,7 @@ export class PackageTree { } } - getPackageFiles(packageName: string, patterns: string[]): string[] { + public getPackageFiles(packageName: string, patterns: string[]): string[] { const { root, packageInfos } = this.options; const packagePath = path.relative(root, path.dirname(packageInfos[packageName].packageJsonPath)).replace(/\\/g, "/"); diff --git a/packages/hasher/src/TargetHasher.ts b/packages/hasher/src/TargetHasher.ts index 74d4929b6..acdad8f54 100644 --- a/packages/hasher/src/TargetHasher.ts +++ b/packages/hasher/src/TargetHasher.ts @@ -5,18 +5,16 @@ import { globAsync } from "@lage-run/globby"; import fs from "fs"; import path from "path"; import { + type DependencyMap, type ParsedLock, - type WorkspaceInfos, type PackageInfos, - getWorkspaceInfosAsync, parseLockFile, createDependencyMap, + getPackageInfo, + getPackageInfosAsync, } from "workspace-tools"; -import type { DependencyMap } from "workspace-tools/lib/graph/createDependencyMap.js"; -import { infoFromPackageJson } from "workspace-tools/lib/infoFromPackageJson.js"; import { hashStrings } from "./hashStrings.js"; -import { resolveInternalDependencies } from "./resolveInternalDependencies.js"; import { resolveExternalDependencies } from "./resolveExternalDependencies.js"; import { FileHasher } from "./FileHasher.js"; import type { Logger } from "@lage-run/logger"; @@ -49,56 +47,28 @@ export interface TargetManifest { /** * TargetHasher is a class that can be used to generate a hash of a target. - * - * Currently, it encapsulates the use of `backfill-hasher` to generate a hash. + * It uses `glob-hasher` internally. */ export class TargetHasher { - targetHashesLog: Record; globalFileHashes: Record }> = {}; - targetHashesDirectory: string; + private targetHashesLog: Record; globalFileHashes: Record }> = {}; + private targetHashesDirectory: string; - logger: Logger | undefined; - fileHasher: FileHasher; - packageTree: PackageTree | undefined; + private logger: Logger | undefined; + private fileHasher: FileHasher; + public packageTree: PackageTree | undefined; - initializedPromise: Promise | undefined; + private initializedPromise: Promise | undefined; - packageInfos: PackageInfos = {}; - workspaceInfo: WorkspaceInfos | undefined; - globalInputsHash: Record | undefined; - lockInfo: ParsedLock | undefined; - targetHashes: Record = {}; + private packageInfos: PackageInfos = {}; + private globalInputsHash: Record | undefined; + private lockInfo: ParsedLock | undefined; + private targetHashes: Record = {}; - dependencyMap: DependencyMap = { + public dependencyMap: DependencyMap = { dependencies: new Map(), dependents: new Map(), }; - getPackageInfos(workspacePackages: WorkspaceInfos): PackageInfos { - const { root } = this.options; - const packageInfos: PackageInfos = {}; - - if (workspacePackages.length) { - for (const pkg of workspacePackages) { - packageInfos[pkg.name] = pkg.packageJson; - } - } else { - const packageJsonPath = path.join(root, "package.json"); - if (fs.existsSync(packageJsonPath)) { - try { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - const rootInfo = infoFromPackageJson(packageJson, packageJsonPath); - if (rootInfo) { - packageInfos[rootInfo.name] = rootInfo; - } - } catch (e) { - throw new Error(`Invalid package.json file detected ${packageJsonPath}: ${(e as Error)?.message || e}`); - } - } - } - - return packageInfos; - } - constructor(private options: TargetHasherOptions) { const { root, logger } = options; this.logger = logger; @@ -114,13 +84,13 @@ export class TargetHasher { } } - ensureInitialized(): void { + private ensureInitialized(): void { if (!this.initializedPromise) { throw new Error("TargetHasher is not initialized"); } } - async initialize(): Promise { + public async initialize(): Promise { const { environmentGlob, root } = this.options; if (this.initializedPromise) { @@ -135,22 +105,27 @@ export class TargetHasher { .then((files) => this.fileHasher.hash(files)) .then((hash) => (this.globalInputsHash = hash)), - getWorkspaceInfosAsync(root) - .then((workspaceInfo) => (this.workspaceInfo = workspaceInfo)) - .then(() => { - this.packageInfos = this.getPackageInfos(this.workspaceInfo!); + getPackageInfosAsync(root).then((packageInfos) => { + if (Object.keys(packageInfos).length) { + this.packageInfos = packageInfos; + } else { + const rootInfo = getPackageInfo(root); + if (rootInfo) { + this.packageInfos = { [rootInfo.name]: rootInfo }; + } + } - this.dependencyMap = createDependencyMap(this.packageInfos, { withDevDependencies: true, withPeerDependencies: false }); - this.packageTree = new PackageTree({ - root, - packageInfos: this.packageInfos, + this.dependencyMap = createDependencyMap(this.packageInfos, { withDevDependencies: true, withPeerDependencies: false }); + this.packageTree = new PackageTree({ + root, + packageInfos: this.packageInfos, - // TODO: (optimization) false if process.env.TF_BUILD || process.env.CI - includeUntracked: true, - }); + // TODO: (optimization) false if process.env.TF_BUILD || process.env.CI + includeUntracked: true, + }); - return this.packageTree.initialize(); - }), + return this.packageTree.initialize(); + }), parseLockFile(root).then((lockInfo) => (this.lockInfo = lockInfo)), ]); @@ -163,7 +138,7 @@ export class TargetHasher { } } - async hash(target: Target): Promise { + public async hash(target: Target): Promise { this.ensureInitialized(); const { root } = this.options; @@ -185,7 +160,6 @@ export class TargetHasher { // 2. add hash of target packages' internal and external deps const { dependencies, devDependencies } = this.packageInfos[target.packageName!]; - const workspaceInfo = this.workspaceInfo!; const parsedLock = this.lockInfo!; const allDependencies: Record = { @@ -193,8 +167,8 @@ export class TargetHasher { ...devDependencies, }; - const internalDeps = resolveInternalDependencies(allDependencies, workspaceInfo); - const externalDeps = resolveExternalDependencies(allDependencies, workspaceInfo, parsedLock); + const internalDeps = Object.keys(allDependencies).filter((dep) => this.packageInfos[dep]); + const externalDeps = resolveExternalDependencies(allDependencies, this.packageInfos, parsedLock); const resolvedDependencies = [...internalDeps, ...externalDeps].sort(); const files = getInputFiles(target, this.dependencyMap, this.packageTree!); @@ -228,7 +202,7 @@ export class TargetHasher { return hashString; } - writeTargetHashesManifest(): void { + private writeTargetHashesManifest(): void { for (const [id, { fileHashes, globalFileHashes }] of Object.entries(this.targetHashesLog)) { const targetHashesManifestPath = path.join(this.targetHashesDirectory, `${id}.json`); if (!fs.existsSync(path.dirname(targetHashesManifestPath))) { @@ -238,7 +212,7 @@ export class TargetHasher { } } - async getEnvironmentGlobHashes(root: string, target: Target): Promise> { + private async getEnvironmentGlobHashes(root: string, target: Target): Promise> { const globalFileHashes = target.environmentGlob ? this.fileHasher.hash(await globAsync(target.environmentGlob ?? [], { cwd: root })) : (this.globalInputsHash ?? {}); @@ -246,8 +220,8 @@ export class TargetHasher { return globalFileHashes; } - async cleanup(): Promise { + public async cleanup(): Promise { this.writeTargetHashesManifest(); - await this.fileHasher.writeManifest(); + this.fileHasher.writeManifest(); } } diff --git a/packages/hasher/src/__tests__/resolveDependenciesHelper.ts b/packages/hasher/src/__tests__/resolveDependenciesHelper.ts deleted file mode 100644 index f3cddfa97..000000000 --- a/packages/hasher/src/__tests__/resolveDependenciesHelper.ts +++ /dev/null @@ -1,47 +0,0 @@ -import path from "path"; -import { Monorepo } from "@lage-run/monorepo-fixture"; -const fixturesPath = path.join(__dirname, "..", "__fixtures__"); - -import { getYarnWorkspaces, getPnpmWorkspaces, getRushWorkspaces } from "workspace-tools"; - -export async function filterDependenciesInYarnFixture(fixture: string, filterFunction: any): Promise { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, fixture)); - const packageRoot = monorepo.root; - - const workspacesPackageInfo = getYarnWorkspaces(packageRoot); - - const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - - const filteredDependencies = filterFunction(dependencies, workspacesPackageInfo); - - return filteredDependencies; -} - -export async function filterDependenciesInPnpmFixture(fixture: string, filterFunction: any): Promise { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, fixture)); - const packageRoot = monorepo.root; - - const workspacesPackageInfo = getPnpmWorkspaces(packageRoot); - - const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - - const filteredDependencies = filterFunction(dependencies, workspacesPackageInfo); - - return filteredDependencies; -} - -export async function filterDependenciesInRushFixture(fixture: string, filterFunction: any): Promise { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, fixture)); - const packageRoot = monorepo.root; - - const workspacesPackageInfo = getRushWorkspaces(packageRoot); - - const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - - const filteredDependencies = filterFunction(dependencies, workspacesPackageInfo); - - return filteredDependencies; -} diff --git a/packages/hasher/src/__tests__/resolveExternalDependencies.test.ts b/packages/hasher/src/__tests__/resolveExternalDependencies.test.ts index 93fd90380..606f37736 100644 --- a/packages/hasher/src/__tests__/resolveExternalDependencies.test.ts +++ b/packages/hasher/src/__tests__/resolveExternalDependencies.test.ts @@ -1,131 +1,121 @@ -import { getPnpmWorkspaces, getRushWorkspaces, getYarnWorkspaces, parseLockFile } from "workspace-tools"; - -import { filterExternalDependencies, resolveExternalDependencies, addToQueue } from "../resolveExternalDependencies"; -import { filterDependenciesInYarnFixture } from "./resolveDependenciesHelper"; +import { parseLockFile, getPackageInfos } from "workspace-tools"; +import { + _filterExternalDependencies, + resolveExternalDependencies, + _addToQueue, + type DependencySpec, + type DependencyQueue, +} from "../resolveExternalDependencies"; import path from "path"; import { Monorepo } from "@lage-run/monorepo-fixture"; + const fixturesPath = path.join(__dirname, "..", "__fixtures__"); -describe("filterExternalDependencies()", () => { +describe("_filterExternalDependencies", () => { + let monorepo: Monorepo | undefined; + + afterEach(async () => { + await monorepo?.cleanup(); + monorepo = undefined; + }); + it("only lists external dependencies", async () => { - const results = await filterDependenciesInYarnFixture("monorepo", filterExternalDependencies); + monorepo = new Monorepo("monorepo"); + await monorepo.init(path.join(fixturesPath, "monorepo")); + + const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; + const results = _filterExternalDependencies(dependencies, getPackageInfos(monorepo.root)); + expect(results).toEqual({ foo: "1.0.0" }); }); it("identifies all dependencies as external packages if there are no workspaces", async () => { - const results = await filterDependenciesInYarnFixture("basic", filterExternalDependencies); - expect(results).toEqual({ foo: "1.0.0", "package-a": "1.0.0" }); + monorepo = new Monorepo("monorepo"); + await monorepo.init(path.join(fixturesPath, "basic")); + + const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; + const results = _filterExternalDependencies(dependencies, getPackageInfos(monorepo.root)); + + expect(results).toEqual(dependencies); }); }); -describe("addToQueue()", () => { +describe("_addToQueue", () => { it("adds external dependencies to queue", () => { const externalDependencies = { foo: "1.0.0" }; - const done: string[] = []; - const queue: [string, string][] = []; + const done = new Set(); + const queue: DependencyQueue = []; - addToQueue(externalDependencies, done, queue); + _addToQueue(externalDependencies, done, queue); - const expectedQueue = [["foo", "1.0.0"]]; - expect(queue).toEqual(expectedQueue); + expect(queue).toEqual([["foo", "1.0.0"]]); }); it("doesn't add to the queue if the dependency has been visited", () => { const externalDependencies = { foo: "1.0.0" }; - const done: string[] = ["foo@1.0.0"]; - const queue: [string, string][] = []; + const done = new Set(["foo@1.0.0"]); + const queue: DependencyQueue = []; - addToQueue(externalDependencies, done, queue); + _addToQueue(externalDependencies, done, queue); expect(queue).toEqual([]); }); it("doesn't add to queue if the dependency is already in the queue", () => { const externalDependencies = { foo: "1.0.0" }; - const done: string[] = []; - const queue: [string, string][] = [["foo", "1.0.0"]]; - - addToQueue(externalDependencies, done, queue); - - const expectedQueue = [["foo", "1.0.0"]]; - expect(queue).toEqual(expectedQueue); - }); -}); - -describe("resolveExternalDependencies() - yarn", () => { - it("given a list of external dependencies and a parsed Lock file, add all dependencies, transitively", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo")); - const packageRoot = monorepo.root; - - const workspaces = getYarnWorkspaces(packageRoot); + const done: Set = new Set(); + const queue: DependencyQueue = [["foo", "1.0.0"]]; - const allDependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - const parsedLockFile = await parseLockFile(packageRoot); + _addToQueue(externalDependencies, done, queue); - const resolvedDependencies = resolveExternalDependencies(allDependencies, workspaces, parsedLockFile); - - expect(resolvedDependencies).toEqual(["foo@1.0.0", "bar@^1.0.0"]); - await monorepo.cleanup(); + expect(queue).toEqual([["foo", "1.0.0"]]); }); }); -describe("resolveExternalDependencies() - pnpm", () => { - it("given a list of external dependencies and a parsed Lock file, add all dependencies, transitively", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo-pnpm")); - const packageRoot = monorepo.root; - const workspaces = getPnpmWorkspaces(packageRoot); - - const allDependencies = { - "package-a": "1.0.0", - once: "1.4.0", - }; - const parsedLockFile = await parseLockFile(packageRoot); +describe("resolveExternalDependencies", () => { + let monorepo: Monorepo | undefined; - const resolvedDependencies = resolveExternalDependencies(allDependencies, workspaces, parsedLockFile); - - expect(resolvedDependencies).toEqual(["once@1.4.0", "wrappy@1.0.2"]); - await monorepo.cleanup(); + afterEach(async () => { + await monorepo?.cleanup(); + monorepo = undefined; }); -}); - -describe("resolveExternalDependencies() - rush+pnpm", () => { - it("given a list of external dependencies and a parsed Lock file, add all dependencies, transitively", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo-rush-pnpm")); - const packageRoot = monorepo.root; - const workspaces = getRushWorkspaces(packageRoot); - - const allDependencies = { - "package-a": "1.0.0", - once: "1.4.0", - }; - const parsedLockFile = await parseLockFile(packageRoot); - - const resolvedDependencies = resolveExternalDependencies(allDependencies, workspaces, parsedLockFile); - - expect(resolvedDependencies).toEqual(["once@1.4.0", "wrappy@1.0.2"]); - await monorepo.cleanup(); - }); -}); - -describe("resolveExternalDependencies() - rush+yarn", () => { - it("given a list of external dependencies and a parsed Lock file, add all dependencies, transitively", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo-rush-yarn")); - const packageRoot = monorepo.root; - const workspaces = getRushWorkspaces(packageRoot); - - const allDependencies = { - "package-a": "1.0.0", - once: "1.4.0", - }; - const parsedLockFile = await parseLockFile(packageRoot); - - const resolvedDependencies = resolveExternalDependencies(allDependencies, workspaces, parsedLockFile); - expect(resolvedDependencies).toEqual(["once@1.4.0", "wrappy@1.0.2"]); - await monorepo.cleanup(); + it.each<{ + manager: "yarn" | "pnpm" | "rush"; + name: string; + fixture: string; + // only specified if different than the most common case + allDependencies?: Record; + expected?: DependencySpec[]; + }>([ + { + manager: "yarn", + name: "yarn", + fixture: "monorepo", + allDependencies: { "package-a": "1.0.0", foo: "1.0.0" }, + expected: ["foo@1.0.0", "bar@^1.0.0"], + }, + { manager: "pnpm", name: "pnpm", fixture: "monorepo-pnpm" }, + { manager: "rush", name: "rush+pnpm", fixture: "monorepo-rush-pnpm" }, + { manager: "rush", name: "rush+yarn", fixture: "monorepo-rush-yarn" }, + ])("transitively adds all external dependencies ($name)", async (params) => { + const { + fixture, + // These are used in most of the test cases + allDependencies = { "package-a": "1.0.0", once: "1.4.0" }, + expected = ["once@1.4.0", "wrappy@1.0.2"], + } = params; + + monorepo = new Monorepo("monorepo"); + await monorepo.init(path.join(fixturesPath, fixture)); + + const parsedLockFile = await parseLockFile(monorepo.root); + // TODO: use getPackageInfos(monorepo.root, manager) once available + // to ensure it's testing with the expected manager + const packageInfos = getPackageInfos(monorepo.root); + + const resolvedDependencies = resolveExternalDependencies(allDependencies, packageInfos, parsedLockFile); + + expect(resolvedDependencies).toEqual(expected); }); }); diff --git a/packages/hasher/src/__tests__/resolveInternalDependencies.test.ts b/packages/hasher/src/__tests__/resolveInternalDependencies.test.ts deleted file mode 100644 index b6331215a..000000000 --- a/packages/hasher/src/__tests__/resolveInternalDependencies.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { getPnpmWorkspaces, getRushWorkspaces, getYarnWorkspaces } from "workspace-tools"; - -import { filterInternalDependencies, resolveInternalDependencies } from "../resolveInternalDependencies"; -import { - filterDependenciesInYarnFixture, - filterDependenciesInPnpmFixture, - filterDependenciesInRushFixture, -} from "./resolveDependenciesHelper"; -import path from "path"; -import { Monorepo } from "@lage-run/monorepo-fixture"; -const fixturesPath = path.join(__dirname, "..", "__fixtures__"); - -describe("filterInternalDependencies() for yarn", () => { - it("only lists internal dependencies", async () => { - const results = await filterDependenciesInYarnFixture("monorepo", filterInternalDependencies); - - expect(results).toEqual(["package-a"]); - }); - - it("lists no internal packages if there are no workspaces", async () => { - const results = await filterDependenciesInYarnFixture("basic", filterInternalDependencies); - - expect(results).toEqual([]); - }); -}); - -describe("resolveInternalDependencies() for yarn", () => { - it("adds internal dependency names to the processedPackages list", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo")); - const packageRoot = monorepo.root; - const workspaces = getYarnWorkspaces(packageRoot); - - const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - - const resolvedDependencies = resolveInternalDependencies(dependencies, workspaces); - - expect(resolvedDependencies).toEqual(["package-a"]); - await monorepo.cleanup(); - }); -}); - -describe("filterInternalDependencies() for pnpm", () => { - it("only lists internal dependencies", async () => { - const results = await filterDependenciesInPnpmFixture("monorepo-pnpm", filterInternalDependencies); - - expect(results).toEqual(["package-a"]); - }); - - it("lists no internal packages if there are no workspaces", async () => { - const results = await filterDependenciesInPnpmFixture("basic", filterInternalDependencies); - - expect(results).toEqual([]); - }); -}); - -describe("resolveInternalDependencies() for pnpm", () => { - it("adds internal dependency names to the processedPackages list", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo-pnpm")); - const packageRoot = monorepo.root; - const workspaces = getPnpmWorkspaces(packageRoot); - - const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - - const resolvedDependencies = resolveInternalDependencies(dependencies, workspaces); - - expect(resolvedDependencies).toEqual(["package-a"]); - await monorepo.cleanup(); - }); -}); - -describe("filterInternalDependencies() for rush+pnpm", () => { - it("only lists internal dependencies", async () => { - const results = await filterDependenciesInRushFixture("monorepo-rush-pnpm", filterInternalDependencies); - - expect(results).toEqual(["package-a"]); - }); - - it("lists no internal packages if there are no workspaces", async () => { - const results = await filterDependenciesInRushFixture("basic", filterInternalDependencies); - - expect(results).toEqual([]); - }); -}); - -describe("resolveInternalDependencies() for rush+pnpm", () => { - it("adds internal dependency names to the processedPackages list", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo-rush-pnpm")); - const packageRoot = monorepo.root; - - const workspaces = getRushWorkspaces(packageRoot); - - const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - - const resolvedDependencies = resolveInternalDependencies(dependencies, workspaces); - - expect(resolvedDependencies).toEqual(["package-a"]); - await monorepo.cleanup(); - }); -}); - -describe("filterInternalDependencies() for rush+yarn", () => { - it("only lists internal dependencies", async () => { - const results = await filterDependenciesInRushFixture("monorepo-rush-yarn", filterInternalDependencies); - - expect(results).toEqual(["package-a"]); - }); - - it("lists no internal packages if there are no workspaces", async () => { - const results = await filterDependenciesInRushFixture("basic", filterInternalDependencies); - - expect(results).toEqual([]); - }); -}); - -describe("resolveInternalDependencies() for rush+yarn", () => { - it("adds internal dependency names to the processedPackages list", async () => { - const monorepo = new Monorepo("monorepo"); - await monorepo.init(path.join(fixturesPath, "monorepo-rush-yarn")); - const packageRoot = monorepo.root; - const workspaces = getRushWorkspaces(packageRoot); - - const dependencies = { "package-a": "1.0.0", foo: "1.0.0" }; - - const resolvedDependencies = resolveInternalDependencies(dependencies, workspaces); - - expect(resolvedDependencies).toEqual(["package-a"]); - await monorepo.cleanup(); - }); -}); diff --git a/packages/hasher/src/nameAtVersion.ts b/packages/hasher/src/nameAtVersion.ts deleted file mode 100644 index 7e40a9373..000000000 --- a/packages/hasher/src/nameAtVersion.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function nameAtVersion(name: string, version: string): string { - return `${name}@${version}`; -} diff --git a/packages/hasher/src/resolveExternalDependencies.ts b/packages/hasher/src/resolveExternalDependencies.ts index 02f5d3839..b5ccd3b13 100644 --- a/packages/hasher/src/resolveExternalDependencies.ts +++ b/packages/hasher/src/resolveExternalDependencies.ts @@ -1,53 +1,55 @@ -import type { ParsedLock, WorkspaceInfos } from "workspace-tools"; -import { queryLockFile } from "workspace-tools"; -import { nameAtVersion } from "./nameAtVersion.js"; +import { queryLockFile, type PackageInfos, type ParsedLock } from "workspace-tools"; -export type Dependencies = { [key in string]: string }; +type Dependencies = Record; -export type ExternalDependenciesQueue = { - name: string; - versionRange: string; -}[]; +export type DependencyQueue = [name: string, versionRange: string][]; -export function filterExternalDependencies(dependencies: Dependencies, workspaces: WorkspaceInfos): Dependencies { - const workspacePackageNames = workspaces.map((ws) => ws.name); +export type DependencySpec = `${string}@${string}`; + +/** Filter the `dependencies` object to only contain deps from outside the repo. */ +export function _filterExternalDependencies(dependencies: Dependencies, packageInfos: PackageInfos): Dependencies { const externalDependencies: Dependencies = {}; - Object.entries(dependencies).forEach(([name, versionRange]) => { - if (workspacePackageNames.indexOf(name) < 0) { + for (const [name, versionRange] of Object.entries(dependencies)) { + if (!packageInfos[name]) { externalDependencies[name] = versionRange; } - }); + } return externalDependencies; } -function isDone(done: string[], key: string): boolean { - return done.indexOf(key) >= 0; -} - -function isInQueue(queue: [string, string][], key: string): boolean { - return Boolean(queue.find(([name, versionRange]) => nameAtVersion(name, versionRange) === key)); +function isInQueue(queue: DependencyQueue, key: string): boolean { + return queue.some(([name, versionRange]) => `${name}@${versionRange}` === key); } -export function addToQueue(dependencies: Dependencies | undefined, done: string[], queue: [string, string][]): void { +export function _addToQueue(dependencies: Dependencies | undefined, done: Set, queue: DependencyQueue): void { if (dependencies) { - Object.entries(dependencies).forEach(([name, versionRange]) => { - const versionRangeSignature = nameAtVersion(name, versionRange); + for (const [name, versionRange] of Object.entries(dependencies)) { + const versionRangeSignature = `${name}@${versionRange}` as const; - if (!isDone(done, versionRangeSignature) && !isInQueue(queue, versionRangeSignature)) { + if (!done.has(versionRangeSignature) && !isInQueue(queue, versionRangeSignature)) { queue.push([name, versionRange]); } - }); + } } } -export function resolveExternalDependencies(allDependencies: Dependencies, workspaces: WorkspaceInfos, lockInfo: ParsedLock): string[] { - const externalDependencies = filterExternalDependencies(allDependencies, workspaces); - - const done: string[] = []; - const doneRange: string[] = []; - const queue = Object.entries(externalDependencies); +/** + * Resolve versions for external (outside repo) dependencies and their transitive dependencies + * using the lock file. + * @returns Array of strings in the format `name@version` + */ +export function resolveExternalDependencies( + allDependencies: Dependencies, + packageInfos: PackageInfos, + lockInfo: ParsedLock +): DependencySpec[] { + const externalDependencies = _filterExternalDependencies(allDependencies, packageInfos); + + const done = new Set(); + const doneRange = new Set(); + const queue: DependencyQueue = Object.entries(externalDependencies); while (queue.length > 0) { const next = queue.shift(); @@ -57,19 +59,19 @@ export function resolveExternalDependencies(allDependencies: Dependencies, works } const [name, versionRange] = next; - doneRange.push(nameAtVersion(name, versionRange)); + doneRange.add(`${name}@${versionRange}`); const lockFileResult = queryLockFile(name, versionRange, lockInfo); if (lockFileResult) { const { version, dependencies } = lockFileResult; - addToQueue(dependencies, doneRange, queue); - done.push(nameAtVersion(name, version)); + _addToQueue(dependencies, doneRange, queue); + done.add(`${name}@${version}`); } else { - done.push(nameAtVersion(name, versionRange)); + done.add(`${name}@${versionRange}`); } } - return done; + return [...done]; } diff --git a/packages/hasher/src/resolveInternalDependencies.ts b/packages/hasher/src/resolveInternalDependencies.ts deleted file mode 100644 index 6692691e8..000000000 --- a/packages/hasher/src/resolveInternalDependencies.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { WorkspaceInfos } from "workspace-tools"; - -export type Dependencies = { [key in string]: string }; - -export function filterInternalDependencies(dependencies: Dependencies, workspaces: WorkspaceInfos): string[] { - const workspacePackageNames = workspaces.map((ws) => ws.name); - return Object.keys(dependencies).filter((dependency) => workspacePackageNames.indexOf(dependency) >= 0); -} - -export function resolveInternalDependencies(allDependencies: Dependencies, workspaces: WorkspaceInfos): string[] { - const dependencyNames = filterInternalDependencies(allDependencies, workspaces); - - return dependencyNames; -} diff --git a/packages/monorepo-fixture/src/monorepo.ts b/packages/monorepo-fixture/src/monorepo.ts index 33f9a7b81..373593126 100644 --- a/packages/monorepo-fixture/src/monorepo.ts +++ b/packages/monorepo-fixture/src/monorepo.ts @@ -221,7 +221,7 @@ export class Monorepo { while (attempts < maxRetries) { try { - fs.rmdirSync(this.root, { recursive: true }); + fs.rmSync(this.root, { recursive: true }); break; } catch (error) { attempts++;