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

View File

@@ -61,7 +61,7 @@ When `NODE_ENV=production`, the Medusa application loads the environment variabl
3. Set `NODE_ENV` to `production` in the system environment variable: 3. Set `NODE_ENV` to `production` in the system environment variable:
```bash npm2yarn title=".medusa/server" ```bash title=".medusa/server"
export NODE_ENV=production export NODE_ENV=production
``` ```

View File

@@ -56,7 +56,7 @@ You can replace `npm run other-build-steps` with the appropriate command for you
If you're deploying both a Medusa application and a storefront on Cloud, Medusa will run the: If you're deploying both a Medusa application and a storefront on Cloud, Medusa will run the:
1. The `build` command defined in the backend's `package.json` file, which must run the `medusa build` command. 1. The `build` command defined in the backend's `package.json` file, which must run the `medusa build` command.
2. The build command relevant to the storefront, depending on the framework you're using. For example, if you're using Next.js for your storefront, Medusa will run the `next build` command in the storefront's directory. 2. The build command relevant to the storefront, depending on the frontend framework you're using. For example, if you're using Next.js for your storefront, Medusa will run the `next build` command in the storefront's directory.
- Medusa currently doesn't support custom build scripts for storefronts. - Medusa currently doesn't support custom build scripts for storefronts.
### What Gets Deployed in the Medusa Application? ### What Gets Deployed in the Medusa Application?

View File

@@ -49,7 +49,7 @@ Medusa provides you with the following starters that you can use to quickly set
- **DTC Starter**: Standard Medusa application with fully-fledged commerce features. - **DTC Starter**: Standard Medusa application with fully-fledged commerce features.
- **B2B Starter**: Medusa application with powerful B2B and commerce features. - **B2B Starter**: Medusa application with powerful B2B and commerce features.
Both starters come with a pre-configured Medusa server, admin dashboard, and Next.js storefront. Both starters come with a pre-configured Medusa server, admin dashboard, and the Next.js Starter Storefront.
To create a project from either of these starters: To create a project from either of these starters:

View File

@@ -4,13 +4,13 @@ export const metadata = {
# {metadata.title} # {metadata.title}
In this guide, learn about the prerequisites for your Medusa application and storefront before deploying it to Medusa Cloud in a new project. In this guide, learn about the prerequisites for your Medusa application and storefront before deploying it to Cloud in a new project.
Alternatively, you can create a project from a starter, as explained in the [Create Projects](../page.mdx) guide. Alternatively, you can create a project from a starter, as explained in the [Create Projects](../page.mdx) guide.
## Who is this guide for? ## Who is this guide for?
This guide is intended for developers and teams deploying their local Medusa applications to Medusa Cloud. This guide is intended for developers and teams deploying their local Medusa applications to Cloud.
You'll learn what setup steps are necessary for: You'll learn what setup steps are necessary for:
@@ -25,7 +25,7 @@ This section covers the prerequisites for deploying your Medusa application (ser
If you're also deploying a storefront with your backend, check the [next section](#prerequisites-for-medusa-application-with-storefront) for additional prerequisites. If you're also deploying a storefront with your backend, check the [next section](#prerequisites-for-medusa-application-with-storefront) for additional prerequisites.
### Configurations Managed by Medusa Cloud ### Configurations Managed in Cloud
Your existing Medusa application (server and admin dashboard) doesn't need specific configurations to be deployed to Cloud. Medusa automatically: Your existing Medusa application (server and admin dashboard) doesn't need specific configurations to be deployed to Cloud. Medusa automatically:

View File

@@ -29,6 +29,7 @@ export type CodeBlockMetaFields = {
title?: string title?: string
hasTabs?: boolean hasTabs?: boolean
npm2yarn?: boolean npm2yarn?: boolean
npx2yarn?: boolean
highlights?: string[][] highlights?: string[][]
apiTesting?: boolean apiTesting?: boolean
testApiMethod?: ApiMethod testApiMethod?: ApiMethod

View File

@@ -7,6 +7,7 @@ import {
import { InlineCode, InlineCodeProps } from "@/components/InlineCode" import { InlineCode, InlineCodeProps } from "@/components/InlineCode"
import { MermaidDiagram } from "@/components/MermaidDiagram" import { MermaidDiagram } from "@/components/MermaidDiagram"
import { Npm2YarnCode } from "../Npm2YarnCode" import { Npm2YarnCode } from "../Npm2YarnCode"
import { Npx2YarnCode } from "../Npx2YarnCode"
export type CodeMdxProps = { export type CodeMdxProps = {
className?: string className?: string
@@ -39,6 +40,8 @@ export const CodeMdx = ({
if (match) { if (match) {
if (rest.npm2yarn) { if (rest.npm2yarn) {
return <Npm2YarnCode npmCode={codeContent} {...rest} /> return <Npm2YarnCode npmCode={codeContent} {...rest} />
} else if (rest.npx2yarn) {
return <Npx2YarnCode npxCode={codeContent} {...rest} />
} else if (match[1] === "mermaid") { } else if (match[1] === "mermaid") {
return <MermaidDiagram diagramContent={codeContent} /> return <MermaidDiagram diagramContent={codeContent} />
} }

View File

@@ -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")
})
})

View File

@@ -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>
)
}

View File

@@ -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")
})
})
})

View File

@@ -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
}

View File

@@ -25,7 +25,8 @@
"scripts": { "scripts": {
"build": "yarn clean && tsc", "build": "yarn clean && tsc",
"clean": "rimraf dist", "clean": "rimraf dist",
"watch": "tsc --watch" "watch": "tsc --watch",
"test": "vitest --passWithNoTests"
}, },
"dependencies": { "dependencies": {
"@mdx-js/mdx": "^3.1.0", "@mdx-js/mdx": "^3.1.0",
@@ -44,7 +45,9 @@
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"tsconfig": "*", "tsconfig": "*",
"types": "*", "types": "*",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
}, },
"engines": { "engines": {
"node": ">=18.17.0" "node": ">=18.17.0"

View File

@@ -16,5 +16,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["src"] "include": ["src"],
"exclude": [
"**/__tests__"
]
} }

View File

@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"include": [
"src/**/*",
"__tests__/**/*",
"__mocks__/**/*"
],
"exclude": []
}

View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import { resolve } from 'path'
export default defineConfig({
plugins: [
tsconfigPaths({
configNames: ["tsconfig.tests.json"]
}),
react()
],
test: {
environment: 'jsdom',
setupFiles: [resolve(__dirname, '../../vitest.setup.ts')],
},
})

View File

@@ -38,4 +38,5 @@ exceptions:
- 'Frontend''s Framework' - 'Frontend''s Framework'
- 'Frontend''s framework' - 'Frontend''s framework'
- 'your framework' - 'your framework'
- 'Log in with Medusa Cloud' - 'Log in with Medusa Cloud'
- 'different framework'

View File

@@ -8238,6 +8238,8 @@ __metadata:
typescript: ^5.3.3 typescript: ^5.3.3
unified: ^11.0.4 unified: ^11.0.4
vfile-matter: ^5.0.0 vfile-matter: ^5.0.0
vite-tsconfig-paths: ^5.1.4
vitest: ^2.1.8
languageName: unknown languageName: unknown
linkType: soft linkType: soft