Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/nub-package-manager.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 15 additions & 2 deletions packages/autoconfig/src/details/framework-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BunPackageManager,
FatalError,
NpmPackageManager,
NubPackageManager,
PnpmPackageManager,
UserError,
YarnPackageManager,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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;
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

if (!pkgManager) {
return NpmPackageManager;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BunPackageManager,
NpmPackageManager,
NubPackageManager,
PnpmPackageManager,
YarnPackageManager,
} from "@cloudflare/workers-utils";
Expand Down Expand Up @@ -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,
}) => {
Expand Down
1 change: 1 addition & 0 deletions packages/workers-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export {
PnpmPackageManager,
YarnPackageManager,
BunPackageManager,
NubPackageManager,
} from "./package-manager";

export {
Expand Down
12 changes: 11 additions & 1 deletion packages/workers-utils/src/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]`). */
Expand Down Expand Up @@ -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;
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
61 changes: 61 additions & 0 deletions packages/workers-utils/tests/package-manager.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Comment thread
NuroDev marked this conversation as resolved.
});
2 changes: 1 addition & 1 deletion packages/wrangler/src/__tests__/package-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.]`
);
});
});
Expand Down
30 changes: 27 additions & 3 deletions packages/wrangler/src/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ export {
PnpmPackageManager,
YarnPackageManager,
BunPackageManager,
NubPackageManager,
} from "@cloudflare/workers-utils";

import {
NpmPackageManager,
PnpmPackageManager,
YarnPackageManager,
BunPackageManager,
NubPackageManager,
} from "@cloudflare/workers-utils";
import type { PackageManager } from "@cloudflare/workers-utils";

export async function getPackageManager(): Promise<PackageManager> {
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();
Expand All @@ -43,6 +46,9 @@ export async function getPackageManager(): Promise<PackageManager> {
} 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
Expand All @@ -58,9 +64,12 @@ export async function getPackageManager(): Promise<PackageManager> {
} 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.",
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
{
telemetryMessage: "package manager detection missing manager",
}
Expand Down Expand Up @@ -100,6 +109,10 @@ function supportsBun(): Promise<boolean> {
return supports("bun");
}

function supportsNub(): Promise<boolean> {
return supports("nub");
}

/**
* The environment variable `npm_config_user_agent` can be used to
* guess the package manager that was used to execute wrangler.
Expand All @@ -111,7 +124,13 @@ function supportsBun(): Promise<boolean> {
* - [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;
Expand All @@ -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";
Expand Down
Loading