Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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