diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md new file mode 100644 index 0000000000..3f7fee21c2 --- /dev/null +++ b/.changeset/nub-package-manager.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/workers-utils": minor +"@cloudflare/autoconfig": minor +"wrangler": minor +--- + +Recognise nub as a package manager + +wrangler now detects nub — from its `npm_config_user_agent` and an installed `nub` binary — and autoconfig detects nub projects by their `nub.lock`, alongside npm, pnpm, yarn, and bun. diff --git a/packages/autoconfig/src/details/framework-detection.ts b/packages/autoconfig/src/details/framework-detection.ts index 3a64c2f601..9835f23a9c 100644 --- a/packages/autoconfig/src/details/framework-detection.ts +++ b/packages/autoconfig/src/details/framework-detection.ts @@ -4,6 +4,7 @@ import { BunPackageManager, FatalError, NpmPackageManager, + NubPackageManager, PnpmPackageManager, UserError, YarnPackageManager, @@ -82,7 +83,10 @@ export async function detectFramework( // Convert the package manager detected by @netlify/build-info to our PackageManager type. // This is populated after getBuildSettings() runs, which triggers the full detection chain. - const packageManager = convertDetectedPackageManager(project.packageManager); + const packageManager = convertDetectedPackageManager( + project.packageManager, + projectPath + ); const lockFileExists = packageManager.lockFiles.some((lockFile) => existsSync(join(projectPath, lockFile)) @@ -145,11 +149,20 @@ export async function detectFramework( * Falls back to npm if no package manager was detected. * * @param pkgManager The package manager detected by @netlify/build-info (from project.packageManager) + * @param projectPath Path to the project root, used to detect nub via its lock file * @returns A PackageManager object compatible with wrangler's package manager utilities */ function convertDetectedPackageManager( - pkgManager: { name: string } | null + pkgManager: { name: string } | null, + projectPath: string ): PackageManager { + // TODO: Remove this nub.lock check once netlify/build#7124 is merged and + // released, after which @netlify/build-info recognises nub directly. + // https://github.com/netlify/build/pull/7124 + if (existsSync(join(projectPath, "nub.lock"))) { + return NubPackageManager; + } + if (!pkgManager) { return NpmPackageManager; } diff --git a/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts b/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts index 22887c46c3..dd6377b0aa 100644 --- a/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts +++ b/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts @@ -1,6 +1,7 @@ import { BunPackageManager, NpmPackageManager, + NubPackageManager, PnpmPackageManager, YarnPackageManager, } from "@cloudflare/workers-utils"; @@ -57,6 +58,17 @@ describe("detectFramework() / package manager detection", () => { expect(result.packageManager).toStrictEqual(BunPackageManager); }); + it("detects nub when nub.lock is present", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "nub.lock": "", + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.packageManager).toStrictEqual(NubPackageManager); + }); + it("falls back to npm when no package manager lock file is present", async ({ expect, }) => { diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index d5c0987d54..833b0cd0ae 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -162,6 +162,7 @@ export { PnpmPackageManager, YarnPackageManager, BunPackageManager, + NubPackageManager, } from "./package-manager"; export { diff --git a/packages/workers-utils/src/package-manager.ts b/packages/workers-utils/src/package-manager.ts index b924b2c93f..3c58204eae 100644 --- a/packages/workers-utils/src/package-manager.ts +++ b/packages/workers-utils/src/package-manager.ts @@ -4,7 +4,7 @@ */ export interface PackageManager { /** The package manager identifier. */ - type: "npm" | "yarn" | "pnpm" | "bun"; + type: "npm" | "yarn" | "pnpm" | "bun" | "nub"; /** The command used to execute packages (e.g. `npx`, `pnpm`, `bunx`). */ npx: string; /** The command segments used to download and execute packages (e.g. `["npx"]`, `["pnpm", "dlx"]`). */ @@ -52,3 +52,13 @@ export const BunPackageManager = { dlx: ["bunx"], lockFiles: ["bun.lockb", "bun.lock"], } as const satisfies PackageManager; + +/** + * Manage packages using nub. + */ +export const NubPackageManager = { + type: "nub", + npx: "nubx", + dlx: ["nubx"], + lockFiles: ["nub.lock"], +} as const satisfies PackageManager; diff --git a/packages/workers-utils/tests/package-manager.test.ts b/packages/workers-utils/tests/package-manager.test.ts new file mode 100644 index 0000000000..f8f370402c --- /dev/null +++ b/packages/workers-utils/tests/package-manager.test.ts @@ -0,0 +1,61 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { + BunPackageManager, + NpmPackageManager, + NubPackageManager, + PnpmPackageManager, + YarnPackageManager, +} from "../src/package-manager"; +import { runInTempDir, seed } from "../src/test-helpers"; +import type { PackageManager } from "../src/package-manager"; + +const packageManagers: PackageManager[] = [ + NpmPackageManager, + PnpmPackageManager, + YarnPackageManager, + BunPackageManager, + NubPackageManager, +]; + +describe("package managers", () => { + it("describes nub", ({ expect }) => { + expect(NubPackageManager).toEqual({ + type: "nub", + npx: "nubx", + dlx: ["nubx"], + lockFiles: ["nub.lock"], + }); + }); + + describe("lock file detection", () => { + runInTempDir(); + + // Detection is lock-file-based: a project is managed by the package + // manager whose lock file is present, matching how consumers resolve it. + const findByLockFile = (dir: string) => + packageManagers.find((pm) => + pm.lockFiles.some((lockFile) => existsSync(join(dir, lockFile))) + ); + + it("detects nub from nub.lock", async ({ expect }) => { + await seed({ "nub.lock": "" }); + expect(findByLockFile(process.cwd())).toBe(NubPackageManager); + }); + + it("does not detect nub when nub.lock is absent", async ({ expect }) => { + await seed({ + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + expect(findByLockFile(process.cwd())).toBe(NpmPackageManager); + }); + + it("still detects nub from a malformed nub.lock", async ({ expect }) => { + // Detection is presence-based and never parses lock file contents, so a + // corrupt or truncated nub.lock resolves to nub just like a valid one. + await seed({ "nub.lock": "\0not-a-valid-lockfile\0" }); + expect(findByLockFile(process.cwd())).toBe(NubPackageManager); + }); + }); +}); diff --git a/packages/wrangler/src/__tests__/package-manager.test.ts b/packages/wrangler/src/__tests__/package-manager.test.ts index 1e4ebe4950..de7cde9b95 100644 --- a/packages/wrangler/src/__tests__/package-manager.test.ts +++ b/packages/wrangler/src/__tests__/package-manager.test.ts @@ -104,7 +104,7 @@ describe("getPackageManager()", () => { await expect(() => getPackageManager() ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Unable to find a package manager. Supported managers are: npm, yarn, pnpm, and bun.]` + `[Error: Unable to find a package manager. Supported managers are: npm, yarn, pnpm, bun, and nub.]` ); }); }); diff --git a/packages/wrangler/src/package-manager.ts b/packages/wrangler/src/package-manager.ts index 1c9c7955d7..1afbb187ef 100644 --- a/packages/wrangler/src/package-manager.ts +++ b/packages/wrangler/src/package-manager.ts @@ -10,6 +10,7 @@ export { PnpmPackageManager, YarnPackageManager, BunPackageManager, + NubPackageManager, } from "@cloudflare/workers-utils"; import { @@ -17,15 +18,17 @@ import { PnpmPackageManager, YarnPackageManager, BunPackageManager, + NubPackageManager, } from "@cloudflare/workers-utils"; import type { PackageManager } from "@cloudflare/workers-utils"; export async function getPackageManager(): Promise { - const [hasYarn, hasNpm, hasPnpm, hasBun] = await Promise.all([ + const [hasYarn, hasNpm, hasPnpm, hasBun, hasNub] = await Promise.all([ supportsYarn(), supportsNpm(), supportsPnpm(), supportsBun(), + supportsNub(), ]); const userAgent = sniffUserAgent(); @@ -43,6 +46,9 @@ export async function getPackageManager(): Promise { } else if (userAgent === "bun" && hasBun) { logger.debug("Using bun as package manager."); return { ...BunPackageManager }; + } else if (userAgent === "nub" && hasNub) { + logger.debug("Using nub as package manager."); + return { ...NubPackageManager }; } // lastly, check what's installed @@ -58,9 +64,12 @@ export async function getPackageManager(): Promise { } else if (hasBun) { logger.debug("Using bun as package manager."); return { ...BunPackageManager }; + } else if (hasNub) { + logger.debug("Using nub as package manager."); + return { ...NubPackageManager }; } else { throw new UserError( - "Unable to find a package manager. Supported managers are: npm, yarn, pnpm, and bun.", + "Unable to find a package manager. Supported managers are: npm, yarn, pnpm, bun, and nub.", { telemetryMessage: "package manager detection missing manager", } @@ -100,6 +109,10 @@ function supportsBun(): Promise { return supports("bun"); } +function supportsNub(): Promise { + return supports("nub"); +} + /** * The environment variable `npm_config_user_agent` can be used to * guess the package manager that was used to execute wrangler. @@ -111,7 +124,13 @@ function supportsBun(): Promise { * - [yarn](https://yarnpkg.com/advanced/lifecycle-scripts#environment-variables) * - [bun](https://github.com/oven-sh/bun/blob/550522e99b303d8172b7b16c5750d458cb056434/src/Global.zig#L205) */ -export function sniffUserAgent(): "npm" | "pnpm" | "yarn" | "bun" | undefined { +export function sniffUserAgent(): + | "npm" + | "pnpm" + | "yarn" + | "bun" + | "nub" + | undefined { const userAgent = env.npm_config_user_agent; if (userAgent === undefined) { return undefined; @@ -129,6 +148,11 @@ export function sniffUserAgent(): "npm" | "pnpm" | "yarn" | "bun" | undefined { return "bun"; } + // nub's user agent contains "npm" (e.g. `nub/0.4.5 npm/? …`), so check it before npm. + if (userAgent.includes("nub")) { + return "nub"; + } + // npm should come last as it is included in the user agent strings of other package managers if (userAgent.includes("npm")) { return "npm";