diff --git a/www/apps/book/app/learn/build/page.mdx b/www/apps/book/app/learn/build/page.mdx
index 387035f362..5337d40163 100644
--- a/www/apps/book/app/learn/build/page.mdx
+++ b/www/apps/book/app/learn/build/page.mdx
@@ -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:
-```bash npm2yarn title=".medusa/server"
+```bash title=".medusa/server"
export NODE_ENV=production
```
diff --git a/www/apps/cloud/app/deployments/page.mdx b/www/apps/cloud/app/deployments/page.mdx
index 7b81b1e145..91ea646654 100644
--- a/www/apps/cloud/app/deployments/page.mdx
+++ b/www/apps/cloud/app/deployments/page.mdx
@@ -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:
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.
### What Gets Deployed in the Medusa Application?
diff --git a/www/apps/cloud/app/projects/page.mdx b/www/apps/cloud/app/projects/page.mdx
index 451fee0daf..dbf0aaa348 100644
--- a/www/apps/cloud/app/projects/page.mdx
+++ b/www/apps/cloud/app/projects/page.mdx
@@ -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.
- **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:
diff --git a/www/apps/cloud/app/projects/prerequisites/page.mdx b/www/apps/cloud/app/projects/prerequisites/page.mdx
index 4608f7d581..b6c2235ef5 100644
--- a/www/apps/cloud/app/projects/prerequisites/page.mdx
+++ b/www/apps/cloud/app/projects/prerequisites/page.mdx
@@ -4,13 +4,13 @@ export const metadata = {
# {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.
## 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:
@@ -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.
-### 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:
diff --git a/www/packages/docs-ui/src/components/CodeBlock/index.tsx b/www/packages/docs-ui/src/components/CodeBlock/index.tsx
index 3c4d5112d0..4e27f20140 100644
--- a/www/packages/docs-ui/src/components/CodeBlock/index.tsx
+++ b/www/packages/docs-ui/src/components/CodeBlock/index.tsx
@@ -29,6 +29,7 @@ export type CodeBlockMetaFields = {
title?: string
hasTabs?: boolean
npm2yarn?: boolean
+ npx2yarn?: boolean
highlights?: string[][]
apiTesting?: boolean
testApiMethod?: ApiMethod
diff --git a/www/packages/docs-ui/src/components/CodeMdx/index.tsx b/www/packages/docs-ui/src/components/CodeMdx/index.tsx
index 7d96d57f73..d386f3e2eb 100644
--- a/www/packages/docs-ui/src/components/CodeMdx/index.tsx
+++ b/www/packages/docs-ui/src/components/CodeMdx/index.tsx
@@ -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
+ } else if (rest.npx2yarn) {
+ return
} else if (match[1] === "mermaid") {
return
}
diff --git a/www/packages/docs-ui/src/components/Npx2YarnCode/__tests__/index.test.tsx b/www/packages/docs-ui/src/components/Npx2YarnCode/__tests__/index.test.tsx
new file mode 100644
index 0000000000..b6a9e8aa2c
--- /dev/null
+++ b/www/packages/docs-ui/src/components/Npx2YarnCode/__tests__/index.test.tsx
@@ -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
+ }) => (
+
+ {children}
+
+ ),
+}))
+
+vi.mock("@/components/CodeTabs/Item", () => ({
+ CodeTab: ({
+ children,
+ label,
+ value,
+ }: {
+ children: React.ReactNode
+ label: string
+ value: string
+ }) => (
+
+ {children}
+
+ ),
+}))
+
+vi.mock("@/components/CodeBlock", () => ({
+ CodeBlock: ({
+ source,
+ lang,
+ title,
+ }: {
+ source: string
+ lang: string
+ title?: string
+ }) => (
+
+ {source}
+
+ ),
+}))
+
+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(
+
+ )
+ 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(
+
+ )
+ expect(container).toBeInTheDocument()
+ const codeBlock = container.querySelector("[data-testid='code-block']")
+ expect(codeBlock).toBeInTheDocument()
+ expect(codeBlock).toHaveAttribute("data-title", "Custom Title")
+ })
+})
diff --git a/www/packages/docs-ui/src/components/Npx2YarnCode/index.tsx b/www/packages/docs-ui/src/components/Npx2YarnCode/index.tsx
new file mode 100644
index 0000000000..8a1c43d9e1
--- /dev/null
+++ b/www/packages/docs-ui/src/components/Npx2YarnCode/index.tsx
@@ -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
+
+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
+
+ {tabs.map((tab, index) => (
+
+
+
+ ))}
+
+ )
+}
diff --git a/www/packages/docs-ui/src/utils/__tests__/npx-to-yarn.test.ts b/www/packages/docs-ui/src/utils/__tests__/npx-to-yarn.test.ts
new file mode 100644
index 0000000000..a7e2d1a8c2
--- /dev/null
+++ b/www/packages/docs-ui/src/utils/__tests__/npx-to-yarn.test.ts
@@ -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")
+ })
+ })
+})
diff --git a/www/packages/docs-ui/src/utils/npx-to-yarn.ts b/www/packages/docs-ui/src/utils/npx-to-yarn.ts
new file mode 100644
index 0000000000..ea4862aacb
--- /dev/null
+++ b/www/packages/docs-ui/src/utils/npx-to-yarn.ts
@@ -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
+}
diff --git a/www/packages/docs-utils/package.json b/www/packages/docs-utils/package.json
index 4a843203e5..cbcfca8bef 100644
--- a/www/packages/docs-utils/package.json
+++ b/www/packages/docs-utils/package.json
@@ -25,7 +25,8 @@
"scripts": {
"build": "yarn clean && tsc",
"clean": "rimraf dist",
- "watch": "tsc --watch"
+ "watch": "tsc --watch",
+ "test": "vitest --passWithNoTests"
},
"dependencies": {
"@mdx-js/mdx": "^3.1.0",
@@ -44,7 +45,9 @@
"rimraf": "^5.0.5",
"tsconfig": "*",
"types": "*",
- "typescript": "^5.3.3"
+ "typescript": "^5.3.3",
+ "vite-tsconfig-paths": "^5.1.4",
+ "vitest": "^2.1.8"
},
"engines": {
"node": ">=18.17.0"
diff --git a/www/packages/docs-utils/tsconfig.json b/www/packages/docs-utils/tsconfig.json
index ce3e8d1889..21960ea72b 100644
--- a/www/packages/docs-utils/tsconfig.json
+++ b/www/packages/docs-utils/tsconfig.json
@@ -16,5 +16,8 @@
"skipLibCheck": true,
"resolveJsonModule": true
},
- "include": ["src"]
+ "include": ["src"],
+ "exclude": [
+ "**/__tests__"
+ ]
}
diff --git a/www/packages/docs-utils/tsconfig.tests.json b/www/packages/docs-utils/tsconfig.tests.json
new file mode 100644
index 0000000000..c201492c62
--- /dev/null
+++ b/www/packages/docs-utils/tsconfig.tests.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "include": [
+ "src/**/*",
+ "__tests__/**/*",
+ "__mocks__/**/*"
+ ],
+ "exclude": []
+}
\ No newline at end of file
diff --git a/www/packages/docs-utils/vitest.config.mts b/www/packages/docs-utils/vitest.config.mts
new file mode 100644
index 0000000000..8c8c5f61a3
--- /dev/null
+++ b/www/packages/docs-utils/vitest.config.mts
@@ -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')],
+ },
+})
+
diff --git a/www/vale/styles/docs/Tooling.yml b/www/vale/styles/docs/Tooling.yml
index fe7f26ad79..fcec53a8c1 100644
--- a/www/vale/styles/docs/Tooling.yml
+++ b/www/vale/styles/docs/Tooling.yml
@@ -38,4 +38,5 @@ exceptions:
- 'Frontend''s Framework'
- 'Frontend''s framework'
- 'your framework'
- - 'Log in with Medusa Cloud'
\ No newline at end of file
+ - 'Log in with Medusa Cloud'
+ - 'different framework'
\ No newline at end of file
diff --git a/www/yarn.lock b/www/yarn.lock
index dfe6fd6a52..ed3788ad59 100644
--- a/www/yarn.lock
+++ b/www/yarn.lock
@@ -8238,6 +8238,8 @@ __metadata:
typescript: ^5.3.3
unified: ^11.0.4
vfile-matter: ^5.0.0
+ vite-tsconfig-paths: ^5.1.4
+ vitest: ^2.1.8
languageName: unknown
linkType: soft