mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
fix(cli): suggest previous minor on errors (#10559)
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
getPackageManager,
|
||||
getPackageManagerFromUserAgent,
|
||||
getPackageRunnerCommand,
|
||||
} from "@/src/utils/get-package-manager"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs-extra"
|
||||
@@ -20,6 +25,8 @@ vi.mock("execa")
|
||||
vi.mock("prompts")
|
||||
vi.mock("@/src/utils/get-package-manager", () => ({
|
||||
getPackageManager: vi.fn().mockResolvedValue("npm"),
|
||||
getPackageManagerFromUserAgent: vi.fn(() => "npm"),
|
||||
getPackageRunnerCommand: vi.fn(() => "npx"),
|
||||
}))
|
||||
vi.mock("@/src/utils/spinner")
|
||||
vi.mock("@/src/utils/logger", () => ({
|
||||
@@ -35,6 +42,9 @@ describe("createProject", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(getPackageManager).mockResolvedValue("npm")
|
||||
vi.mocked(getPackageManagerFromUserAgent).mockReturnValue("npm")
|
||||
vi.mocked(getPackageRunnerCommand).mockReturnValue("npx")
|
||||
|
||||
// Reset all fs mocks
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { detect } from "@antfu/ni"
|
||||
|
||||
export type PackageManager = "yarn" | "pnpm" | "bun" | "npm" | "deno"
|
||||
|
||||
export async function getPackageManager(
|
||||
targetDir: string,
|
||||
{ withFallback }: { withFallback?: boolean } = {
|
||||
withFallback: false,
|
||||
}
|
||||
): Promise<"yarn" | "pnpm" | "bun" | "npm" | "deno"> {
|
||||
): Promise<PackageManager> {
|
||||
const packageManager = await detect({ programmatic: true, cwd: targetDir })
|
||||
|
||||
if (packageManager === "yarn@berry") return "yarn"
|
||||
@@ -17,8 +19,12 @@ export async function getPackageManager(
|
||||
}
|
||||
|
||||
// Fallback to user agent if not detected.
|
||||
const userAgent = process.env.npm_config_user_agent || ""
|
||||
return getPackageManagerFromUserAgent() ?? "npm"
|
||||
}
|
||||
|
||||
export function getPackageManagerFromUserAgent(
|
||||
userAgent = process.env.npm_config_user_agent || ""
|
||||
): PackageManager | null {
|
||||
if (userAgent.startsWith("yarn")) {
|
||||
return "yarn"
|
||||
}
|
||||
@@ -31,15 +37,27 @@ export async function getPackageManager(
|
||||
return "bun"
|
||||
}
|
||||
|
||||
return "npm"
|
||||
if (userAgent.startsWith("deno")) {
|
||||
return "deno"
|
||||
}
|
||||
|
||||
if (userAgent.startsWith("npm")) {
|
||||
return "npm"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getPackageRunner(cwd: string) {
|
||||
const packageManager = await getPackageManager(cwd)
|
||||
|
||||
export function getPackageRunnerCommand(packageManager: PackageManager | null) {
|
||||
if (packageManager === "pnpm") return "pnpm dlx"
|
||||
|
||||
if (packageManager === "bun") return "bunx"
|
||||
|
||||
return "npx"
|
||||
}
|
||||
|
||||
export async function getPackageRunner(cwd: string) {
|
||||
const packageManager = await getPackageManager(cwd)
|
||||
|
||||
return getPackageRunnerCommand(packageManager)
|
||||
}
|
||||
|
||||
100
packages/shadcn/src/utils/handle-error.test.ts
Normal file
100
packages/shadcn/src/utils/handle-error.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { RegistryNotFoundError } from "@/src/registry/errors"
|
||||
import {
|
||||
getPreviousMinorCommand,
|
||||
getPreviousMinorVersion,
|
||||
handleError,
|
||||
} from "@/src/utils/handle-error"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest"
|
||||
|
||||
vi.mock("@/src/utils/highlighter", () => ({
|
||||
highlighter: {
|
||||
error: (value: string) => value,
|
||||
info: (value: string) => value,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/src/utils/logger", () => ({
|
||||
logger: {
|
||||
break: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("handleError", () => {
|
||||
const originalArgv = process.argv
|
||||
const originalUserAgent = process.env.npm_config_user_agent
|
||||
const exit = vi.spyOn(process, "exit").mockImplementation((code) => {
|
||||
throw new Error(`process.exit:${code}`)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
process.argv = ["node", "shadcn", "add", "foo"]
|
||||
process.env.npm_config_user_agent = "npm/10.0.0 node/v22"
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv
|
||||
if (originalUserAgent) {
|
||||
process.env.npm_config_user_agent = originalUserAgent
|
||||
} else {
|
||||
delete process.env.npm_config_user_agent
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
exit.mockRestore()
|
||||
})
|
||||
|
||||
it("gets the previous minor version", () => {
|
||||
expect(getPreviousMinorVersion("4.6.0")).toBe("4.5.0")
|
||||
expect(getPreviousMinorVersion("4.6.3")).toBe("4.5.0")
|
||||
expect(getPreviousMinorVersion("4.0.0")).toBeNull()
|
||||
expect(getPreviousMinorVersion("latest")).toBeNull()
|
||||
})
|
||||
|
||||
it("builds a previous minor command from the failed arguments", () => {
|
||||
expect(getPreviousMinorCommand("4.6.0", ["add", "foo"])).toBe(
|
||||
"npx shadcn@4.5.0 add foo"
|
||||
)
|
||||
})
|
||||
|
||||
it("uses the package runner from the user agent", () => {
|
||||
process.env.npm_config_user_agent = "pnpm/10.0.0 npm/? node/v22"
|
||||
|
||||
expect(getPreviousMinorCommand("4.6.0", ["add", "foo"])).toBe(
|
||||
"pnpm dlx shadcn@4.5.0 add foo"
|
||||
)
|
||||
})
|
||||
|
||||
it("quotes arguments that need shell escaping", () => {
|
||||
expect(
|
||||
getPreviousMinorCommand("4.6.0", ["add", "hello world", "it's-working"])
|
||||
).toBe("npx shadcn@4.5.0 add 'hello world' 'it'\\''s-working'")
|
||||
})
|
||||
|
||||
it("prints the previous minor command before exiting", () => {
|
||||
expect(() => {
|
||||
handleError(
|
||||
new RegistryNotFoundError(
|
||||
"http://localhost:4000/r/styles/new-york-v4/foo.json"
|
||||
)
|
||||
)
|
||||
}).toThrow("process.exit:1")
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"You can also try a previous version to see if that works:"
|
||||
)
|
||||
expect(logger.error).toHaveBeenCalledWith("npx shadcn@4.5.0 add foo")
|
||||
expect(exit).toHaveBeenCalledWith(1)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,14 @@
|
||||
import { RegistryError } from "@/src/registry/errors"
|
||||
import {
|
||||
getPackageManagerFromUserAgent,
|
||||
getPackageRunnerCommand,
|
||||
} from "@/src/utils/get-package-manager"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { z } from "zod"
|
||||
|
||||
import packageJson from "../../package.json"
|
||||
|
||||
export function handleError(error: unknown) {
|
||||
logger.break()
|
||||
logger.error(
|
||||
@@ -12,8 +18,7 @@ export function handleError(error: unknown) {
|
||||
logger.error("")
|
||||
if (typeof error === "string") {
|
||||
logger.error(error)
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
exitWithPreviousVersionSuggestion()
|
||||
}
|
||||
|
||||
if (error instanceof RegistryError) {
|
||||
@@ -31,8 +36,7 @@ export function handleError(error: unknown) {
|
||||
logger.error("\nSuggestion:")
|
||||
logger.error(error.suggestion)
|
||||
}
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
exitWithPreviousVersionSuggestion()
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
@@ -40,16 +44,68 @@ export function handleError(error: unknown) {
|
||||
for (const [key, value] of Object.entries(error.flatten().fieldErrors)) {
|
||||
logger.error(`- ${highlighter.info(key)}: ${value}`)
|
||||
}
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
exitWithPreviousVersionSuggestion()
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message)
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
exitWithPreviousVersionSuggestion()
|
||||
}
|
||||
|
||||
exitWithPreviousVersionSuggestion()
|
||||
}
|
||||
|
||||
export function getPreviousMinorVersion(version: string) {
|
||||
const match = version.match(/^(\d+)\.(\d+)\.\d+/)
|
||||
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
const major = Number.parseInt(match[1], 10)
|
||||
const minor = Number.parseInt(match[2], 10)
|
||||
|
||||
if (minor === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${major}.${minor - 1}.0`
|
||||
}
|
||||
|
||||
export function getPreviousMinorCommand(
|
||||
version = packageJson.version,
|
||||
args = process.argv.slice(2)
|
||||
) {
|
||||
const previousMinorVersion = getPreviousMinorVersion(version)
|
||||
|
||||
if (!previousMinorVersion) {
|
||||
return null
|
||||
}
|
||||
|
||||
const runner = getPackageRunnerCommand(getPackageManagerFromUserAgent())
|
||||
|
||||
return [...runner.split(" "), `shadcn@${previousMinorVersion}`, ...args]
|
||||
.map(quoteShellArg)
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
function exitWithPreviousVersionSuggestion() {
|
||||
const command = getPreviousMinorCommand()
|
||||
|
||||
if (command) {
|
||||
logger.error("")
|
||||
logger.error("You can also try a previous version to see if that works:")
|
||||
logger.error(command)
|
||||
}
|
||||
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function quoteShellArg(value: string) {
|
||||
if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
return `'${value.replace(/'/g, "'\\''")}'`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import path from "path"
|
||||
import { expect, test } from "vitest"
|
||||
|
||||
import { getPackageManager } from "../../src/utils/get-package-manager"
|
||||
import {
|
||||
getPackageManager,
|
||||
getPackageManagerFromUserAgent,
|
||||
getPackageRunnerCommand,
|
||||
} from "../../src/utils/get-package-manager"
|
||||
|
||||
test("get package manager", async () => {
|
||||
expect(
|
||||
@@ -30,3 +34,21 @@ test("get package manager", async () => {
|
||||
await getPackageManager(path.resolve(__dirname, "../fixtures/next"))
|
||||
).toBe("pnpm")
|
||||
})
|
||||
|
||||
test("get package manager from user agent", () => {
|
||||
expect(getPackageManagerFromUserAgent("pnpm/10.0.0 npm/? node/v22")).toBe(
|
||||
"pnpm"
|
||||
)
|
||||
expect(getPackageManagerFromUserAgent("bun/1.2.0 npm/? node/v22")).toBe(
|
||||
"bun"
|
||||
)
|
||||
expect(getPackageManagerFromUserAgent("npm/10.0.0 node/v22")).toBe("npm")
|
||||
expect(getPackageManagerFromUserAgent("")).toBeNull()
|
||||
})
|
||||
|
||||
test("get package runner command", () => {
|
||||
expect(getPackageRunnerCommand("pnpm")).toBe("pnpm dlx")
|
||||
expect(getPackageRunnerCommand("bun")).toBe("bunx")
|
||||
expect(getPackageRunnerCommand("npm")).toBe("npx")
|
||||
expect(getPackageRunnerCommand(null)).toBe("npx")
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user