docs: add npx2yarn component (#14512)

* initial

* initial

* update tests

* remove unused import

* allow passing with no tests

* vale fixes
This commit is contained in:
Shahed Nasser
2026-01-12 13:42:30 +02:00
committed by GitHub
parent 5f90cd0650
commit 43951ce60e
16 changed files with 330 additions and 10 deletions
@@ -29,6 +29,7 @@ export type CodeBlockMetaFields = {
title?: string
hasTabs?: boolean
npm2yarn?: boolean
npx2yarn?: boolean
highlights?: string[][]
apiTesting?: boolean
testApiMethod?: ApiMethod
@@ -7,6 +7,7 @@ import {
import { InlineCode, InlineCodeProps } from "@/components/InlineCode"
import { MermaidDiagram } from "@/components/MermaidDiagram"
import { Npm2YarnCode } from "../Npm2YarnCode"
import { Npx2YarnCode } from "../Npx2YarnCode"
export type CodeMdxProps = {
className?: string
@@ -39,6 +40,8 @@ export const CodeMdx = ({
if (match) {
if (rest.npm2yarn) {
return <Npm2YarnCode npmCode={codeContent} {...rest} />
} else if (rest.npx2yarn) {
return <Npx2YarnCode npxCode={codeContent} {...rest} />
} else if (match[1] === "mermaid") {
return <MermaidDiagram diagramContent={codeContent} />
}
@@ -0,0 +1,112 @@
import React from "react"
import { describe, expect, test, vi } from "vitest"
import { render } from "@testing-library/react"
// mock functions
const npxToYarnMock = vi.fn((code: string, packageManager: "yarn" | "pnpm") => code)
// mock components
vi.mock("@/components/CodeTabs", () => ({
CodeTabs: ({
children,
group,
}: {
children: React.ReactNode
group: string
}) => (
<div data-testid="code-tabs" data-group={group}>
{children}
</div>
),
}))
vi.mock("@/components/CodeTabs/Item", () => ({
CodeTab: ({
children,
label,
value,
}: {
children: React.ReactNode
label: string
value: string
}) => (
<div data-testid="code-tab" data-label={label} data-value={value}>
{children}
</div>
),
}))
vi.mock("@/components/CodeBlock", () => ({
CodeBlock: ({
source,
lang,
title,
}: {
source: string
lang: string
title?: string
}) => (
<div data-testid="code-block" data-lang={lang} data-title={title}>
{source}
</div>
),
}))
vi.mock("@/utils/npx-to-yarn", () => ({
npxToYarn: (code: string, packageManager: "yarn" | "pnpm") =>
npxToYarnMock(code, packageManager),
}))
import { Npx2YarnCode } from "../index"
describe("render", () => {
test("renders npm2yarn code", () => {
const { container } = render(
<Npx2YarnCode npxCode="npx medusa db:migrate" />
)
expect(npxToYarnMock).toHaveBeenCalledTimes(2)
expect(container).toBeInTheDocument()
const codeTabs = container.querySelector("[data-testid='code-tabs']")
expect(codeTabs).toBeInTheDocument()
expect(codeTabs).toHaveAttribute("data-group", "npm2yarn")
const codeTabsChildren = codeTabs?.querySelectorAll(
"[data-testid='code-tab']"
)
expect(codeTabsChildren).toHaveLength(3)
expect(codeTabsChildren![0]).toHaveAttribute("data-label", "npx")
expect(codeTabsChildren![0]).toHaveAttribute("data-value", "npm")
const npxCodeBlock = codeTabsChildren![0].querySelector(
"[data-testid='code-block']"
)
expect(npxCodeBlock).toBeInTheDocument()
expect(npxCodeBlock).toHaveAttribute("data-lang", "bash")
expect(npxCodeBlock).toHaveTextContent("npx medusa db:migrate")
expect(codeTabsChildren![1]).toHaveAttribute("data-label", "yarn")
expect(codeTabsChildren![1]).toHaveAttribute("data-value", "yarn")
const yarnCodeBlock = codeTabsChildren![1].querySelector(
"[data-testid='code-block']"
)
expect(yarnCodeBlock).toBeInTheDocument()
expect(yarnCodeBlock).toHaveAttribute("data-lang", "bash")
expect(codeTabsChildren![2]).toHaveAttribute("data-label", "pnpm")
expect(codeTabsChildren![2]).toHaveAttribute("data-value", "pnpm")
const pnpmCodeBlock = codeTabsChildren![2].querySelector(
"[data-testid='code-block']"
)
expect(pnpmCodeBlock).toBeInTheDocument()
expect(pnpmCodeBlock).toHaveAttribute("data-lang", "bash")
})
test("renders npm2yarn code with custom code options", () => {
const { container } = render(
<Npx2YarnCode
npxCode="npx medusa db:migrate"
title="Custom Title"
/>
)
expect(container).toBeInTheDocument()
const codeBlock = container.querySelector("[data-testid='code-block']")
expect(codeBlock).toBeInTheDocument()
expect(codeBlock).toHaveAttribute("data-title", "Custom Title")
})
})
@@ -0,0 +1,63 @@
import React from "react"
import { CodeBlock, CodeBlockMetaFields } from "@/components/CodeBlock"
import { CodeTabs } from "@/components/CodeTabs"
import { CodeTab } from "@/components/CodeTabs/Item"
import { npxToYarn } from "@/utils/npx-to-yarn"
type Npx2YarnCodeProps = {
npxCode: string
} & Omit<CodeBlockMetaFields, "npx2yarn">
export const Npx2YarnCode = ({
npxCode,
...codeOptions
}: Npx2YarnCodeProps) => {
// convert npx code
const yarnCode = npxToYarn(npxCode, "yarn")
const pnpmCode = npxToYarn(npxCode, "pnpm")
const lang = "bash"
codeOptions.hasTabs = true
const tabs = [
{
label: "npx",
// keep it npm so it matches the tab name in Npm2YarnCode
value: "npm",
code: {
source: npxCode,
lang,
...codeOptions,
},
},
{
label: "yarn",
value: "yarn",
code: {
source: yarnCode,
lang,
...codeOptions,
},
},
{
label: "pnpm",
value: "pnpm",
code: {
source: pnpmCode,
lang,
...codeOptions,
},
},
]
return (
// Keep the group name same as Npm2YarnCode, value selection will be synced across both components
<CodeTabs group="npm2yarn">
{tabs.map((tab, index) => (
<CodeTab label={tab.label} value={tab.value} key={index}>
<CodeBlock {...tab.code} />
</CodeTab>
))}
</CodeTabs>
)
}
@@ -0,0 +1,70 @@
import { describe, it, expect } from "vitest"
import { npxToYarn } from "../npx-to-yarn.js"
describe("npxToYarn", () => {
describe("yarn conversion", () => {
it("should convert basic npx command to yarn", () => {
const result = npxToYarn("npx medusa db:migrate", "yarn")
expect(result).toBe("yarn medusa db:migrate")
})
it("should convert npx command with multiple arguments", () => {
const result = npxToYarn("npx medusa develop --port 9000", "yarn")
expect(result).toBe("yarn medusa develop --port 9000")
})
it("should convert npx command with flags", () => {
const result = npxToYarn("npx medusa user --email admin@test.com", "yarn")
expect(result).toBe("yarn medusa user --email admin@test.com")
})
it("should handle npx command with leading/trailing whitespace", () => {
const result = npxToYarn(" npx medusa db:migrate ", "yarn")
expect(result).toBe("yarn medusa db:migrate")
})
})
describe("pnpm conversion", () => {
it("should convert basic npx command to pnpm", () => {
const result = npxToYarn("npx medusa db:migrate", "pnpm")
expect(result).toBe("pnpm medusa db:migrate")
})
it("should convert npx command with multiple arguments", () => {
const result = npxToYarn("npx medusa develop --port 9000", "pnpm")
expect(result).toBe("pnpm medusa develop --port 9000")
})
it("should convert npx command with flags", () => {
const result = npxToYarn("npx medusa user --email admin@test.com", "pnpm")
expect(result).toBe("pnpm medusa user --email admin@test.com")
})
it("should handle npx command with leading/trailing whitespace", () => {
const result = npxToYarn(" npx medusa db:migrate ", "pnpm")
expect(result).toBe("pnpm medusa db:migrate")
})
})
describe("edge cases", () => {
it("should return original command if it does not start with npx", () => {
const result = npxToYarn("npm install medusa", "yarn")
expect(result).toBe("npm install medusa")
})
it("should handle command with only npx and package name", () => {
const result = npxToYarn("npx medusa", "yarn")
expect(result).toBe("yarn medusa")
})
it("should preserve command structure with special characters", () => {
const result = npxToYarn("npx medusa db:seed --file=./data.json", "pnpm")
expect(result).toBe("pnpm medusa db:seed --file=./data.json")
})
it("should handle command with path separators", () => {
const result = npxToYarn("npx @medusajs/medusa-cli develop", "yarn")
expect(result).toBe("yarn @medusajs/medusa-cli develop")
})
})
})
@@ -0,0 +1,34 @@
/**
* Converts an npx command to its yarn or pnpm equivalent
* Assumes the package is installed locally in node_modules
* @param npxCommand - The npx command to convert (e.g., "npx medusa db:migrate")
* @param packageManager - The target package manager ("yarn" or "pnpm")
* @returns The converted command
*
* @example
* npxToYarn("npx medusa db:migrate", "yarn") // "yarn medusa db:migrate"
* npxToYarn("npx medusa db:migrate", "pnpm") // "pnpm medusa db:migrate"
*/
export function npxToYarn(
npxCommand: string,
packageManager: "yarn" | "pnpm"
): string {
// Remove leading/trailing whitespace
const trimmed = npxCommand.trim()
// Check if command starts with npx
if (!trimmed.startsWith("npx ")) {
return trimmed
}
// Remove "npx " prefix and replace with the target package manager
const command = trimmed.slice(4)
if (packageManager === "yarn") {
return `yarn ${command}`
} else if (packageManager === "pnpm") {
return `pnpm ${command}`
}
return trimmed
}