Files
medusa-store/packages/cli/oas/medusa-oas-cli/src/command-oas.ts
Harminder Virk 71a9a7210c fix: oas CLI to convert Windows paths to unix (#12254)
* fix: oas CLI to convert Windows paths to unix

* Create tidy-pigs-heal.md

* refactor: revert to isolate the issue

* refactor: add back the change with logs

* fix: remove external dependency
2025-04-22 14:41:58 +02:00

234 lines
6.1 KiB
TypeScript

import OpenAPIParser from "@readme/openapi-parser"
import { Command, Option, OptionValues } from "commander"
import { lstat, mkdir, writeFile } from "fs/promises"
import { OpenAPIObject } from "openapi3-ts"
import * as path from "path"
import swaggerInline from "swagger-inline"
import { combineOAS } from "./utils/combine-oas"
import {
mergeBaseIntoOAS,
mergePathsAndSchemasIntoOAS,
} from "./utils/merge-oas"
import { isFile } from "./utils/fs-utils"
/**
* Constants
*/
// Medusa core package directory
const medusaPackagePath = path.dirname(require.resolve("@medusajs/medusa"))
const basePath = path.resolve(__dirname, "../")
/**
* CLI Command declaration
*/
export const commandName = "oas"
export const commandDescription =
"Compile full OAS from swagger-inline compliant JSDoc."
export const commandOptions: Option[] = [
new Option("-t, --type <type>", "API type to compile.")
.choices(["admin", "store", "combined"])
.makeOptionMandatory(),
new Option(
"-o, --out-dir <outDir>",
"Destination directory to output generated OAS files."
).default(process.cwd()),
new Option("-D, --dry-run", "Do not output files."),
new Option(
"-p, --paths <paths...>",
"Additional paths to crawl for OAS JSDoc."
),
new Option(
"-b, --base <base>",
"Custom base OAS file to use for swagger-inline."
),
new Option("-F, --force", "Ignore OAS validation and output OAS files."),
new Option(
"--local",
"Generate OAS from local files rather than public OAS. This is useful for generating references in the Medusa monorepo."
),
]
export function getCommand() {
const command = new Command(commandName)
command.description(commandDescription)
for (const opt of commandOptions) {
command.addOption(opt)
}
command.action(async (options) => await execute(options))
command.showHelpAfterError(true)
return command
}
/**
* Main
*/
export async function execute(cliParams: OptionValues) {
/**
* Process CLI options
*/
const dryRun = !!cliParams.dryRun
const force = !!cliParams.force
const v2 = !!cliParams.v2
const local = !!cliParams.local
const apiType: ApiType = cliParams.type
const outDir = path.resolve(cliParams.outDir)
const additionalPaths = (cliParams.paths ?? []).map((additionalPath) =>
path.resolve(additionalPath)
)
for (const additionalPath of additionalPaths) {
if (!(await isDirectory(additionalPath))) {
throw new Error(`--paths must be a directory - ${additionalPath}`)
}
}
const baseFile = cliParams.base ? path.resolve(cliParams.base) : undefined
if (baseFile) {
if (!(await isFile(cliParams.base))) {
throw new Error(`--base must be a file - ${baseFile}`)
}
}
/**
* Command execution
*/
if (!dryRun) {
await mkdir(outDir, { recursive: true })
}
let oas: OpenAPIObject
console.log(`🟣 Generating OAS - ${apiType}`)
if (apiType === "combined") {
const adminOAS = !local
? await getPublicOas("admin")
: await getOASFromCodebase("admin")
const storeOAS = !local
? await getPublicOas("store")
: await getOASFromCodebase("store")
oas = await combineOAS(adminOAS, storeOAS)
} else {
oas = !local
? await getPublicOas(apiType)
: await getOASFromCodebase(apiType)
}
if (additionalPaths.length || baseFile) {
const customOAS = await getOASFromPaths(additionalPaths, baseFile)
if (baseFile) {
mergeBaseIntoOAS(oas, customOAS)
}
if (additionalPaths.length) {
mergePathsAndSchemasIntoOAS(oas, customOAS)
}
}
await validateOAS(oas, apiType, force)
if (dryRun) {
console.log(`⚫️ Dry run - no files generated`)
return
}
await exportOASToJSON(oas, apiType, outDir)
}
/**
* Methods
*/
async function getOASFromCodebase(apiType: ApiType): Promise<OpenAPIObject> {
/**
* OAS output directory
*/
const oasOutputPath = path.resolve(
__dirname,
"..",
"..",
"..",
"..",
"..",
"www",
"utils",
"generated",
"oas-output"
)
const gen = await swaggerInline(
[
path.resolve(oasOutputPath, "operations", apiType),
path.resolve(oasOutputPath, "schemas"),
// We currently load error schemas from here. If we change
// that in the future, we should change the path.
path.resolve(medusaPackagePath, "dist", "utils/middlewares"),
],
{
base: path.resolve(oasOutputPath, "base", `${apiType}.oas.base.yaml`),
format: ".json",
}
)
return (await OpenAPIParser.parse(JSON.parse(gen))) as OpenAPIObject
}
async function getPublicOas(apiType: ApiType) {
const url = `https://docs.medusajs.com/api/download/${apiType}`
return (await OpenAPIParser.parse(url)) as OpenAPIObject
}
async function getOASFromPaths(
additionalPaths: string[] = [],
customBaseFile?: string
): Promise<OpenAPIObject> {
console.log(`🔵 Gathering custom OAS`)
const gen = await swaggerInline(
additionalPaths.map((additionalPath) => {
return additionalPath.replace(/\\/g, "/")
}),
{
base:
customBaseFile ??
path.resolve(basePath, "oas", "default.oas.base.yaml"),
format: ".json",
logger: (log) => {
console.log(log)
},
}
)
return (await OpenAPIParser.parse(JSON.parse(gen))) as OpenAPIObject
}
async function validateOAS(
oas: OpenAPIObject,
apiType: ApiType,
force = false
): Promise<void> {
try {
await OpenAPIParser.validate(JSON.parse(JSON.stringify(oas)))
console.log(`🟢 Valid OAS - ${apiType}`)
} catch (err) {
console.error(`🔴 Invalid OAS - ${apiType}`, err)
if (!force) {
process.exit(1)
}
}
}
async function exportOASToJSON(
oas: OpenAPIObject,
apiType: ApiType,
targetDir: string
): Promise<void> {
const json = JSON.stringify(oas, null, 2)
const filePath = path.resolve(targetDir, `${apiType}.oas.json`)
await writeFile(filePath, json)
console.log(`⚫️ Exported OAS - ${apiType} - ${filePath}`)
}
async function isDirectory(dirPath: string): Promise<boolean> {
try {
return (await lstat(path.resolve(dirPath))).isDirectory()
} catch (err) {
console.log(err)
return false
}
}