From 5f90cd0650eb11bfbd27cc326ca8c261fc588c55 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 12 Jan 2026 12:55:26 +0200 Subject: [PATCH] feat(create-medusa-app): add support for pnpm and specifying package manager (#14443) * feat(create-medusa-app): add support for pnpm and specifying package manager * fixes * add medusa command method * add tests for package manager * fix duplicate messages * throw if chosen package manager is not available * better package manager and version detector * add debug message * fix version detection * fix for yarn v3 * fix migration command * yarn v3 fix * remove .yarn directory for non-yarn package managers * run npm ci to validate npm installation * fixes * fixes * remove corepack line * remove if condition --- .changeset/slimy-gifts-wear.md | 5 + .../cli/create-medusa-app/jest.config.cjs | 14 + packages/cli/create-medusa-app/package.json | 5 +- packages/cli/create-medusa-app/src/index.ts | 20 +- .../utils/__tests__/package-manager.test.ts | 637 ++++++++++++++++++ .../create-medusa-app/src/utils/clone-repo.ts | 41 +- .../create-medusa-app/src/utils/execute.ts | 31 +- .../src/utils/nextjs-utils.ts | 26 +- .../src/utils/package-manager.ts | 313 ++++++++- .../src/utils/prepare-project.ts | 102 +-- .../src/utils/process-manager.ts | 6 +- .../src/utils/project-creator/creator.ts | 10 +- .../project-creator/medusa-plugin-creator.ts | 3 +- .../project-creator/medusa-project-creator.ts | 48 +- .../src/utils/start-medusa.ts | 11 +- packages/cli/create-medusa-app/tsconfig.json | 7 + 16 files changed, 1130 insertions(+), 149 deletions(-) create mode 100644 .changeset/slimy-gifts-wear.md create mode 100644 packages/cli/create-medusa-app/jest.config.cjs create mode 100644 packages/cli/create-medusa-app/src/utils/__tests__/package-manager.test.ts diff --git a/.changeset/slimy-gifts-wear.md b/.changeset/slimy-gifts-wear.md new file mode 100644 index 0000000000..6c869f6620 --- /dev/null +++ b/.changeset/slimy-gifts-wear.md @@ -0,0 +1,5 @@ +--- +"create-medusa-app": patch +--- + +feat(create-medusa-app): add support for pnpm and specifying package manager diff --git a/packages/cli/create-medusa-app/jest.config.cjs b/packages/cli/create-medusa-app/jest.config.cjs new file mode 100644 index 0000000000..ba9c0b3692 --- /dev/null +++ b/packages/cli/create-medusa-app/jest.config.cjs @@ -0,0 +1,14 @@ +const defineJestConfig = require("../../../define_jest_config") + +module.exports = defineJestConfig({ + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + extensionsToTreatAsEsm: [".ts"], + testPathIgnorePatterns: [ + "dist/", + "node_modules/", + "__fixtures__/", + "__mocks__/", + ], +}) diff --git a/packages/cli/create-medusa-app/package.json b/packages/cli/create-medusa-app/package.json index 04309510d1..d839f310af 100644 --- a/packages/cli/create-medusa-app/package.json +++ b/packages/cli/create-medusa-app/package.json @@ -9,8 +9,9 @@ "scripts": { "dev": "node --loader ts-node/esm src/index.ts", "start": "node dist/index.js", - "build": "tsc", - "watch": "yarn run -T tsc --watch" + "build": "yarn run -T tsc", + "watch": "yarn run -T tsc --watch", + "test": "../../../node_modules/.bin/jest --passWithNoTests src" }, "dependencies": { "@medusajs/deps": "2.12.5", diff --git a/packages/cli/create-medusa-app/src/index.ts b/packages/cli/create-medusa-app/src/index.ts index bee8292c43..50228a04d5 100644 --- a/packages/cli/create-medusa-app/src/index.ts +++ b/packages/cli/create-medusa-app/src/index.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { program } from "commander" +import { Option, program } from "commander" import create from "./commands/create.js" program @@ -42,6 +42,24 @@ program "Show all logs of underlying commands. Useful for debugging.", false ) + .addOption( + new Option("--use-npm", "Use npm as the package manager").conflicts([ + "usePnpm", + "useYarn", + ]) + ) + .addOption( + new Option("--use-yarn", "Use yarn as the package manager").conflicts([ + "useNpm", + "usePnpm", + ]) + ) + .addOption( + new Option("--use-pnpm", "Use pnpm as the package manager").conflicts([ + "useNpm", + "useYarn", + ]) + ) .parse() void create(program.args, program.opts()) diff --git a/packages/cli/create-medusa-app/src/utils/__tests__/package-manager.test.ts b/packages/cli/create-medusa-app/src/utils/__tests__/package-manager.test.ts new file mode 100644 index 0000000000..bffb006e0c --- /dev/null +++ b/packages/cli/create-medusa-app/src/utils/__tests__/package-manager.test.ts @@ -0,0 +1,637 @@ +import PackageManager from "../package-manager" +import ProcessManager from "../process-manager" +import execute from "../execute" +import { existsSync, rmSync } from "fs" +import logMessage from "../log-message" + +// Mock dependencies +jest.mock("../execute") +jest.mock("fs") +jest.mock("../log-message") + +const mockExecute = execute as jest.MockedFunction +const mockExistsSync = existsSync as jest.MockedFunction +const mockRmSync = rmSync as jest.MockedFunction +const mockLogMessage = logMessage as jest.MockedFunction + +describe("PackageManager", () => { + let processManager: ProcessManager + let originalEnv: NodeJS.ProcessEnv + + beforeEach(() => { + processManager = new ProcessManager() + originalEnv = { ...process.env } + jest.clearAllMocks() + }) + + afterEach(() => { + process.env = originalEnv + }) + + describe("constructor", () => { + it("should initialize with default options", () => { + const pm = new PackageManager(processManager) + expect(pm.getPackageManager()).toBeUndefined() + }) + + it("should set npm as chosen package manager when useNpm is true", () => { + const pm = new PackageManager(processManager, { useNpm: true }) + expect(pm["chosenPackageManager"]).toBe("npm") + }) + + it("should set yarn as chosen package manager when useYarn is true", () => { + const pm = new PackageManager(processManager, { useYarn: true }) + expect(pm["chosenPackageManager"]).toBe("yarn") + }) + + it("should set pnpm as chosen package manager when usePnpm is true", () => { + const pm = new PackageManager(processManager, { usePnpm: true }) + expect(pm["chosenPackageManager"]).toBe("pnpm") + }) + + it("should respect verbose option", () => { + const pm = new PackageManager(processManager, { verbose: true }) + expect(pm["verbose"]).toBe(true) + }) + + it("should prioritize npm over other options", () => { + const pm = new PackageManager(processManager, { + useNpm: true, + useYarn: true, + usePnpm: true, + }) + expect(pm["chosenPackageManager"]).toBe("npm") + }) + }) + + describe("detectFromUserAgent", () => { + it("should detect pnpm from user agent with version", () => { + process.env.npm_config_user_agent = "pnpm/8.0.0" + const pm = new PackageManager(processManager) + const result = pm["detectFromUserAgent"]() + expect(result.manager).toBe("pnpm") + expect(result.version).toBe("8.0.0") + }) + + it("should detect pnpm from pnpx with version", () => { + process.env.npm_config_user_agent = "pnpx/8.0.0" + const pm = new PackageManager(processManager) + const result = pm["detectFromUserAgent"]() + expect(result.manager).toBe("pnpm") + expect(result.version).toBe("8.0.0") + }) + + it("should detect yarn from user agent with version", () => { + process.env.npm_config_user_agent = "yarn/1.22.0" + const pm = new PackageManager(processManager) + const result = pm["detectFromUserAgent"]() + expect(result.manager).toBe("yarn") + expect(result.version).toBe("1.22.0") + }) + + it("should default to npm for unknown user agent", () => { + process.env.npm_config_user_agent = "some-unknown-manager/1.0.0" + const pm = new PackageManager(processManager) + const result = pm["detectFromUserAgent"]() + expect(result.manager).toBe("npm") + expect(result.version).toBeUndefined() + }) + + it("should default to npm when user agent is undefined", () => { + delete process.env.npm_config_user_agent + const pm = new PackageManager(processManager) + const result = pm["detectFromUserAgent"]() + expect(result.manager).toBe("npm") + expect(result.version).toBeUndefined() + }) + }) + + describe("setPackageManager", () => { + it("should not run if package manager is already set", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + + await pm.setPackageManager({}) + + expect(mockExecute).not.toHaveBeenCalled() + }) + + it("should set chosen package manager if available", async () => { + mockExecute.mockResolvedValue({ stdout: "8.0.0", stderr: "" }) + const pm = new PackageManager(processManager, { usePnpm: true }) + + await pm.setPackageManager({}) + + expect(pm.getPackageManager()).toBe("pnpm") + expect(await pm.getPackageManagerString()).toBe("pnpm@8.0.0") + expect(mockExecute).toHaveBeenCalledWith( + ["pnpm -v", {}], + { verbose: false } + ) + }) + + it("should throw error when chosen package manager is not available", async () => { + mockExecute.mockRejectedValueOnce(new Error("Command not found")) + + const pm = new PackageManager(processManager, { usePnpm: true }) + + await pm.setPackageManager({}) + + expect(mockLogMessage).toHaveBeenCalledWith({ + type: "error", + message: expect.stringContaining('"pnpm" is not available'), + }) + }) + + it("should detect from user agent when no package manager chosen", async () => { + process.env.npm_config_user_agent = "yarn/1.22.0" + + const pm = new PackageManager(processManager) + await pm.setPackageManager({}) + + expect(pm.getPackageManager()).toBe("yarn") + expect(await pm.getPackageManagerString()).toBe("yarn@1.22.0") + // Detection message should not be logged in non-verbose mode + expect(mockLogMessage).not.toHaveBeenCalledWith({ + type: "info", + message: expect.stringContaining('Using detected package manager "yarn"'), + }) + // Should not call getVersion since version is in user agent + expect(mockExecute).not.toHaveBeenCalled() + }) + + it("should use detected package manager even without version in user agent", async () => { + mockExecute.mockResolvedValue({ stdout: "1.22.0", stderr: "" }) + process.env.npm_config_user_agent = "yarn" + + const pm = new PackageManager(processManager) + await pm.setPackageManager({}) + + expect(pm.getPackageManager()).toBe("yarn") + expect(await pm.getPackageManagerString()).toBe("yarn@1.22.0") + // Should call getVersion since version is not in user agent + expect(mockExecute).toHaveBeenCalledWith(["yarn -v", {}], { + verbose: false, + }) + // Fallback message should not be logged in non-verbose mode + expect(mockLogMessage).not.toHaveBeenCalledWith({ + type: "info", + message: expect.stringContaining("Falling back to"), + }) + }) + + it("should log detection messages in verbose mode", async () => { + process.env.npm_config_user_agent = "yarn/1.22.0" + + const pm = new PackageManager(processManager, { verbose: true }) + await pm.setPackageManager({}) + + expect(pm.getPackageManager()).toBe("yarn") + expect(mockLogMessage).toHaveBeenCalledWith({ + type: "info", + message: expect.stringContaining('Using detected package manager "yarn"'), + }) + // Should not call getVersion since version is in user agent + expect(mockExecute).not.toHaveBeenCalled() + }) + + it("should log fallback messages in verbose mode when no version in user agent", async () => { + mockExecute.mockResolvedValue({ stdout: "1.22.0", stderr: "" }) + process.env.npm_config_user_agent = "yarn" + + const pm = new PackageManager(processManager, { verbose: true }) + await pm.setPackageManager({}) + + expect(pm.getPackageManager()).toBe("yarn") + expect(mockLogMessage).toHaveBeenCalledWith({ + type: "info", + message: expect.stringContaining("Falling back to yarn"), + }) + // Should call getVersion to get the version + expect(mockExecute).toHaveBeenCalledWith(["yarn -v", {}], { + verbose: false, + }) + }) + }) + + describe("removeLockFiles", () => { + it("should not remove files if package manager is not set", async () => { + const pm = new PackageManager(processManager) + + await pm.removeLockFiles("/test/path") + + expect(mockExistsSync).not.toHaveBeenCalled() + expect(mockRmSync).not.toHaveBeenCalled() + }) + + it("should remove yarn.lock, pnpm-lock.yaml, and .yarn when using npm", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + mockExistsSync.mockReturnValue(true) + + await pm.removeLockFiles("/test/path") + + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/yarn.lock") + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/pnpm-lock.yaml") + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/.yarn") + expect(mockRmSync).toHaveBeenCalledWith("/test/path/yarn.lock", { + force: true, + recursive: true, + }) + expect(mockRmSync).toHaveBeenCalledWith("/test/path/pnpm-lock.yaml", { + force: true, + recursive: true, + }) + expect(mockRmSync).toHaveBeenCalledWith("/test/path/.yarn", { + force: true, + recursive: true, + }) + }) + + it("should remove package-lock.json and pnpm-lock.yaml when using yarn", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + mockExistsSync.mockReturnValue(true) + + await pm.removeLockFiles("/test/path") + + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/package-lock.json") + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/pnpm-lock.yaml") + expect(mockRmSync).toHaveBeenCalledWith("/test/path/package-lock.json", { + force: true, + recursive: true, + }) + expect(mockRmSync).toHaveBeenCalledWith("/test/path/pnpm-lock.yaml", { + force: true, + recursive: true, + }) + }) + + it("should remove yarn.lock, package-lock.json, and .yarn when using pnpm", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "pnpm" + mockExistsSync.mockReturnValue(true) + + await pm.removeLockFiles("/test/path") + + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/yarn.lock") + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/package-lock.json") + expect(mockExistsSync).toHaveBeenCalledWith("/test/path/.yarn") + expect(mockRmSync).toHaveBeenCalledWith("/test/path/yarn.lock", { + force: true, + recursive: true, + }) + expect(mockRmSync).toHaveBeenCalledWith("/test/path/package-lock.json", { + force: true, + recursive: true, + }) + expect(mockRmSync).toHaveBeenCalledWith("/test/path/.yarn", { + force: true, + recursive: true, + }) + }) + + it("should not remove files that don't exist", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + mockExistsSync.mockReturnValue(false) + + await pm.removeLockFiles("/test/path") + + expect(mockExistsSync).toHaveBeenCalled() + expect(mockRmSync).not.toHaveBeenCalled() + }) + }) + + describe("installDependencies", () => { + it("should set package manager before installing if not set", async () => { + process.env.npm_config_user_agent = "yarn/1.22.0" + + const pm = new PackageManager(processManager) + await pm.installDependencies({ cwd: "/test/path" }) + + expect(pm.getPackageManager()).toBe("yarn") + }) + + it("should remove lock files before installing", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + mockExistsSync.mockReturnValue(true) + + const pm = new PackageManager(processManager, { useNpm: true }) + pm["packageManager"] = "npm" + + await pm.installDependencies({ cwd: "/test/path" }) + + expect(mockRmSync).toHaveBeenCalled() + }) + + it("should execute yarn command when using yarn", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + + await pm.installDependencies({ cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["yarn", { cwd: "/test/path" }], + { verbose: false } + ) + }) + + it("should execute pnpm install when using pnpm", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "pnpm" + + await pm.installDependencies({ cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["pnpm install", { cwd: "/test/path" }], + { verbose: false } + ) + }) + + it("should execute npm install then npm ci when using npm", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + + await pm.installDependencies({ cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["npm install", { cwd: "/test/path" }], + { verbose: false } + ) + expect(mockExecute).toHaveBeenCalledWith( + ["npm ci", { cwd: "/test/path" }], + { verbose: false } + ) + expect(mockExecute).toHaveBeenCalledTimes(2) + }) + + it("should re-run npm install if npm ci fails", async () => { + mockExecute + .mockResolvedValueOnce({ stdout: "", stderr: "" }) // npm install succeeds + .mockRejectedValueOnce(new Error("npm ci failed")) // npm ci fails + .mockResolvedValueOnce({ stdout: "", stderr: "" }) // npm install retry succeeds + + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + + await pm.installDependencies({ cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["npm install", { cwd: "/test/path" }], + { verbose: false } + ) + expect(mockExecute).toHaveBeenCalledWith( + ["npm ci", { cwd: "/test/path" }], + { verbose: false } + ) + // Second npm install call + expect(mockExecute).toHaveBeenCalledTimes(3) + }) + + it("should respect verbose option", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager, { verbose: true }) + pm["packageManager"] = "yarn" + + await pm.installDependencies({ cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + expect.anything(), + { verbose: true } + ) + }) + }) + + describe("getCommandStr", () => { + it("should throw error if package manager is not set", () => { + const pm = new PackageManager(processManager) + + expect(() => pm.getCommandStr("dev")).toThrow("Package manager not set") + }) + + it("should return yarn command format", () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + + expect(pm.getCommandStr("dev")).toBe("yarn dev") + }) + + it("should return pnpm command format", () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "pnpm" + + expect(pm.getCommandStr("build")).toBe("pnpm build") + }) + + it("should return npm command format with 'run'", () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + + expect(pm.getCommandStr("test")).toBe("npm run test") + }) + }) + + describe("runCommand", () => { + it("should set package manager before running if not set", async () => { + process.env.npm_config_user_agent = "yarn/1.22.0" + + const pm = new PackageManager(processManager) + await pm.runCommand("dev", { cwd: "/test/path" }) + + expect(pm.getPackageManager()).toBe("yarn") + }) + + it("should execute command with correct format for yarn", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + + await pm.runCommand("dev", { cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["yarn dev", { cwd: "/test/path" }], + { verbose: false } + ) + }) + + it("should execute command with correct format for npm", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + + await pm.runCommand("build", { cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["npm run build", { cwd: "/test/path" }], + { verbose: false } + ) + }) + + it("should pass through verbose options", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager, { verbose: true }) + pm["packageManager"] = "yarn" + + await pm.runCommand("dev", { cwd: "/test/path" }, { needOutput: true }) + + expect(mockExecute).toHaveBeenCalledWith( + expect.anything(), + { verbose: true, needOutput: true } + ) + }) + + it("should return execution result", async () => { + const result = { stdout: "success", stderr: "" } + mockExecute.mockResolvedValue(result) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + + const output = await pm.runCommand("dev", {}) + + expect(output).toEqual(result) + }) + }) + + describe("runMedusaCommand", () => { + it("should set package manager before running if not set", async () => { + process.env.npm_config_user_agent = "yarn/1.22.0" + + const pm = new PackageManager(processManager) + await pm.runMedusaCommand("migrate", { cwd: "/test/path" }) + + expect(pm.getPackageManager()).toBe("yarn") + }) + + it("should execute yarn medusa command for yarn", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + + await pm.runMedusaCommand("migrate", { cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["yarn medusa migrate", { cwd: "/test/path" }], + { verbose: false } + ) + }) + + it("should execute pnpm medusa command for pnpm", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "pnpm" + + await pm.runMedusaCommand("seed", { cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["pnpm medusa seed", { cwd: "/test/path" }], + { verbose: false } + ) + }) + + it("should execute npx medusa command for npm", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + + await pm.runMedusaCommand("start", { cwd: "/test/path" }) + + expect(mockExecute).toHaveBeenCalledWith( + ["npx medusa start", { cwd: "/test/path" }], + { verbose: false } + ) + }) + + it("should pass through verbose options", async () => { + mockExecute.mockResolvedValue({ stdout: "", stderr: "" }) + + const pm = new PackageManager(processManager, { verbose: true }) + pm["packageManager"] = "yarn" + + await pm.runMedusaCommand("migrate", { cwd: "/test/path" }, { needOutput: true }) + + expect(mockExecute).toHaveBeenCalledWith( + expect.anything(), + { verbose: true, needOutput: true } + ) + }) + + it("should return execution result", async () => { + const result = { stdout: "migration complete", stderr: "" } + mockExecute.mockResolvedValue(result) + + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + + const output = await pm.runMedusaCommand("migrate", {}) + + expect(output).toEqual(result) + }) + }) + + describe("getPackageManager", () => { + it("should return undefined when not set", () => { + const pm = new PackageManager(processManager) + expect(pm.getPackageManager()).toBeUndefined() + }) + + it("should return the current package manager", () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + expect(pm.getPackageManager()).toBe("yarn") + }) + }) + + describe("getPackageManagerString", () => { + it("should return undefined when version is not set", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + expect(await pm.getPackageManagerString()).toBeUndefined() + }) + + it("should return packageManager@version format", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "yarn" + pm["packageManagerVersion"] = "4.9.0" + expect(await pm.getPackageManagerString()).toBe("yarn@4.9.0") + }) + + it("should work with pnpm", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "pnpm" + pm["packageManagerVersion"] = "8.15.0" + expect(await pm.getPackageManagerString()).toBe("pnpm@8.15.0") + }) + + it("should work with npm", async () => { + const pm = new PackageManager(processManager) + pm["packageManager"] = "npm" + pm["packageManagerVersion"] = "10.0.0" + expect(await pm.getPackageManagerString()).toBe("npm@10.0.0") + }) + + it("should call setPackageManager if package manager is not set", async () => { + process.env.npm_config_user_agent = "yarn/1.22.0" + + const pm = new PackageManager(processManager) + const result = await pm.getPackageManagerString() + + expect(pm.getPackageManager()).toBe("yarn") + expect(result).toBe("yarn@1.22.0") + }) + }) +}) diff --git a/packages/cli/create-medusa-app/src/utils/clone-repo.ts b/packages/cli/create-medusa-app/src/utils/clone-repo.ts index 9177f8e13b..a217fe5046 100644 --- a/packages/cli/create-medusa-app/src/utils/clone-repo.ts +++ b/packages/cli/create-medusa-app/src/utils/clone-repo.ts @@ -4,6 +4,7 @@ import path from "path" import { isAbortError } from "./create-abort-controller.js" import execute from "./execute.js" import logMessage from "./log-message.js" +import { execFileSync } from "child_process" type CloneRepoOptions = { directoryName?: string @@ -80,13 +81,37 @@ export async function runCloneRepo({ } function deleteGitDirectory(projectDirectory: string) { - fs.rmSync(path.join(projectDirectory, ".git"), { - recursive: true, - force: true, - }) + try { + fs.rmSync(path.join(projectDirectory, ".git"), { + recursive: true, + force: true, + }) + } catch (error) { + deleteWithCommand(projectDirectory, ".git") + } - fs.rmSync(path.join(projectDirectory, ".github"), { - recursive: true, - force: true, - }) + try { + fs.rmSync(path.join(projectDirectory, ".github"), { + recursive: true, + force: true, + }) + } catch (error) { + deleteWithCommand(projectDirectory, ".github") + } +} + +/** + * Useful for deleting directories when fs methods fail (e.g., with Yarn v3) + */ +function deleteWithCommand(projectDirectory: string, dirName: string) { + const dirPath = path.normalize(path.join(projectDirectory, dirName)) + if (!fs.existsSync(dirPath)) { + return + } + + if (process.platform === "win32") { + execFileSync("cmd", ["/c", "rmdir", "/s", "/q", dirPath]) + } else { + execFileSync("rm", ["-rf", dirPath]) + } } diff --git a/packages/cli/create-medusa-app/src/utils/execute.ts b/packages/cli/create-medusa-app/src/utils/execute.ts index 02df8c5e8c..835d21fc51 100644 --- a/packages/cli/create-medusa-app/src/utils/execute.ts +++ b/packages/cli/create-medusa-app/src/utils/execute.ts @@ -4,12 +4,12 @@ import { getAbortError } from "./create-abort-controller.js" const promiseExec = util.promisify(exec) -type ExecuteOptions = { +export type ExecuteResult = { stdout?: string stderr?: string } -type VerboseOptions = { +export type VerboseOptions = { verbose?: boolean // Since spawn doesn't allow us to both retrieve the // output and output it live without using events, @@ -26,21 +26,27 @@ type SpawnParams = [string, SpawnSyncOptions] const execute = async ( command: SpawnParams | PromiseExecParams, { verbose = false, needOutput = false }: VerboseOptions -): Promise => { +): Promise => { if (verbose) { const [commandStr, options] = command as SpawnParams const childProcess = spawnSync(commandStr, { ...options, shell: true, - stdio: needOutput ? - "pipe" : - [process.stdin, process.stdout, process.stderr], + stdio: needOutput + ? "pipe" + : [process.stdin, process.stdout, process.stderr], + env: { + ...process.env, + ...(options.env || {}), + }, }) if (childProcess.error || childProcess.status !== 0) { - throw childProcess.error || - childProcess.stderr?.toString() || + throw ( + childProcess.error || + childProcess.stderr?.toString() || `${commandStr} failed with status ${childProcess.status}` + ) } if ( @@ -61,7 +67,14 @@ const execute = async ( stderr: childProcess.stderr?.toString() || "", } } else { - const childProcess = await promiseExec(...(command as PromiseExecParams)) + const [commandStr, options] = command as PromiseExecParams + const childProcess = await promiseExec(commandStr, { + ...options, + env: { + ...process.env, + ...(options?.env || {}), + }, + }) return { stdout: childProcess.stdout as string, diff --git a/packages/cli/create-medusa-app/src/utils/nextjs-utils.ts b/packages/cli/create-medusa-app/src/utils/nextjs-utils.ts index 8691fa86f5..0edb32f880 100644 --- a/packages/cli/create-medusa-app/src/utils/nextjs-utils.ts +++ b/packages/cli/create-medusa-app/src/utils/nextjs-utils.ts @@ -7,8 +7,8 @@ import { isAbortError } from "./create-abort-controller.js" import execute from "./execute.js" import { displayFactBox, FactBoxOptions } from "./facts.js" import logMessage from "./log-message.js" -import ProcessManager from "./process-manager.js" import { updatePackageVersions } from "./update-package-versions.js" +import PackageManager from "./package-manager.js" const NEXTJS_REPO = "https://github.com/medusajs/nextjs-starter-medusa" const NEXTJS_BRANCH = "main" @@ -31,7 +31,7 @@ type InstallOptions = { abortController?: AbortController factBoxOptions: FactBoxOptions verbose?: boolean - processManager: ProcessManager + packageManager: PackageManager version?: string } @@ -40,7 +40,7 @@ export async function installNextjsStarter({ abortController, factBoxOptions, verbose = false, - processManager, + packageManager, version, }: InstallOptions): Promise { factBoxOptions.interval = displayFactBox({ @@ -83,20 +83,7 @@ export async function installNextjsStarter({ signal: abortController?.signal, cwd: nextjsDirectory, } - await processManager.runProcess({ - process: async () => { - try { - await execute([`yarn`, execOptions], { verbose }) - } catch (e) { - // yarn isn't available - // use npm - await execute([`npm install`, execOptions], { - verbose, - }) - } - }, - ignoreERESOLVE: true, - }) + await packageManager.installDependencies(execOptions) } catch (e) { if (isAbortError(e)) { process.exit() @@ -130,14 +117,17 @@ type StartOptions = { directory: string abortController?: AbortController verbose?: boolean + packageManager: PackageManager } export function startNextjsStarter({ directory, abortController, verbose = false, + packageManager, }: StartOptions) { - const childProcess = exec(`npm run dev`, { + const command = packageManager.getCommandStr(`dev`) + const childProcess = exec(command, { cwd: directory, signal: abortController?.signal, }) diff --git a/packages/cli/create-medusa-app/src/utils/package-manager.ts b/packages/cli/create-medusa-app/src/utils/package-manager.ts index 5f51fa264e..8b8072b1c4 100644 --- a/packages/cli/create-medusa-app/src/utils/package-manager.ts +++ b/packages/cli/create-medusa-app/src/utils/package-manager.ts @@ -1,14 +1,113 @@ -import execute from "./execute.js" +import path from "path" +import execute, { VerboseOptions } from "./execute.js" +import logMessage from "./log-message.js" import ProcessManager from "./process-manager.js" +import { existsSync, rmSync } from "fs" + +export type PackageManagerType = "npm" | "yarn" | "pnpm" + +type PackageManagerOptions = { + verbose?: boolean + useNpm?: boolean + usePnpm?: boolean + useYarn?: boolean +} export default class PackageManager { - protected packageManager?: "npm" | "yarn" + protected packageManager?: PackageManagerType + protected packageManagerVersion?: string protected processManager: ProcessManager protected verbose + protected chosenPackageManager?: PackageManagerType - constructor(processManager: ProcessManager, verbose = false) { + constructor( + processManager: ProcessManager, + options: PackageManagerOptions = {} + ) { this.processManager = processManager - this.verbose = verbose + this.verbose = options.verbose || false + + if (options.useNpm) { + this.chosenPackageManager = "npm" + } else if (options.usePnpm) { + this.chosenPackageManager = "pnpm" + } else if (options.useYarn) { + this.chosenPackageManager = "yarn" + } + } + + private detectFromUserAgent(): { + manager: PackageManagerType + version?: string + } { + const userAgent = process.env.npm_config_user_agent + + if (!userAgent) { + return { manager: "npm" } + } + + // Extract package manager and version (e.g., "yarn/4.9.0" -> ["yarn", "4.9.0"]) + const match = userAgent.match(/(pnpm|pnpx|yarn|npm)\/(\d+\.\d+\.\d+)/) + if (match) { + const [, manager, version] = match + + if (this.verbose) { + logMessage({ + type: "info", + message: `Detected from user agent: ${manager}@${version}`, + }) + } + + // pnpx is an alias for pnpm + if (manager === "pnpx") { + return { manager: "pnpm", version } + } + + return { manager: manager as PackageManagerType, version } + } + + // Fallback detection without version + if (userAgent.includes("pnpm") || userAgent.includes("pnpx")) { + return { manager: "pnpm" } + } + if (userAgent.includes("yarn")) { + return { manager: "yarn" } + } + + return { manager: "npm" } + } + + private async getVersion( + pm: PackageManagerType, + execOptions: Record + ): Promise { + const commands: Record = { + yarn: "yarn -v", + pnpm: "pnpm -v", + npm: "npm -v", + } + + try { + const result = await execute([commands[pm], execOptions], { + verbose: false, + }) + const version = result.stdout?.trim() + if (this.verbose) { + logMessage({ + type: "info", + message: `Detected ${pm} version: ${version}`, + }) + } + return version + } catch { + if (this.verbose) { + logMessage({ + type: "info", + message: `Failed to get version for package manager: ${pm}`, + }) + } + return undefined + } } async setPackageManager(execOptions: Record): Promise { @@ -16,38 +115,127 @@ export default class PackageManager { return } - // check whether yarn is available + // check whether package manager is available and get version await this.processManager.runProcess({ process: async () => { - try { - await execute([`yarn -v`, execOptions], { verbose: this.verbose }) - // yarn is available - this.packageManager = "yarn" - } catch (e) { - // yarn isn't available - // use npm - this.packageManager = "npm" + if (this.chosenPackageManager) { + const version = await this.getVersion( + this.chosenPackageManager, + execOptions + ) + + if (version) { + this.packageManager = this.chosenPackageManager + // Store version if we don't have it from user agent + if (!this.packageManagerVersion) { + this.packageManagerVersion = version + } + return + } + + // Error logs exit the process, so command execution will stop here + logMessage({ + type: "error", + message: `The specified package manager "${this.chosenPackageManager}" is not available. Please install it or choose another package manager.`, + }) + } + + const detectedResult = this.detectFromUserAgent() + // fallback to npm if detection fails + this.packageManager = detectedResult.manager || "npm" + this.packageManagerVersion = detectedResult.version + if (!this.packageManagerVersion) { + // get version for the detected package manager (or npm fallback) + this.packageManagerVersion = await this.getVersion( + this.packageManager, + execOptions + ) + + if (this.verbose) { + logMessage({ + type: "info", + message: `Falling back to ${this.packageManager} as the package manager.`, + }) + } + } else { + if (this.verbose) { + logMessage({ + type: "info", + message: `Using detected package manager "${this.packageManager}".`, + }) + } } }, ignoreERESOLVE: true, }) } - async installDependencies( - execOptions: Record, - ) { + async removeLockFiles(directory: string): Promise { + const lockFiles: Record = { + npm: ["yarn.lock", "pnpm-lock.yaml", ".yarn"], + yarn: ["package-lock.json", "pnpm-lock.yaml"], + pnpm: ["yarn.lock", "package-lock.json", ".yarn"], + } + + if (!this.packageManager) { + return + } + + const filesToRemove = lockFiles[this.packageManager] || [] + for (const file of filesToRemove) { + const filePath = path.join(directory, file) + if (existsSync(filePath)) { + rmSync(filePath, { + force: true, + recursive: true, + }) + } + } + } + + async installDependencies(execOptions: Record) { if (!this.packageManager) { await this.setPackageManager(execOptions) } - const command = this.packageManager === "yarn" ? - `yarn` : `npm install` + // Remove lock files from other package managers + if (execOptions.cwd && typeof execOptions.cwd === "string") { + await this.removeLockFiles(execOptions.cwd) + } + + const commands: Record = { + yarn: "yarn", + pnpm: "pnpm install", + npm: "npm install", + } + + const command = commands[this.packageManager || "npm"] await this.processManager.runProcess({ process: async () => { await execute([command, execOptions], { - verbose: this.verbose + verbose: this.verbose, }) + + // For npm, run npm ci after npm install to validate installation + if (this.packageManager === "npm") { + try { + await execute(["npm ci", execOptions], { + verbose: this.verbose, + }) + } catch (error) { + // If npm ci fails, re-run npm install + if (this.verbose) { + logMessage({ + type: "info", + message: "npm ci validation failed, re-running npm install...", + }) + } + await execute(["npm install", execOptions], { + verbose: this.verbose, + }) + } + } }, ignoreERESOLVE: true, }) @@ -56,6 +244,7 @@ export default class PackageManager { async runCommand( command: string, execOptions: Record, + verboseOptions: VerboseOptions = {} ) { if (!this.packageManager) { await this.setPackageManager(execOptions) @@ -63,25 +252,83 @@ export default class PackageManager { const commandStr = this.getCommandStr(command) - await this.processManager.runProcess({ - process: async () => { - await execute([commandStr, execOptions], { - verbose: this.verbose - }) - }, - ignoreERESOLVE: true, - }) + return await this.processManager.runProcess({ + process: async () => { + return await execute([commandStr, execOptions], { + verbose: this.verbose, + ...verboseOptions, + }) + }, + ignoreERESOLVE: true, + }) } - getCommandStr( + async runMedusaCommand( command: string, - ): string { + execOptions: Record, + verboseOptions: VerboseOptions = {} + ) { + if (!this.packageManager) { + await this.setPackageManager(execOptions) + } + + const formats: Record = { + yarn: `yarn medusa ${command}`, + pnpm: `pnpm medusa ${command}`, + npm: `npx medusa ${command}`, + } + + const commandStr = formats[this.packageManager || "npm"] + + return await this.processManager.runProcess({ + process: async () => { + return await execute([commandStr, execOptions], { + verbose: this.verbose, + ...verboseOptions, + }) + }, + ignoreERESOLVE: true, + }) + } + + getCommandStr(command: string): string { if (!this.packageManager) { throw new Error("Package manager not set") } - return this.packageManager === "yarn" - ? `yarn ${command}` - : `npm run ${command}` + const formats: Record = { + yarn: `yarn ${command}`, + pnpm: `pnpm ${command}`, + npm: `npm run ${command}`, + } + + return formats[this.packageManager] } -} \ No newline at end of file + + getPackageManager(): PackageManagerType | undefined { + return this.packageManager + } + + async getPackageManagerString(): Promise { + if (!this.packageManager) { + await this.setPackageManager({}) + } + if (!this.packageManagerVersion) { + if (this.verbose) { + logMessage({ + type: "info", + message: `No version detected for package manager: ${this.packageManager}`, + }) + } + return undefined + } + const result = `${this.packageManager}@${this.packageManagerVersion}` + if (this.verbose) { + logMessage({ + type: "info", + message: `Package manager string: ${result}`, + }) + } + return result + } +} diff --git a/packages/cli/create-medusa-app/src/utils/prepare-project.ts b/packages/cli/create-medusa-app/src/utils/prepare-project.ts index 1b95b78b25..ecbf8d916d 100644 --- a/packages/cli/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/cli/create-medusa-app/src/utils/prepare-project.ts @@ -1,7 +1,7 @@ import fs from "fs" import path from "path" import { Ora } from "ora" -import execute from "./execute.js" +import { ExecuteResult } from "./execute.js" import { EOL } from "os" import { displayFactBox, FactBoxOptions } from "./facts.js" import ProcessManager from "./process-manager.js" @@ -95,6 +95,12 @@ async function preparePlugin({ // Update name packageJson.name = projectName + // Add packageManager field to ensure consistent version usage + const packageManagerString = await packageManager.getPackageManagerString() + if (packageManagerString) { + packageJson.packageManager = packageManagerString + } + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)) factBoxOptions.interval = displayFactBox({ @@ -162,6 +168,12 @@ async function prepareProject({ // Update name packageJson.name = projectName + // Add packageManager field to ensure consistent version usage + const packageManagerString = await packageManager.getPackageManagerString() + if (packageManagerString) { + packageJson.packageManager = packageManagerString + } + // Update medusa dependencies versions if (version) { updatePackageVersions(packageJson, version) @@ -210,59 +222,59 @@ async function prepareProject({ }) // run migrations - await processManager.runProcess({ - process: async () => { - const proc = await execute(["npx medusa db:migrate", npxOptions], { - verbose, - needOutput: true, - }) + const migrationExecResult = await packageManager.runMedusaCommand( + "db:migrate", + npxOptions, + { + verbose, + needOutput: true, + } + ) - if (client) { - // check the migrations table is in the database - // to ensure that migrations ran - let errorOccurred = false - try { - const migrations = await client.query( - `SELECT * FROM "mikro_orm_migrations"` - ) - errorOccurred = migrations.rowCount == 0 - } catch (e) { - // avoid error thrown if the migrations table - // doesn't exist - errorOccurred = true - } + if (client) { + // check the migrations table is in the database + // to ensure that migrations ran + let errorOccurred = false + try { + const migrations = await client.query( + `SELECT count(tablename) from pg_tables WHERE tablename = 'mikro_orm_migrations'` + ) + errorOccurred = migrations.rowCount == 0 + } catch (e) { + // avoid error thrown if the migrations table + // doesn't exist + errorOccurred = true + } - // ensure that migrations actually ran in case of an uncaught error - if (errorOccurred && (proc.stderr || proc.stdout)) { - throw new Error( - `An error occurred while running migrations: ${ - proc.stderr || proc.stdout - }` - ) - } - } - }, - }) + // ensure that migrations actually ran in case of an uncaught error + if ( + errorOccurred && + (migrationExecResult.stderr || migrationExecResult.stdout) + ) { + throw new Error( + `An error occurred while running migrations: ${ + migrationExecResult.stderr || migrationExecResult.stdout + }` + ) + } + } factBoxOptions.interval = displayFactBox({ ...factBoxOptions, message: "Ran Migrations", }) - await processManager.runProcess({ - process: async () => { - const proc = await execute( - [`npx medusa user -e ${ADMIN_EMAIL} --invite`, npxOptions], - { verbose, needOutput: true } - ) + const userExecResult = (await packageManager.runMedusaCommand( + `user -e ${ADMIN_EMAIL} --invite`, + npxOptions, + { verbose, needOutput: true } + )) as ExecuteResult - // get invite token from stdout - const match = (proc.stdout as string).match( - /Invite token: (?.+)/ - ) - inviteToken = match?.groups?.token - }, - }) + // get invite token from stdout + const match = (userExecResult.stdout as string).match( + /Invite token: (?.+)/ + ) + inviteToken = match?.groups?.token // TODO for now we just seed the default data // we should add onboarding seeding again if it makes diff --git a/packages/cli/create-medusa-app/src/utils/process-manager.ts b/packages/cli/create-medusa-app/src/utils/process-manager.ts index b530cf47a8..33ced20595 100644 --- a/packages/cli/create-medusa-app/src/utils/process-manager.ts +++ b/packages/cli/create-medusa-app/src/utils/process-manager.ts @@ -16,8 +16,8 @@ export default class ProcessManager { } onTerminated(fn: () => Promise | void) { - process.on("SIGTERM", () => fn()) - process.on("SIGINT", () => fn()) + process.on("SIGTERM", async () => fn()) + process.on("SIGINT", async () => fn()) } addInterval(interval: NodeJS.Timeout) { @@ -34,7 +34,7 @@ export default class ProcessManager { do { ++retries try { - await process() + return await process() } catch (error) { if ( typeof error === "object" && diff --git a/packages/cli/create-medusa-app/src/utils/project-creator/creator.ts b/packages/cli/create-medusa-app/src/utils/project-creator/creator.ts index de40d12fe6..54d57ab4df 100644 --- a/packages/cli/create-medusa-app/src/utils/project-creator/creator.ts +++ b/packages/cli/create-medusa-app/src/utils/project-creator/creator.ts @@ -17,6 +17,9 @@ export interface ProjectOptions { verbose?: boolean plugin?: boolean version?: string + useNpm?: boolean + usePnpm?: boolean + useYarn?: boolean } export interface ProjectCreator { @@ -42,7 +45,12 @@ export abstract class BaseProjectCreator { ) { this.spinner = ora() this.processManager = new ProcessManager() - this.packageManager = new PackageManager(this.processManager) + this.packageManager = new PackageManager(this.processManager, { + verbose: options.verbose, + useNpm: options.useNpm, + usePnpm: options.usePnpm, + useYarn: options.useYarn, + }) this.abortController = createAbortController(this.processManager) this.projectName = projectName const basePath = diff --git a/packages/cli/create-medusa-app/src/utils/project-creator/medusa-plugin-creator.ts b/packages/cli/create-medusa-app/src/utils/project-creator/medusa-plugin-creator.ts index 8ae96d49fb..d4f6998efb 100644 --- a/packages/cli/create-medusa-app/src/utils/project-creator/medusa-plugin-creator.ts +++ b/packages/cli/create-medusa-app/src/utils/project-creator/medusa-plugin-creator.ts @@ -3,7 +3,6 @@ import boxen from "boxen" import chalk from "chalk" import { emojify } from "node-emoji" import { EOL } from "os" -import slugifyType from "slugify" import { runCloneRepo } from "../clone-repo.js" import { isAbortError } from "../create-abort-controller.js" import { displayFactBox } from "../facts.js" @@ -92,7 +91,7 @@ export class PluginProjectCreator logMessage({ message: boxen( chalk.green( - `Change to the \`${this.projectName}\` directory to explore your Medusa plugin.${EOL}${EOL}Check out the Medusa plugin documentation to start your development:${EOL}${EOL}https://docs.medusajs.com/${EOL}${EOL}Star us on GitHub if you like what we're building:${EOL}${EOL}https://github.com/medusajs/medusa/stargazers` + `Change to the \`${this.projectName}\` directory to explore your Medusa plugin.${EOL}${EOL}Check out the Medusa plugin documentation to start your development:${EOL}${EOL}https://docs.medusajs.com/learn/fundamentals/plugins${EOL}${EOL}Star us on GitHub if you like what we're building:${EOL}${EOL}https://github.com/medusajs/medusa/stargazers` ), { titleAlignment: "center", diff --git a/packages/cli/create-medusa-app/src/utils/project-creator/medusa-project-creator.ts b/packages/cli/create-medusa-app/src/utils/project-creator/medusa-project-creator.ts index f1571cdfb8..337b47fb08 100644 --- a/packages/cli/create-medusa-app/src/utils/project-creator/medusa-project-creator.ts +++ b/packages/cli/create-medusa-app/src/utils/project-creator/medusa-project-creator.ts @@ -75,32 +75,28 @@ export class MedusaProjectCreator title: "Setting up project...", }) - try { - await runCloneRepo({ - projectName: this.projectPath, - repoUrl: this.options.repoUrl ?? "", + await runCloneRepo({ + projectName: this.projectPath, + repoUrl: this.options.repoUrl ?? "", + abortController: this.abortController, + spinner: this.spinner, + verbose: this.options.verbose, + }) + + this.factBoxOptions.interval = displayFactBox({ + ...this.factBoxOptions, + message: "Created project directory", + }) + + if (installNextjs) { + this.nextjsDirectory = await installNextjsStarter({ + directoryName: this.projectPath, abortController: this.abortController, - spinner: this.spinner, + factBoxOptions: this.factBoxOptions, verbose: this.options.verbose, + packageManager: this.packageManager, + version: this.options.version, }) - - this.factBoxOptions.interval = displayFactBox({ - ...this.factBoxOptions, - message: "Created project directory", - }) - - if (installNextjs) { - this.nextjsDirectory = await installNextjsStarter({ - directoryName: this.projectPath, - abortController: this.abortController, - factBoxOptions: this.factBoxOptions, - verbose: this.options.verbose, - processManager: this.processManager, - version: this.options.version, - }) - } - } catch (e) { - throw e } } @@ -177,6 +173,7 @@ export class MedusaProjectCreator startMedusa({ directory: this.projectPath, abortController: this.abortController, + packageManager: this.packageManager, }) if (this.nextjsDirectory) { @@ -184,6 +181,7 @@ export class MedusaProjectCreator directory: this.nextjsDirectory, abortController: this.abortController, verbose: this.options.verbose, + packageManager: this.packageManager, }) } @@ -195,13 +193,13 @@ export class MedusaProjectCreator private async openBrowser(): Promise { await waitOn({ resources: ["http://localhost:9000/health"], - }).then(async () => { + }).then(async () => open( this.inviteToken ? `http://localhost:9000/app/invite?token=${this.inviteToken}&first_run=true` : "http://localhost:9000/app" ) - }) + ) } private handleError(e: Error): void { diff --git a/packages/cli/create-medusa-app/src/utils/start-medusa.ts b/packages/cli/create-medusa-app/src/utils/start-medusa.ts index 95189b5317..979f751176 100644 --- a/packages/cli/create-medusa-app/src/utils/start-medusa.ts +++ b/packages/cli/create-medusa-app/src/utils/start-medusa.ts @@ -1,12 +1,19 @@ import { exec } from "child_process" +import PackageManager from "./package-manager.js" type StartOptions = { directory: string abortController?: AbortController + packageManager: PackageManager } -export default ({ directory, abortController }: StartOptions) => { - const childProcess = exec(`npm run dev`, { +export default ({ + directory, + abortController, + packageManager, +}: StartOptions) => { + const command = packageManager.getCommandStr(`dev`) + const childProcess = exec(command, { cwd: directory, signal: abortController?.signal, env: { diff --git a/packages/cli/create-medusa-app/tsconfig.json b/packages/cli/create-medusa-app/tsconfig.json index 17926777ae..37069140f8 100644 --- a/packages/cli/create-medusa-app/tsconfig.json +++ b/packages/cli/create-medusa-app/tsconfig.json @@ -10,6 +10,13 @@ "resolveJsonModule": true }, "include": ["src"], + "exclude": [ + "dist", + "src/**/__tests__", + "src/**/__mocks__", + "src/**/__fixtures__", + "node_modules" + ], "ts-node": { "esm": true, "experimentalSpecifierResolution": "node",