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
This commit is contained in:
5
.changeset/slimy-gifts-wear.md
Normal file
5
.changeset/slimy-gifts-wear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-medusa-app": patch
|
||||
---
|
||||
|
||||
feat(create-medusa-app): add support for pnpm and specifying package manager
|
||||
14
packages/cli/create-medusa-app/jest.config.cjs
Normal file
14
packages/cli/create-medusa-app/jest.config.cjs
Normal file
@@ -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__/",
|
||||
],
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<typeof execute>
|
||||
const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>
|
||||
const mockRmSync = rmSync as jest.MockedFunction<typeof rmSync>
|
||||
const mockLogMessage = logMessage as jest.MockedFunction<typeof logMessage>
|
||||
|
||||
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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ExecuteOptions> => {
|
||||
): Promise<ExecuteResult> => {
|
||||
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 ||
|
||||
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,
|
||||
|
||||
@@ -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<string> {
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -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<string, unknown>
|
||||
): Promise<string | undefined> {
|
||||
const commands: Record<PackageManagerType, string> = {
|
||||
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<string, unknown>): Promise<void> {
|
||||
@@ -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<string, unknown>,
|
||||
) {
|
||||
async removeLockFiles(directory: string): Promise<void> {
|
||||
const lockFiles: Record<PackageManagerType, string[]> = {
|
||||
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<string, unknown>) {
|
||||
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<PackageManagerType, string> = {
|
||||
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<string, unknown>,
|
||||
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<string, unknown>,
|
||||
verboseOptions: VerboseOptions = {}
|
||||
) {
|
||||
if (!this.packageManager) {
|
||||
await this.setPackageManager(execOptions)
|
||||
}
|
||||
|
||||
const formats: Record<PackageManagerType, string> = {
|
||||
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<PackageManagerType, string> = {
|
||||
yarn: `yarn ${command}`,
|
||||
pnpm: `pnpm ${command}`,
|
||||
npm: `npm run ${command}`,
|
||||
}
|
||||
|
||||
return formats[this.packageManager]
|
||||
}
|
||||
|
||||
getPackageManager(): PackageManagerType | undefined {
|
||||
return this.packageManager
|
||||
}
|
||||
|
||||
async getPackageManagerString(): Promise<string | undefined> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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: (?<token>.+)/
|
||||
)
|
||||
inviteToken = match?.groups?.token
|
||||
},
|
||||
})
|
||||
// get invite token from stdout
|
||||
const match = (userExecResult.stdout as string).match(
|
||||
/Invite token: (?<token>.+)/
|
||||
)
|
||||
inviteToken = match?.groups?.token
|
||||
|
||||
// TODO for now we just seed the default data
|
||||
// we should add onboarding seeding again if it makes
|
||||
|
||||
@@ -16,8 +16,8 @@ export default class ProcessManager {
|
||||
}
|
||||
|
||||
onTerminated(fn: () => Promise<void> | 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" &&
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<void> {
|
||||
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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"src/**/__tests__",
|
||||
"src/**/__mocks__",
|
||||
"src/**/__fixtures__",
|
||||
"node_modules"
|
||||
],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifierResolution": "node",
|
||||
|
||||
Reference in New Issue
Block a user