From 5bb8b1a28523a80a0efdbef7a653b18113c37c15 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:29:04 -0700 Subject: [PATCH 1/8] Add nub to recognised package managers Add a NubPackageManager descriptor so projects with a nub.lock lock file are detected as nub-managed. --- .changeset/nub-package-manager.md | 7 +++ packages/workers-utils/src/index.ts | 1 + packages/workers-utils/src/package-manager.ts | 12 ++++- .../tests/package-manager.test.ts | 47 +++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .changeset/nub-package-manager.md create mode 100644 packages/workers-utils/tests/package-manager.test.ts diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md new file mode 100644 index 0000000000..bccec0bd4f --- /dev/null +++ b/.changeset/nub-package-manager.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/workers-utils": minor +--- + +Add nub to the list of recognised package managers + +nub is a package manager whose lock file is `nub.lock`. Adding a `NubPackageManager` descriptor lets projects managed by nub be detected by their lock file. diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index dccd99c3f4..e62bd93cf6 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -150,6 +150,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..dc3b6e3b03 --- /dev/null +++ b/packages/workers-utils/tests/package-manager.test.ts @@ -0,0 +1,47 @@ +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); + }); + }); +}); From bf68c91a30a6c90d284062abf06e6b1e8e6ca233 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:00:12 -0700 Subject: [PATCH 2/8] Rewrite changeset to describe user-facing impact, not implementation Per REVIEW.md, changesets target users rather than maintainers and should avoid naming internal constructs. Drops the `NubPackageManager` descriptor reference flagged in review. --- .changeset/nub-package-manager.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md index bccec0bd4f..cf73c51410 100644 --- a/.changeset/nub-package-manager.md +++ b/.changeset/nub-package-manager.md @@ -4,4 +4,4 @@ Add nub to the list of recognised package managers -nub is a package manager whose lock file is `nub.lock`. Adding a `NubPackageManager` descriptor lets projects managed by nub be detected by their lock file. +Projects using nub can now be automatically detected by their `nub.lock` file. From 1e4f3dbe1ade6402ebc82259c5d0be35d3eaa6c8 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:04:00 -0700 Subject: [PATCH 3/8] Reclassify nub-package-manager changeset as patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding one entry to the recognised-package-manager list is additive, same scale as the D1-bindings validation changeset (#14530), not a headline feature on the order of #14474 (cache options for WorkerEntrypoint exports) — patch fits workers-utils' own convention better than minor. --- .changeset/nub-package-manager.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md index cf73c51410..029815a16a 100644 --- a/.changeset/nub-package-manager.md +++ b/.changeset/nub-package-manager.md @@ -1,5 +1,5 @@ --- -"@cloudflare/workers-utils": minor +"@cloudflare/workers-utils": patch --- Add nub to the list of recognised package managers From 539f578b9624c649360e232176f92f3110898eb3 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:15:30 -0700 Subject: [PATCH 4/8] Support nub in package installation helpers Widening the shared PackageManager["type"] union to include "nub" left the install helpers in @cloudflare/cli-shared-helpers narrowed to npm/pnpm/yarn/bun, so passing packageManager.type into installWrangler and installPackages failed to type-check (TS2345 at autoconfig src/run.ts:182), breaking the workspace build and every downstream job. Teach installPackages, getWorkspaceInstallRootFlag, and installWrangler about nub, mirroring pnpm's CLI (nub is pnpm-CLI-compatible): --workspace-root at the workspace root and --no-frozen-lockfile on a full install. --- .changeset/nub-package-manager.md | 3 ++- packages/cli/packages.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md index 029815a16a..ef87e32ef5 100644 --- a/.changeset/nub-package-manager.md +++ b/.changeset/nub-package-manager.md @@ -1,7 +1,8 @@ --- "@cloudflare/workers-utils": patch +"@cloudflare/cli-shared-helpers": patch --- Add nub to the list of recognised package managers -Projects using nub can now be automatically detected by their `nub.lock` file. +Projects using nub can now be automatically detected by their `nub.lock` file, and package installation helpers now support nub alongside npm, pnpm, yarn, and bun. diff --git a/packages/cli/packages.ts b/packages/cli/packages.ts index 63d801887c..e18d533e44 100644 --- a/packages/cli/packages.ts +++ b/packages/cli/packages.ts @@ -25,7 +25,7 @@ type InstallConfig = { * @param config.force - Whether to install with `--force` or not */ export const installPackages = async ( - packageManager: "npm" | "pnpm" | "yarn" | "bun", + packageManager: "npm" | "pnpm" | "yarn" | "bun" | "nub", packages: string[], config: InstallConfig = {} ) => { @@ -49,7 +49,9 @@ export const installPackages = async ( packageManager, ...(cmd ? [cmd] : []), ...packages, - ...(packageManager === "pnpm" ? ["--no-frozen-lockfile"] : []), + ...(packageManager === "pnpm" || packageManager === "nub" + ? ["--no-frozen-lockfile"] + : []), ...(force === true ? ["--force"] : []), ...getWorkspaceInstallRootFlag(packageManager, isWorkspaceRoot), ], @@ -129,7 +131,7 @@ export const installPackages = async ( * @returns an array containing the flag(/s) to use, or an empty array if not supported or not running in the workspace root. */ function getWorkspaceInstallRootFlag( - packageManager: "npm" | "pnpm" | "yarn" | "bun", + packageManager: "npm" | "pnpm" | "yarn" | "bun" | "nub", isWorkspaceRoot: boolean ): string[] { if (!isWorkspaceRoot) { @@ -137,6 +139,7 @@ function getWorkspaceInstallRootFlag( } switch (packageManager) { + case "nub": case "pnpm": return ["--workspace-root"]; case "yarn": @@ -152,7 +155,7 @@ function getWorkspaceInstallRootFlag( * Installs the latest version of wrangler in the project directory if it isn't already. */ export async function installWrangler( - packageManager: "npm" | "pnpm" | "yarn" | "bun", + packageManager: "npm" | "pnpm" | "yarn" | "bun" | "nub", isWorkspaceRoot: boolean ) { const packages = [`wrangler@latest`] satisfies string[]; From c12edefc01cc939c330bc1c817028824b29a5639 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:18:32 -0700 Subject: [PATCH 5/8] Wire nub into detection: wrangler getPackageManager/sniffUserAgent + autoconfig nub.lock The NubPackageManager descriptor was inert. Detect nub in wrangler from its npm_config_user_agent (checked before npm, which nub's UA also contains) and an installed nub binary; detect nub projects in autoconfig via nub.lock, since @netlify/build-info doesn't recognise it. --- .changeset/nub-package-manager.md | 6 ++-- .../src/details/framework-detection.ts | 6 +++- packages/wrangler/src/package-manager.ts | 30 +++++++++++++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md index ef87e32ef5..830b47efc6 100644 --- a/.changeset/nub-package-manager.md +++ b/.changeset/nub-package-manager.md @@ -1,8 +1,10 @@ --- "@cloudflare/workers-utils": patch +"@cloudflare/autoconfig": patch "@cloudflare/cli-shared-helpers": patch +"wrangler": patch --- -Add nub to the list of recognised package managers +Recognise nub as a package manager -Projects using nub can now be automatically detected by their `nub.lock` file, and package installation helpers now support nub alongside npm, pnpm, yarn, and bun. +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. Package installation helpers use `nub`/`nubx` accordingly. diff --git a/packages/autoconfig/src/details/framework-detection.ts b/packages/autoconfig/src/details/framework-detection.ts index 3a64c2f601..cd4b1d5798 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); + // @netlify/build-info doesn't recognise nub, so prefer its nub.lock when present. + const packageManager = existsSync(join(projectPath, "nub.lock")) + ? NubPackageManager + : convertDetectedPackageManager(project.packageManager); const lockFileExists = packageManager.lockFiles.some((lockFile) => existsSync(join(projectPath, lockFile)) 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"; From 50003b0a67650ccdd7262b8e9a171ae6b630cf6a Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:11:21 -0700 Subject: [PATCH 6/8] Reconcile nub package-manager support with merged main and address review - packages/cli/packages.ts: drop the duplicate `case "nub"` left by merging main; the file now matches main's version from #14499, resolving the no-duplicate-case lint error. - autoconfig: move the nub.lock check into convertDetectedPackageManager, with a TODO to remove it once netlify/build#7124 (nub in @netlify/build-info) is merged and released. - changeset: bump the affected packages minor instead of patch. - tests: cover nub.lock absent and malformed lock file cases in workers-utils, and nub detection via detectFramework in autoconfig. --- .changeset/nub-package-manager.md | 8 ++++---- .../src/details/framework-detection.ts | 19 ++++++++++++++----- .../package-manager-detection.test.ts | 12 ++++++++++++ packages/cli/packages.ts | 1 - .../tests/package-manager.test.ts | 14 ++++++++++++++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md index 830b47efc6..b1302466f0 100644 --- a/.changeset/nub-package-manager.md +++ b/.changeset/nub-package-manager.md @@ -1,8 +1,8 @@ --- -"@cloudflare/workers-utils": patch -"@cloudflare/autoconfig": patch -"@cloudflare/cli-shared-helpers": patch -"wrangler": patch +"@cloudflare/workers-utils": minor +"@cloudflare/autoconfig": minor +"@cloudflare/cli-shared-helpers": minor +"wrangler": minor --- Recognise nub as a package manager diff --git a/packages/autoconfig/src/details/framework-detection.ts b/packages/autoconfig/src/details/framework-detection.ts index cd4b1d5798..9835f23a9c 100644 --- a/packages/autoconfig/src/details/framework-detection.ts +++ b/packages/autoconfig/src/details/framework-detection.ts @@ -83,10 +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. - // @netlify/build-info doesn't recognise nub, so prefer its nub.lock when present. - const packageManager = existsSync(join(projectPath, "nub.lock")) - ? NubPackageManager - : convertDetectedPackageManager(project.packageManager); + const packageManager = convertDetectedPackageManager( + project.packageManager, + projectPath + ); const lockFileExists = packageManager.lockFiles.some((lockFile) => existsSync(join(projectPath, lockFile)) @@ -149,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/cli/packages.ts b/packages/cli/packages.ts index 9b0ecc56f4..99b94542e2 100644 --- a/packages/cli/packages.ts +++ b/packages/cli/packages.ts @@ -141,7 +141,6 @@ function getWorkspaceInstallRootFlag( } switch (packageManager) { - case "nub": case "pnpm": case "nub": return ["--workspace-root"]; diff --git a/packages/workers-utils/tests/package-manager.test.ts b/packages/workers-utils/tests/package-manager.test.ts index dc3b6e3b03..f8f370402c 100644 --- a/packages/workers-utils/tests/package-manager.test.ts +++ b/packages/workers-utils/tests/package-manager.test.ts @@ -43,5 +43,19 @@ describe("package managers", () => { 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); + }); }); }); From 6984396fe88605afd665846e8470992bb08d6872 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:14:15 -0700 Subject: [PATCH 7/8] Drop stale cli-shared-helpers changeset entry (that file now matches main) --- .changeset/nub-package-manager.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/nub-package-manager.md b/.changeset/nub-package-manager.md index b1302466f0..3f7fee21c2 100644 --- a/.changeset/nub-package-manager.md +++ b/.changeset/nub-package-manager.md @@ -1,10 +1,9 @@ --- "@cloudflare/workers-utils": minor "@cloudflare/autoconfig": minor -"@cloudflare/cli-shared-helpers": 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. Package installation helpers use `nub`/`nubx` accordingly. +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. From e6a7442adaf788d667fff1f6cb49d0bded68670c Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Mon, 20 Jul 2026 14:57:34 +0100 Subject: [PATCH 8/8] fix: outdated package manager snapshots --- packages/wrangler/src/__tests__/package-manager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.]` ); }); });