## What
New `medusa-oas docs` to improve OAS compatibility with for Redocly API documentation viewer and handle deep circular references.
## Why
We wish to share the tooling we have been using to generate our public API documentation.
## How
* Move `/docs-utils/redocly` under `medusa-oas-cli` package.
* Move some of the operations from `oas-github-ci/scripts/build-openapi.js` under the `medusa-oas docs` command.
* Improve DevX when dealing with circular references by outputting precise troubleshooting recommendations in the error message.
* Extract some common operations in utility methods.
## Tests
### Step 1
* Run `yarn install`
* Run `yarn build`
* Run `yarn openapi:generate --dry-run`
* Expect same behaviour as before where OAS is validated and circular references are checked.
### Step 2
* Run `yarn openapi:generate`
* Expect same behaviour as before where API documentation is generated in `/docs`
### Step 3
* Move to the `packages/oas/medusa-oas-cli`
* Run `yarn medusa-oas oas --type store --out-dir ~/tmp/oas` to generate the raw OAS file.
* Run `yarn medusa-oas docs --src-file ~/tmp/oas/store.oas.json --preview`
* Open url from the console output in a browser
* Expect a preview of the API documentation using Redocly.
### Step 4
* Run `yarn medusa-oas docs --src-file ~/tmp/oas/store.oas.json --out-dir ~/tmp/docs/store --clean --split`
* Expect a similiar output as `yarn openapi:generate`
### Step 5
* Run `yarn medusa-oas docs --src-file ~/tmp/oas/store.oas.json --out-dir ~/tmp/docs/store --clean --html`
* Expect `index.html` to have been created.
* Run `npx http-server ~/tmp/docs/store -p 8000`
* Open http://127.0.0.1:8000 in a browser.
* Expect a zero-dependency static rendering of the API documentation using Redocly.
### Step 6
* To emulate an unhandled circular reference, edit [packages/oas/medusa-oas-cli/redocly/redocly-config.yaml](d180f47e16/packages/oas/medusa-oas-cli/redocly/redocly-config.yaml (L9-L10)) and comment out "Address: - Customer"
* Run `yarn medusa-oas docs --src-file ~/tmp/oas/store.oas.json --dry-run`
* Expect an error message with a hint on how to resolve the issue.
* Create a file `~/tmp/redocly-config.yaml` and paste in the recommendation from the error message.
* Run `yarn medusa-oas docs --src-file ~/tmp/oas/store.oas.json --dry-run --config ~/tmp/redocly-config.yaml`
* Expect Dry run to succeed.
177 lines
4.4 KiB
TypeScript
177 lines
4.4 KiB
TypeScript
import path from "path"
|
|
import {
|
|
generate,
|
|
HttpClient,
|
|
Indent,
|
|
PackageNames,
|
|
} from "@medusajs/openapi-typescript-codegen"
|
|
import { upperFirst } from "lodash"
|
|
import fs, { mkdir, readFile } from "fs/promises"
|
|
import { OpenAPIObject } from "openapi3-ts"
|
|
import { Command, Option, OptionValues } from "commander"
|
|
|
|
/**
|
|
* CLI Command declaration
|
|
*/
|
|
export const commandName = "client"
|
|
export const commandDescription = "Generate API clients from OAS."
|
|
export const commandOptions: Option[] = [
|
|
new Option(
|
|
"-t, --type <type>",
|
|
"Namespace for the generated client. Usually `admin` or `store`."
|
|
).makeOptionMandatory(),
|
|
|
|
new Option(
|
|
"-s, --src-file <srcFile>",
|
|
"Path to source OAS JSON file."
|
|
).makeOptionMandatory(),
|
|
|
|
new Option(
|
|
"-o, --out-dir <outDir>",
|
|
"Output directory for generated client files."
|
|
).default(path.resolve(process.cwd(), "client")),
|
|
|
|
new Option(
|
|
"-c, --component <component>",
|
|
"Client component types to generate."
|
|
)
|
|
.choices(["all", "types", "client", "hooks"])
|
|
.default("all"),
|
|
|
|
new Option(
|
|
"--types-package <name>",
|
|
"Replace relative import statements by types package name."
|
|
),
|
|
|
|
new Option(
|
|
"--client-package <name>",
|
|
"Replace relative import statements by client package name."
|
|
),
|
|
|
|
new Option(
|
|
"--clean",
|
|
"Delete destination directory content before generating client."
|
|
),
|
|
]
|
|
|
|
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
|
|
*/
|
|
if (
|
|
["client", "hooks"].includes(cliParams.component) &&
|
|
!cliParams.typesPackage
|
|
) {
|
|
throw new Error(
|
|
`--types-package must be declared when using --component=${cliParams.component}`
|
|
)
|
|
}
|
|
if (cliParams.component === "hooks" && !cliParams.clientPackage) {
|
|
throw new Error(
|
|
`--client-package must be declared when using --component=${cliParams.component}`
|
|
)
|
|
}
|
|
|
|
const shouldClean = !!cliParams.clean
|
|
const srcFile = path.resolve(cliParams.srcFile)
|
|
const outDir = path.resolve(cliParams.outDir)
|
|
const apiName = cliParams.type
|
|
const packageNames: PackageNames = {
|
|
models: cliParams.typesPackage,
|
|
client: cliParams.clientPackage,
|
|
}
|
|
const exportComponent = cliParams.component
|
|
|
|
/**
|
|
* Command execution
|
|
*/
|
|
console.log(`🟣 Generating client - ${apiName} - ${exportComponent}`)
|
|
|
|
if (shouldClean) {
|
|
console.log(`🟠 Cleaning output directory`)
|
|
await fs.rm(outDir, { recursive: true, force: true })
|
|
}
|
|
await mkdir(outDir, { recursive: true })
|
|
|
|
const oas = await getOASFromFile(srcFile)
|
|
await generateClientSDK(oas, outDir, apiName, exportComponent, packageNames)
|
|
|
|
console.log(
|
|
`⚫️ Client generated - ${apiName} - ${exportComponent} - ${outDir}`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Methods
|
|
*/
|
|
const getOASFromFile = async (jsonFile: string): Promise<OpenAPIObject> => {
|
|
const jsonString = await readFile(jsonFile, "utf8")
|
|
return JSON.parse(jsonString)
|
|
}
|
|
|
|
const generateClientSDK = async (
|
|
oas: OpenAPIObject,
|
|
targetDir: string,
|
|
apiName: string,
|
|
exportComponent: "all" | "types" | "client" | "hooks",
|
|
packageNames: PackageNames = {}
|
|
) => {
|
|
const exports = {
|
|
exportCore: false,
|
|
exportServices: false,
|
|
exportModels: false,
|
|
exportHooks: false,
|
|
}
|
|
|
|
switch (exportComponent) {
|
|
case "types":
|
|
exports.exportModels = true
|
|
break
|
|
case "client":
|
|
exports.exportCore = true
|
|
exports.exportServices = true
|
|
break
|
|
case "hooks":
|
|
exports.exportHooks = true
|
|
break
|
|
default:
|
|
exports.exportCore = true
|
|
exports.exportServices = true
|
|
exports.exportModels = true
|
|
exports.exportHooks = true
|
|
}
|
|
|
|
await generate({
|
|
input: oas,
|
|
output: targetDir,
|
|
httpClient: HttpClient.AXIOS,
|
|
useOptions: true,
|
|
useUnionTypes: true,
|
|
exportCore: exports.exportCore,
|
|
exportServices: exports.exportServices,
|
|
exportModels: exports.exportModels,
|
|
exportHooks: exports.exportHooks,
|
|
exportSchemas: false,
|
|
indent: Indent.SPACE_2,
|
|
postfixServices: "Service",
|
|
postfixModels: "",
|
|
clientName: `Medusa${upperFirst(apiName)}`,
|
|
request: undefined,
|
|
packageNames,
|
|
})
|
|
}
|