feat(medusa-cli): add codemod command + codemod for replacing zod imports (#14520)
* feat(medusa-cli): add codemod command + codemod for replacing zod imports * fixes
This commit is contained in:
5
.changeset/bold-moons-hear.md
Normal file
5
.changeset/bold-moons-hear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/cli": patch
|
||||
---
|
||||
|
||||
feat(medusa-cli): add codemod command + codemod for replacing zod imports
|
||||
35
packages/cli/medusa-cli/src/codemods/__tests__/index.test.ts
Normal file
35
packages/cli/medusa-cli/src/codemods/__tests__/index.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "@jest/globals"
|
||||
import { getCodemod, listCodemods } from "../index"
|
||||
|
||||
describe("Codemod dispatcher", () => {
|
||||
describe("listCodemods", () => {
|
||||
it("should return array of available codemod names", () => {
|
||||
const codemods = listCodemods()
|
||||
expect(Array.isArray(codemods)).toBe(true)
|
||||
expect(codemods.length).toBeGreaterThan(0)
|
||||
expect(codemods).toContain("replace-zod-imports")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getCodemod", () => {
|
||||
it("should return codemod for valid name", () => {
|
||||
const codemod = getCodemod("replace-zod-imports")
|
||||
expect(codemod).not.toBeNull()
|
||||
expect(codemod?.name).toBe("replace-zod-imports")
|
||||
expect(codemod?.description).toBeTruthy()
|
||||
expect(typeof codemod?.run).toBe("function")
|
||||
})
|
||||
|
||||
it("should return null for invalid codemod name", () => {
|
||||
const codemod = getCodemod("non-existent-codemod")
|
||||
expect(codemod).toBeNull()
|
||||
})
|
||||
|
||||
it("should return codemod with correct interface", () => {
|
||||
const codemod = getCodemod("replace-zod-imports")
|
||||
expect(codemod).toHaveProperty("name")
|
||||
expect(codemod).toHaveProperty("description")
|
||||
expect(codemod).toHaveProperty("run")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,324 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"
|
||||
import replaceZodImports from "../replace-zod-imports"
|
||||
|
||||
describe("replace-zod-imports codemod", () => {
|
||||
const tempDir = path.join(__dirname, "temp-test-codemod")
|
||||
let originalCwd: string
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original working directory
|
||||
originalCwd = process.cwd()
|
||||
|
||||
// Create temp directory for test files
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
fs.mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
// Change to temp directory so codemod runs there
|
||||
process.chdir(tempDir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original working directory
|
||||
process.chdir(originalCwd)
|
||||
|
||||
// Clean up temp directory
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe("codemod metadata", () => {
|
||||
it("should have correct name and description", () => {
|
||||
expect(replaceZodImports.name).toBe("replace-zod-imports")
|
||||
expect(replaceZodImports.description).toBeTruthy()
|
||||
expect(typeof replaceZodImports.run).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("named import transformations", () => {
|
||||
it("should transform named imports from zod", async () => {
|
||||
const testFile = path.join(tempDir, "test1.ts")
|
||||
fs.writeFileSync(testFile, `import { z, ZodSchema } from "zod"`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(
|
||||
`import { z, ZodSchema } from "@medusajs/framework/zod"`
|
||||
)
|
||||
})
|
||||
|
||||
it("should transform named imports with single quotes", async () => {
|
||||
const testFile = path.join(tempDir, "test2.ts")
|
||||
fs.writeFileSync(testFile, `import { z } from 'zod'`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(`import { z } from "@medusajs/framework/zod"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("default import transformations", () => {
|
||||
it("should transform default imports with identifier zod to aliased named imports", async () => {
|
||||
const testFile = path.join(tempDir, "test3.ts")
|
||||
fs.writeFileSync(testFile, `import zod from "zod"`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(`import { z as zod } from "@medusajs/framework/zod"`)
|
||||
})
|
||||
|
||||
it("should transform default imports with identifier z to named imports", async () => {
|
||||
const testFile = path.join(tempDir, "test3b.ts")
|
||||
fs.writeFileSync(testFile, `import z from "zod"`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(`import { z } from "@medusajs/framework/zod"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("namespace import transformations", () => {
|
||||
it("should transform namespace imports with identifier z", async () => {
|
||||
const testFile = path.join(tempDir, "test4.ts")
|
||||
fs.writeFileSync(testFile, `import * as z from "zod"`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(`import { z as z } from "@medusajs/framework/zod"`)
|
||||
})
|
||||
|
||||
it("should transform namespace imports with custom identifier", async () => {
|
||||
const testFile = path.join(tempDir, "test4b.ts")
|
||||
fs.writeFileSync(testFile, `import * as validator from "zod"`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(
|
||||
`import { z as validator } from "@medusajs/framework/zod"`
|
||||
)
|
||||
})
|
||||
|
||||
it("should transform namespace imports with zod identifier", async () => {
|
||||
const testFile = path.join(tempDir, "test4c.ts")
|
||||
fs.writeFileSync(testFile, `import * as zod from "zod"`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(`import { z as zod } from "@medusajs/framework/zod"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("type import transformations", () => {
|
||||
it("should transform type imports", async () => {
|
||||
const testFile = path.join(tempDir, "test5.ts")
|
||||
fs.writeFileSync(testFile, `import type { ZodSchema } from "zod"`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(
|
||||
`import type { ZodSchema } from "@medusajs/framework/zod"`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("require statement transformations", () => {
|
||||
it("should transform require statements", async () => {
|
||||
const testFile = path.join(tempDir, "test6.js")
|
||||
fs.writeFileSync(testFile, `const zod = require("zod")`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(`const zod = require("@medusajs/framework/zod")`)
|
||||
})
|
||||
|
||||
it("should transform require with single quotes", async () => {
|
||||
const testFile = path.join(tempDir, "test7.js")
|
||||
fs.writeFileSync(testFile, `const z = require('zod')`)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
expect(result).toBe(`const z = require("@medusajs/framework/zod")`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiple imports in one file", () => {
|
||||
it("should handle multiple zod imports", async () => {
|
||||
const testFile = path.join(tempDir, "test8.ts")
|
||||
const content = `import { z } from "zod"
|
||||
import { something } from "other-package"
|
||||
import type { ZodSchema } from "zod"
|
||||
const zodRequire = require("zod")`
|
||||
fs.writeFileSync(testFile, content)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
const expected = `import { z } from "@medusajs/framework/zod"
|
||||
import { something } from "other-package"
|
||||
import type { ZodSchema } from "@medusajs/framework/zod"
|
||||
const zodRequire = require("@medusajs/framework/zod")`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("dry-run mode", () => {
|
||||
it("should not modify files in dry-run mode", async () => {
|
||||
const testFile = path.join(tempDir, "test9.ts")
|
||||
const originalContent = `import { z } from "zod"`
|
||||
fs.writeFileSync(testFile, originalContent)
|
||||
|
||||
const result = await replaceZodImports.run({ dryRun: true })
|
||||
|
||||
const afterContent = fs.readFileSync(testFile, "utf8")
|
||||
expect(afterContent).toBe(originalContent)
|
||||
expect(result.filesModified).toBeGreaterThan(0)
|
||||
expect(result.errors).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("files without zod imports", () => {
|
||||
it("should not modify files without zod imports", async () => {
|
||||
const testFile = path.join(tempDir, "test10.ts")
|
||||
const originalContent = `import { something } from "other-package"`
|
||||
fs.writeFileSync(testFile, originalContent)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const afterContent = fs.readFileSync(testFile, "utf8")
|
||||
expect(afterContent).toBe(originalContent)
|
||||
})
|
||||
|
||||
it("should not modify partial matches like zodiac", async () => {
|
||||
const testFile = path.join(tempDir, "test11.ts")
|
||||
const originalContent = `import { something } from "zodiac"`
|
||||
fs.writeFileSync(testFile, originalContent)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const afterContent = fs.readFileSync(testFile, "utf8")
|
||||
expect(afterContent).toBe(originalContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiple files", () => {
|
||||
it("should handle multiple files with different extensions", async () => {
|
||||
const file1 = path.join(tempDir, "file1.ts")
|
||||
const file2 = path.join(tempDir, "file2.js")
|
||||
const file3 = path.join(tempDir, "file3.tsx")
|
||||
|
||||
fs.writeFileSync(file1, `import { z } from "zod"`)
|
||||
fs.writeFileSync(file2, `const z = require("zod")`)
|
||||
fs.writeFileSync(file3, `import type { ZodType } from "zod"`)
|
||||
|
||||
const result = await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
expect(fs.readFileSync(file1, "utf8")).toBe(
|
||||
`import { z } from "@medusajs/framework/zod"`
|
||||
)
|
||||
expect(fs.readFileSync(file2, "utf8")).toBe(
|
||||
`const z = require("@medusajs/framework/zod")`
|
||||
)
|
||||
expect(fs.readFileSync(file3, "utf8")).toBe(
|
||||
`import type { ZodType } from "@medusajs/framework/zod"`
|
||||
)
|
||||
expect(result.filesModified).toBeGreaterThanOrEqual(3)
|
||||
expect(result.errors).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("result reporting", () => {
|
||||
it("should return correct counts", async () => {
|
||||
const file1 = path.join(tempDir, "count1.ts")
|
||||
const file2 = path.join(tempDir, "count2.ts")
|
||||
const file3 = path.join(tempDir, "no-zod.ts")
|
||||
|
||||
fs.writeFileSync(file1, `import { z } from "zod"`)
|
||||
fs.writeFileSync(file2, `import { z } from "zod"`)
|
||||
fs.writeFileSync(file3, `import { x } from "other"`)
|
||||
|
||||
const result = await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
expect(result.filesScanned).toBeGreaterThanOrEqual(2)
|
||||
expect(result.filesModified).toBeGreaterThanOrEqual(2)
|
||||
expect(result.errors).toBe(0)
|
||||
})
|
||||
|
||||
it("should return zero counts when no files have zod imports", async () => {
|
||||
const testFile = path.join(tempDir, "empty.ts")
|
||||
fs.writeFileSync(testFile, `import { x } from "other"`)
|
||||
|
||||
const result = await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
expect(result.filesScanned).toBe(0)
|
||||
expect(result.filesModified).toBe(0)
|
||||
expect(result.errors).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("file formatting preservation", () => {
|
||||
it("should preserve whitespace and comments", async () => {
|
||||
const testFile = path.join(tempDir, "formatted.ts")
|
||||
const content = `// Header comment
|
||||
import { z } from "zod"
|
||||
|
||||
// Function comment
|
||||
export function validate() {
|
||||
return z.string()
|
||||
}`
|
||||
fs.writeFileSync(testFile, content)
|
||||
|
||||
await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
const result = fs.readFileSync(testFile, "utf8")
|
||||
const expected = `// Header comment
|
||||
import { z } from "@medusajs/framework/zod"
|
||||
|
||||
// Function comment
|
||||
export function validate() {
|
||||
return z.string()
|
||||
}`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("directory exclusions", () => {
|
||||
it("should ignore files in src/admin directories", async () => {
|
||||
const adminDir = path.join(tempDir, "src", "admin")
|
||||
fs.mkdirSync(adminDir, { recursive: true })
|
||||
|
||||
const adminFile = path.join(adminDir, "admin-component.tsx")
|
||||
const originalContent = `import { z } from "zod"`
|
||||
fs.writeFileSync(adminFile, originalContent)
|
||||
|
||||
const regularFile = path.join(tempDir, "regular-file.ts")
|
||||
fs.writeFileSync(regularFile, `import { z } from "zod"`)
|
||||
|
||||
const result = await replaceZodImports.run({ dryRun: false })
|
||||
|
||||
// Admin file should not be modified
|
||||
const adminContent = fs.readFileSync(adminFile, "utf8")
|
||||
expect(adminContent).toBe(originalContent)
|
||||
|
||||
// Regular file should be modified
|
||||
const regularContent = fs.readFileSync(regularFile, "utf8")
|
||||
expect(regularContent).toBe(`import { z } from "@medusajs/framework/zod"`)
|
||||
|
||||
// Result should only count the regular file
|
||||
expect(result.filesModified).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
26
packages/cli/medusa-cli/src/codemods/index.ts
Normal file
26
packages/cli/medusa-cli/src/codemods/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Codemod } from "./types"
|
||||
import replaceZodImports from "./replace-zod-imports"
|
||||
|
||||
/**
|
||||
* Registry of available codemods
|
||||
*/
|
||||
const CODEMODS: Record<string, Codemod> = {
|
||||
"replace-zod-imports": replaceZodImports,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a codemod by name
|
||||
* @param name - The name of the codemod to retrieve
|
||||
* @returns The codemod if found, null otherwise
|
||||
*/
|
||||
export function getCodemod(name: string): Codemod | null {
|
||||
return CODEMODS[name] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available codemod names
|
||||
* @returns Array of codemod names
|
||||
*/
|
||||
export function listCodemods(): string[] {
|
||||
return Object.keys(CODEMODS)
|
||||
}
|
||||
165
packages/cli/medusa-cli/src/codemods/replace-zod-imports.ts
Normal file
165
packages/cli/medusa-cli/src/codemods/replace-zod-imports.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import fs from "fs"
|
||||
import reporter from "../reporter/index"
|
||||
import type { Codemod, CodemodOptions, CodemodResult } from "./types"
|
||||
import { glob } from "glob"
|
||||
|
||||
const CODEMOD: Codemod = {
|
||||
name: "replace-zod-imports",
|
||||
description: "Replace all zod imports with @medusajs/framework/zod imports",
|
||||
run: replaceZodImports,
|
||||
}
|
||||
|
||||
export default CODEMOD
|
||||
|
||||
// Replacement patterns for zod imports
|
||||
// Order matters: more specific patterns must come before general ones
|
||||
const REPLACEMENTS = [
|
||||
// Default import with identifier "zod": import zod from "zod" -> import { z as zod } from "@medusajs/framework/zod"
|
||||
{
|
||||
pattern: /import\s+zod\s+from\s+['"]zod['"]/g,
|
||||
replacement: `import { z as zod } from "@medusajs/framework/zod"`,
|
||||
},
|
||||
// Default import with identifier "z": import z from "zod" -> import { z } from "@medusajs/framework/zod"
|
||||
{
|
||||
pattern: /import\s+z\s+from\s+['"]zod['"]/g,
|
||||
replacement: `import { z } from "@medusajs/framework/zod"`,
|
||||
},
|
||||
// Namespace import with other identifier: import * as something from "zod" -> import { z as something } from "@medusajs/framework/zod"
|
||||
{
|
||||
pattern: /import\s+\*\s+as\s+(\w+)\s+from\s+['"]zod['"]/g,
|
||||
replacement: `import { z as $1 } from "@medusajs/framework/zod"`,
|
||||
},
|
||||
// Named/type imports: import { z } from "zod" or import type { ZodSchema } from "zod"
|
||||
{
|
||||
pattern: /from\s+['"]zod['"]/g,
|
||||
replacement: `from "@medusajs/framework/zod"`,
|
||||
},
|
||||
// CommonJS require: require("zod")
|
||||
{
|
||||
pattern: /require\s*\(\s*['"]zod['"]\s*\)/g,
|
||||
replacement: `require("@medusajs/framework/zod")`,
|
||||
},
|
||||
]
|
||||
|
||||
const ZOD_IMPORT_PATTERN = /from\s+['"]zod['"]|require\s*\(\s*['"]zod['"]\s*\)/
|
||||
|
||||
/**
|
||||
* Replace all zod imports with @medusajs/framework/zod imports
|
||||
*/
|
||||
async function replaceZodImports(
|
||||
options: CodemodOptions
|
||||
): Promise<CodemodResult> {
|
||||
const { dryRun = false } = options
|
||||
const targetFiles = await getTargetFiles()
|
||||
const numberOfFiles = Object.keys(targetFiles).length
|
||||
|
||||
if (numberOfFiles === 0) {
|
||||
reporter.info(" No files found with zod imports")
|
||||
return { filesScanned: 0, filesModified: 0, errors: 0 }
|
||||
}
|
||||
|
||||
reporter.info(` Found ${numberOfFiles} files to process`)
|
||||
|
||||
let filesModified = 0
|
||||
let errors = 0
|
||||
|
||||
for (const [filePath, content] of Object.entries(targetFiles)) {
|
||||
try {
|
||||
if (processFile(filePath, content, dryRun)) {
|
||||
filesModified++
|
||||
}
|
||||
} catch (error) {
|
||||
reporter.error(`✗ Error processing ${filePath}: ${error.message}`)
|
||||
errors++
|
||||
}
|
||||
}
|
||||
|
||||
return { filesScanned: numberOfFiles, filesModified, errors }
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single file and replace zod imports
|
||||
* @returns true if the file was modified, false otherwise
|
||||
*/
|
||||
function processFile(
|
||||
filePath: string,
|
||||
content: string,
|
||||
dryRun: boolean
|
||||
): boolean {
|
||||
let modifiedContent = content
|
||||
|
||||
for (const { pattern, replacement } of REPLACEMENTS) {
|
||||
modifiedContent = modifiedContent.replace(pattern, replacement)
|
||||
}
|
||||
|
||||
if (modifiedContent === content) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
reporter.info(` Would update: ${filePath}`)
|
||||
} else {
|
||||
fs.writeFileSync(filePath, modifiedContent)
|
||||
reporter.info(`✓ Updated: ${filePath}`)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all TypeScript/JavaScript files that contain zod imports
|
||||
* @returns Array of file paths with zod imports
|
||||
*/
|
||||
async function getTargetFiles(): Promise<Record<string, string>> {
|
||||
try {
|
||||
// Find TypeScript/JavaScript files, excluding build artifacts, dependencies, and src/admin
|
||||
const files = await glob("**/*.{ts,js,tsx,jsx}", {
|
||||
ignore: [
|
||||
"**/node_modules/**",
|
||||
"**/.git/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/coverage/**",
|
||||
"**/.medusa/**",
|
||||
"**/src/admin/**",
|
||||
],
|
||||
nodir: true,
|
||||
})
|
||||
|
||||
reporter.info(` Scanning ${files.length} files for zod imports...`)
|
||||
|
||||
const targetFiles: Record<string, string> = {}
|
||||
let processedCount = 0
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = fs.readFileSync(file, "utf8")
|
||||
|
||||
if (ZOD_IMPORT_PATTERN.test(content)) {
|
||||
targetFiles[file] = content
|
||||
}
|
||||
|
||||
processedCount++
|
||||
if (processedCount % 100 === 0) {
|
||||
process.stdout.write(
|
||||
`\r Processed ${processedCount}/${files.length} files...`
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be read
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount > 0) {
|
||||
process.stdout.write(
|
||||
`\r Processed ${processedCount} files. \n`
|
||||
)
|
||||
}
|
||||
|
||||
return targetFiles
|
||||
} catch (error) {
|
||||
reporter.error(`Error finding target files: ${error.message}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
45
packages/cli/medusa-cli/src/codemods/types.ts
Normal file
45
packages/cli/medusa-cli/src/codemods/types.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Options for running a codemod
|
||||
*/
|
||||
export interface CodemodOptions {
|
||||
/**
|
||||
* Run the codemod without making actual file changes
|
||||
*/
|
||||
dryRun?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of running a codemod
|
||||
*/
|
||||
export interface CodemodResult {
|
||||
/**
|
||||
* Total number of files scanned for changes
|
||||
*/
|
||||
filesScanned: number
|
||||
/**
|
||||
* Number of files that were modified
|
||||
*/
|
||||
filesModified: number
|
||||
/**
|
||||
* Number of errors encountered during execution
|
||||
*/
|
||||
errors: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A codemod that can be executed to transform code
|
||||
*/
|
||||
export interface Codemod {
|
||||
/**
|
||||
* Unique identifier for the codemod
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* Human-readable description of what the codemod does
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* Function that executes the codemod
|
||||
*/
|
||||
run: (options: CodemodOptions) => Promise<CodemodResult>
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { setTelemetryEnabled } from "@medusajs/telemetry"
|
||||
import { sync as existsSync } from "fs-exists-cached"
|
||||
import path from "path"
|
||||
import resolveCwd from "resolve-cwd"
|
||||
import { getCodemod, listCodemods } from "./codemods/index"
|
||||
import { newStarter } from "./commands/new"
|
||||
import { didYouMean } from "./did-you-mean"
|
||||
import reporter from "./reporter"
|
||||
@@ -296,7 +297,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
command: "plugin:build",
|
||||
desc: "Build plugin source for publishing to a package registry",
|
||||
handler: handlerP(
|
||||
getCommandHandler("plugin/build", (args, cmd) => {
|
||||
getCommandHandler("plugin/build", async (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
cmd(args)
|
||||
return new Promise((resolve) => {})
|
||||
@@ -307,7 +308,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
command: "plugin:develop",
|
||||
desc: "Start plugin development process in watch mode. Changes will be re-published to the local packages registry",
|
||||
handler: handlerP(
|
||||
getCommandHandler("plugin/develop", (args, cmd) => {
|
||||
getCommandHandler("plugin/develop", async (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
cmd(args)
|
||||
return new Promise(() => {})
|
||||
@@ -318,7 +319,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
command: "plugin:publish",
|
||||
desc: "Publish the plugin to the local packages registry",
|
||||
handler: handlerP(
|
||||
getCommandHandler("plugin/publish", (args, cmd) => {
|
||||
getCommandHandler("plugin/publish", async (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
cmd(args)
|
||||
return new Promise(() => {})
|
||||
@@ -336,7 +337,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
},
|
||||
},
|
||||
handler: handlerP(
|
||||
getCommandHandler("plugin/add", (args, cmd) => {
|
||||
getCommandHandler("plugin/add", async (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
cmd(args)
|
||||
return new Promise(() => {})
|
||||
@@ -365,6 +366,62 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
)
|
||||
}),
|
||||
})
|
||||
.command({
|
||||
command: `codemod <codemod-name>`,
|
||||
desc: `Run automated code transformations`,
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("codemod-name", {
|
||||
type: "string",
|
||||
describe: "Name of the codemod to run",
|
||||
demandOption: true,
|
||||
})
|
||||
.option(`dry-run`, {
|
||||
type: `boolean`,
|
||||
description: `Preview changes without modifying files`,
|
||||
default: false,
|
||||
}),
|
||||
handler: handlerP(async ({ codemodName, dryRun }) => {
|
||||
const codemod = getCodemod(codemodName)
|
||||
|
||||
if (!codemod) {
|
||||
const available = listCodemods()
|
||||
reporter.error(`Unknown codemod: ${codemodName}`)
|
||||
reporter.info(
|
||||
`\nAvailable codemods:\n${available
|
||||
.map((n) => ` - ${n}`)
|
||||
.join("\n")}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
reporter.info(`Running codemod: ${codemod.name}`)
|
||||
reporter.info(codemod.description)
|
||||
|
||||
if (dryRun) {
|
||||
reporter.info(`\n DRY RUN MODE - No files will be modified\n`)
|
||||
}
|
||||
|
||||
const result = await codemod.run({ dryRun })
|
||||
|
||||
reporter.info(`\n Summary:`)
|
||||
reporter.info(` Files scanned: ${result.filesScanned}`)
|
||||
reporter.info(` Files modified: ${result.filesModified}`)
|
||||
reporter.info(` Errors: ${result.errors}`)
|
||||
|
||||
if (dryRun && result.filesModified > 0) {
|
||||
reporter.info(`\n Run without --dry-run to apply changes`)
|
||||
} else if (result.filesModified > 0) {
|
||||
reporter.info(`\n Codemod completed successfully!`)
|
||||
reporter.info(`\n Next steps:`)
|
||||
reporter.info(` 1. Review changes: git diff`)
|
||||
reporter.info(` 2. Run tests to verify`)
|
||||
reporter.info(` 3. Commit if satisfied`)
|
||||
} else {
|
||||
reporter.info(`\n No modifications needed`)
|
||||
}
|
||||
}),
|
||||
})
|
||||
.command({
|
||||
command: `develop`,
|
||||
desc: `Start development server. Watches file and rebuilds when something changes`,
|
||||
@@ -392,7 +449,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
: `Set port. Defaults to ${defaultPort}`,
|
||||
}),
|
||||
handler: handlerP(
|
||||
getCommandHandler(`develop`, (args, cmd) => {
|
||||
getCommandHandler(`develop`, async (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
|
||||
cmd(args)
|
||||
@@ -447,7 +504,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
"Number of server processes in cluster mode or a percentage of cluster size (e.g., 25%).",
|
||||
}),
|
||||
handler: handlerP(
|
||||
getCommandHandler(`start`, (args, cmd) => {
|
||||
getCommandHandler(`start`, async (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `production`
|
||||
cmd(args)
|
||||
// Return an empty promise to prevent handlerP from exiting early.
|
||||
@@ -468,7 +525,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
"Only build the admin to serve it separately (outDir .medusa/admin)",
|
||||
}),
|
||||
handler: handlerP(
|
||||
getCommandHandler(`build`, (args, cmd) => {
|
||||
getCommandHandler(`build`, async (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
cmd(args)
|
||||
|
||||
@@ -501,7 +558,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
default: false,
|
||||
}),
|
||||
handler: handlerP(
|
||||
getCommandHandler(`user`, (args, cmd) => {
|
||||
getCommandHandler(`user`, async (args, cmd) => {
|
||||
cmd(args)
|
||||
// Return an empty promise to prevent handlerP from exiting early.
|
||||
// The development server shouldn't ever exit until the user directly
|
||||
@@ -514,7 +571,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
command: `exec [file] [args..]`,
|
||||
desc: `Run a function defined in a file.`,
|
||||
handler: handlerP(
|
||||
getCommandHandler(`exec`, (args, cmd) => {
|
||||
getCommandHandler(`exec`, async (args, cmd) => {
|
||||
cmd(args)
|
||||
// Return an empty promise to prevent handlerP from exiting early.
|
||||
// The development server shouldn't ever exit until the user directly
|
||||
|
||||
Reference in New Issue
Block a user