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:
1 parent
8426fca710
commit
42235825ee
7 files changed
+666
-9
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/cli": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa-cli): add codemod command + codemod for replacing zod imports
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { sync as existsSync } from "fs-exists-cached"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import resolveCwd from "resolve-cwd"
|
import resolveCwd from "resolve-cwd"
|
||||||
|
import { getCodemod, listCodemods } from "./codemods/index"
|
||||||
import { newStarter } from "./commands/new"
|
import { newStarter } from "./commands/new"
|
||||||
import { didYouMean } from "./did-you-mean"
|
import { didYouMean } from "./did-you-mean"
|
||||||
import reporter from "./reporter"
|
import reporter from "./reporter"
|
||||||
@@ -296,7 +297,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
command: "plugin:build",
|
command: "plugin:build",
|
||||||
desc: "Build plugin source for publishing to a package registry",
|
desc: "Build plugin source for publishing to a package registry",
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler("plugin/build", (args, cmd) => {
|
getCommandHandler("plugin/build", async (args, cmd) => {
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
cmd(args)
|
cmd(args)
|
||||||
return new Promise((resolve) => {})
|
return new Promise((resolve) => {})
|
||||||
@@ -307,7 +308,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
command: "plugin:develop",
|
command: "plugin:develop",
|
||||||
desc: "Start plugin development process in watch mode. Changes will be re-published to the local packages registry",
|
desc: "Start plugin development process in watch mode. Changes will be re-published to the local packages registry",
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler("plugin/develop", (args, cmd) => {
|
getCommandHandler("plugin/develop", async (args, cmd) => {
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
cmd(args)
|
cmd(args)
|
||||||
return new Promise(() => {})
|
return new Promise(() => {})
|
||||||
@@ -318,7 +319,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
command: "plugin:publish",
|
command: "plugin:publish",
|
||||||
desc: "Publish the plugin to the local packages registry",
|
desc: "Publish the plugin to the local packages registry",
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler("plugin/publish", (args, cmd) => {
|
getCommandHandler("plugin/publish", async (args, cmd) => {
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
cmd(args)
|
cmd(args)
|
||||||
return new Promise(() => {})
|
return new Promise(() => {})
|
||||||
@@ -336,7 +337,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler("plugin/add", (args, cmd) => {
|
getCommandHandler("plugin/add", async (args, cmd) => {
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
cmd(args)
|
cmd(args)
|
||||||
return new Promise(() => {})
|
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({
|
||||||
command: `develop`,
|
command: `develop`,
|
||||||
desc: `Start development server. Watches file and rebuilds when something changes`,
|
desc: `Start development server. Watches file and rebuilds when something changes`,
|
||||||
@@ -392,7 +449,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
: `Set port. Defaults to ${defaultPort}`,
|
: `Set port. Defaults to ${defaultPort}`,
|
||||||
}),
|
}),
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler(`develop`, (args, cmd) => {
|
getCommandHandler(`develop`, async (args, cmd) => {
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
|
|
||||||
cmd(args)
|
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%).",
|
"Number of server processes in cluster mode or a percentage of cluster size (e.g., 25%).",
|
||||||
}),
|
}),
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler(`start`, (args, cmd) => {
|
getCommandHandler(`start`, async (args, cmd) => {
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || `production`
|
process.env.NODE_ENV = process.env.NODE_ENV || `production`
|
||||||
cmd(args)
|
cmd(args)
|
||||||
// Return an empty promise to prevent handlerP from exiting early.
|
// 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)",
|
"Only build the admin to serve it separately (outDir .medusa/admin)",
|
||||||
}),
|
}),
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler(`build`, (args, cmd) => {
|
getCommandHandler(`build`, async (args, cmd) => {
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
cmd(args)
|
cmd(args)
|
||||||
|
|
||||||
@@ -501,7 +558,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
default: false,
|
default: false,
|
||||||
}),
|
}),
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler(`user`, (args, cmd) => {
|
getCommandHandler(`user`, async (args, cmd) => {
|
||||||
cmd(args)
|
cmd(args)
|
||||||
// Return an empty promise to prevent handlerP from exiting early.
|
// Return an empty promise to prevent handlerP from exiting early.
|
||||||
// The development server shouldn't ever exit until the user directly
|
// The development server shouldn't ever exit until the user directly
|
||||||
@@ -514,7 +571,7 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
command: `exec [file] [args..]`,
|
command: `exec [file] [args..]`,
|
||||||
desc: `Run a function defined in a file.`,
|
desc: `Run a function defined in a file.`,
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler(`exec`, (args, cmd) => {
|
getCommandHandler(`exec`, async (args, cmd) => {
|
||||||
cmd(args)
|
cmd(args)
|
||||||
// Return an empty promise to prevent handlerP from exiting early.
|
// Return an empty promise to prevent handlerP from exiting early.
|
||||||
// The development server shouldn't ever exit until the user directly
|
// The development server shouldn't ever exit until the user directly
|
||||||
|
|||||||
Reference in new issue
Block a user