diff --git a/packages/server/src/server/session/daemon/daemon-self-updater.test.ts b/packages/server/src/server/session/daemon/daemon-self-updater.test.ts index c272a7f11a..711fd11d80 100644 --- a/packages/server/src/server/session/daemon/daemon-self-updater.test.ts +++ b/packages/server/src/server/session/daemon/daemon-self-updater.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from "vitest"; +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; +import { describe, expect, onTestFinished, test } from "vitest"; import { DaemonSelfUpdateInProgressError, DaemonSelfUpdater, @@ -106,9 +108,11 @@ async function runUpdate(input: { describe("DaemonSelfUpdater", () => { test("probes and updates the running install's prefix when npm defaults elsewhere", async () => { - const prefix = path.resolve("custom npm prefix"); + const prefix = mkdtempSync(path.join(tmpdir(), "custom npm prefix ")); + onTestFinished(() => rmSync(prefix, { recursive: true, force: true })); const root = process.platform === "win32" ? prefix : path.join(prefix, "lib"); const packagePath = path.join(root, "node_modules", "@getpaseo", "cli"); + mkdirSync(packagePath, { recursive: true }); const commands: string[][] = []; let version = "0.1.15"; const npm = new DefaultNpmGlobalPaseoCli(async (_command, args) => { diff --git a/packages/server/src/server/session/daemon/npm-global-cli.test.ts b/packages/server/src/server/session/daemon/npm-global-cli.test.ts index aece7eb09a..ba852e232e 100644 --- a/packages/server/src/server/session/daemon/npm-global-cli.test.ts +++ b/packages/server/src/server/session/daemon/npm-global-cli.test.ts @@ -1,5 +1,7 @@ +import { mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; -import { describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test } from "vitest"; import { DefaultNpmGlobalPaseoCli } from "./npm-global-cli.js"; interface CommandCall { @@ -10,25 +12,65 @@ interface CommandCall { } const globalRoot = path.join(path.sep, "global", "lib"); -const globalNodeModules = path.join(globalRoot, "node_modules"); -const cliPackagePath = path.join(globalNodeModules, "@getpaseo", "cli"); -function npmGlobalPaseoCliJson(version: string, options?: { linked?: boolean }): string { - return JSON.stringify({ - name: "lib", - path: globalRoot, - dependencies: { - "@getpaseo/cli": { - version, - path: cliPackagePath, - link: options?.linked === true, - }, - }, - }); +interface GlobalInstall { + root: string; + packagePath: string; + // npm 7+ `ls --json --long` marks neither kind of install with a link flag. + npmLsJson: string; } describe("DefaultNpmGlobalPaseoCli", () => { + const tempDirs: string[] = []; + + afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + function createGlobalInstall(options: { linked: boolean }): GlobalInstall { + const dir = mkdtempSync(path.join(tmpdir(), "paseo-npm-global-")); + tempDirs.push(dir); + const root = path.join(dir, "prefix", "lib"); + const packagePath = path.join(root, "node_modules", "@getpaseo", "cli"); + const checkoutCli = path.join(dir, "checkout", "packages", "cli"); + if (options.linked) { + mkdirSync(checkoutCli, { recursive: true }); + mkdirSync(path.dirname(packagePath), { recursive: true }); + symlinkSync(checkoutCli, packagePath, "junction"); + } else { + mkdirSync(packagePath, { recursive: true }); + } + const npmLsJson = JSON.stringify({ + name: "lib", + path: root, + dependencies: { + "@getpaseo/cli": { + version: "0.1.15", + resolved: options.linked + ? `file:${path.relative(root, checkoutCli)}` + : "https://registry.npmjs.org/@getpaseo/cli/-/cli-0.1.15.tgz", + path: packagePath, + }, + }, + }); + return { root, packagePath, npmLsJson }; + } + + test("reports a global install linked to a local checkout as linked", async () => { + const install = createGlobalInstall({ linked: true }); + const cli = new DefaultNpmGlobalPaseoCli(async () => ({ + exitCode: 0, + stdout: install.npmLsJson, + stderr: "", + })); + + await expect(cli.inspect()).resolves.toMatchObject({ isLinked: true }); + }); + test("inspects the npm global cli install with npm -g ls", async () => { + const install = createGlobalInstall({ linked: false }); const calls: CommandCall[] = []; const cli = new DefaultNpmGlobalPaseoCli(async (command, args, options) => { calls.push({ @@ -37,13 +79,13 @@ describe("DefaultNpmGlobalPaseoCli", () => { timeout: options?.timeout, maxBuffer: options?.maxBuffer, }); - return { exitCode: 0, stdout: npmGlobalPaseoCliJson("0.1.15"), stderr: "" }; + return { exitCode: 0, stdout: install.npmLsJson, stderr: "" }; }); await expect(cli.inspect()).resolves.toEqual({ version: "0.1.15", - packagePath: cliPackagePath, - globalRootPath: globalRoot, + packagePath: install.packagePath, + globalRootPath: install.root, isLinked: false, }); expect(calls).toEqual([ diff --git a/packages/server/src/server/session/daemon/npm-global-cli.ts b/packages/server/src/server/session/daemon/npm-global-cli.ts index 2eef49980f..6133dcd9d1 100644 --- a/packages/server/src/server/session/daemon/npm-global-cli.ts +++ b/packages/server/src/server/session/daemon/npm-global-cli.ts @@ -1,3 +1,4 @@ +import { lstatSync } from "node:fs"; import { getErrorMessage } from "@getpaseo/protocol/error-utils"; import { z } from "zod"; import { execCommand } from "../../../utils/spawn.js"; @@ -19,7 +20,6 @@ const NpmGlobalCliPackageSchema = z .object({ version: z.string(), path: z.string(), - link: z.boolean().optional(), }) .passthrough(); @@ -112,7 +112,9 @@ function parseNpmGlobalPaseoInstall(stdout: string): NpmGlobalPaseoInstall | nul version: cliPackage.data.version, packagePath: cliPackage.data.path, globalRootPath: list.data.path ?? null, - isLinked: cliPackage.data.link === true, + // npm links an install by making its node_modules entry a symlink (a junction + // on Windows), and `npm ls --json` reports no flag for it. + isLinked: lstatSync(cliPackage.data.path).isSymbolicLink(), }; }