feat(oas): medusa-oas-cli as OAS build tool (#3213)

## What

Introduce a CLI for extracting OAS from the core `medusa` package.

## Why

We need to decouple OAS tooling from documentation tooling in order to allow packages and external systems to leverage our OAS has a dependency.

## How

Introduce a new OAS workspace within packages in order to organize current and future OAS related package. Only 1 OAS package for now.

Introduce a new CLI only package to act as the main gateway for all upcoming OAS tooling. Only 1 command for now.

Update documentation tooling pertaining to OAS to use the CLI instead.

## Test

### Prerequisite
From the monorepo root:
* `yarn install`
* `yarn build`

### Documentation

#### Case - validation only - success
* Run `yarn openapi:generate --dry-run`
* Expect console output `🟢 Valid OAS` but no mention of `🔵 Exported OAS`

#### Case - validation only - invalid
* Introduce a bug by renaming `@schema Cart` to `@schema Kart` in [models/cart.ts](0adb0d9ff9/packages/medusa/src/models/cart.ts (L2))
* Run `yarn build` to update `@medusajs/medusa` package with the bug.
* Run `yarn openapi:generate --dry-run`
* Expect console output `🔴 Invalid OAS` with a stack trace of the issue.

#### Case - docs generation
* Run `yarn openapi:generate`
* Expect `docs/api/` directory to contain:
  * `admin.oas.json` (raw OAS)
  * `store.oas.json` (raw OAS)
  * `admin.oas.yaml` (sanitized OAS)
  * `store.oas.yaml` (sanitized OAS)
  * `admin/` (updated redocly split output)
  * `store/` (updated redocly split output)

### CLI

#### Case - crawl additional paths
* From a local medusa server (`medusa-starter-default`), add an `index.ts` file in `src/models/`
* In the `index.ts`, add dummy OAS JSDoc like `/** @schema Foobar */`
* From the root of the monorepo, run `yarn medusa-oas --type store --paths path-to-medusa-server/src`
* Expect a `store.oas.json` to be created at the root of the monorepo.
* The `store.oas.json` should contain an additional `Foobar` entry in `components.schemas`.
This commit is contained in:
Patrick
2023-02-15 09:55:58 -05:00
committed by GitHub
parent 1bfbe27b9b
commit 8137061908
20 changed files with 613 additions and 82708 deletions

View File

@@ -1,151 +1,91 @@
#!/usr/bin/env node
const fs = require("fs")
const OAS = require("oas-normalize")
const swaggerInline = require("swagger-inline")
const { exec } = require("child_process")
const fs = require("fs/promises")
const path = require("path")
const execa = require("execa")
const yaml = require("js-yaml")
const OpenAPIParser = require("@readme/openapi-parser")
const isDryRun = process.argv.indexOf("--dry-run") !== -1
const basePath = path.resolve(__dirname, `../`)
const docsApiPath = path.resolve(basePath, "docs/api/")
// Storefront API
swaggerInline(
[
"./packages/medusa/src/models",
"./packages/medusa/src/types",
"./packages/medusa/src/api/middlewares",
"./packages/medusa/src/api/routes/store",
],
{
base: "./docs/api/store-spec3-base.yaml",
const run = async () => {
await generateOASSources(docsApiPath, isDryRun)
if (isDryRun) {
return
}
)
.then((gen) => {
const oas = new OAS(gen)
oas
.validate(true)
.then(() => {
if (!isDryRun) {
fs.writeFileSync("./docs/api/store-spec3.json", gen)
}
})
.catch((err) => {
console.log("Error in store")
console.error(err)
process.exit(1)
})
})
.catch((err) => {
console.log("Error in store")
console.error(err)
process.exit(1)
})
swaggerInline(
[
"./packages/medusa/src/models",
"./packages/medusa/src/types",
"./packages/medusa/src/api/middlewares",
"./packages/medusa/src/api/routes/store",
],
{
base: "./docs/api/store-spec3-base.yaml",
format: "yaml",
}
)
.then((gen) => {
if (!isDryRun) {
fs.writeFileSync("./docs/api/store-spec3.yaml", gen)
exec(
[
"rm -rf docs/api/store/",
"yarn run -- redocly bundle docs/api/store-spec3.yaml -o docs/api/store-spec3.yaml --config=docs-util/redocly/config.yaml",
"yarn run -- redocly split docs/api/store-spec3.yaml --outDir=docs/api/store/",
].join(" && "),
(error, stdout, stderr) => {
if (error) {
throw new Error(`error: ${error.message}`)
}
console.log(`${stderr || stdout}`)
}
)
} else {
console.log("No errors occurred while generating Store API Reference")
}
})
.catch((err) => {
console.log("Error in store")
console.error(err)
process.exit(1)
})
for (const apiType of ["store", "admin"]) {
const inputJsonFile = path.resolve(docsApiPath, `${apiType}.oas.json`)
const outputYamlFile = path.resolve(docsApiPath, `${apiType}.oas.yaml`)
// Admin API
swaggerInline(
[
"./packages/medusa/src/models",
"./packages/medusa/src/types",
"./packages/medusa/src/api/middlewares",
"./packages/medusa/src/api/routes/admin",
],
{
base: "./docs/api/admin-spec3-base.yaml",
await jsonFileToYamlFile(inputJsonFile, outputYamlFile)
await sanitizeOAS(outputYamlFile)
await circularReferenceCheck(outputYamlFile)
await generateReference(outputYamlFile, apiType)
}
)
.then((gen) => {
const oas = new OAS(gen)
oas
.validate(true)
.then(() => {
if (!isDryRun) {
fs.writeFileSync("./docs/api/admin-spec3.json", gen)
}
})
.catch((err) => {
console.log("Error in admin")
console.error(err)
process.exit(1)
})
})
.catch((err) => {
console.log("Error in admin")
console.error(err)
process.exit(1)
})
}
swaggerInline(
[
"./packages/medusa/src/models",
"./packages/medusa/src/types",
"./packages/medusa/src/api/middlewares",
"./packages/medusa/src/api/routes/admin",
],
{
base: "./docs/api/admin-spec3-base.yaml",
format: "yaml",
const generateOASSources = async (outDir, isDryRun) => {
const params = ["oas", `--out-dir=${outDir}`]
if (isDryRun) {
params.push("--dry-run")
}
)
.then((gen) => {
if (!isDryRun) {
fs.writeFileSync("./docs/api/admin-spec3.yaml", gen)
exec(
[
"rm -rf docs/api/admin/",
"yarn run -- redocly bundle docs/api/admin-spec3.yaml -o docs/api/admin-spec3.yaml --config=docs-util/redocly/config.yaml",
"yarn run -- redocly split docs/api/admin-spec3.yaml --outDir=docs/api/admin/",
].join(" && "),
(error, stdout, stderr) => {
if (error) {
throw new Error(`error: ${error.message}`)
}
console.log(`${stderr || stdout}`)
return
}
)
} else {
console.log("No errors occurred while generating Admin API Reference")
}
const { all: logs } = await execa("medusa-oas", params, {
cwd: basePath,
all: true,
})
.catch((err) => {
console.log("Error in admin")
console.error(err)
process.exit(1)
console.log(logs)
}
const jsonFileToYamlFile = async (inputJsonFile, outputYamlFile) => {
const jsonString = await fs.readFile(inputJsonFile, "utf8")
const jsonObject = JSON.parse(jsonString)
const yamlString = yaml.dump(jsonObject)
await fs.writeFile(outputYamlFile, yamlString, "utf8")
}
const sanitizeOAS = async (srcFile) => {
const { all: logs } = await execa(
"redocly",
[
"bundle",
srcFile,
`--output=${srcFile}`,
"--config=docs-util/redocly/config.yaml",
],
{ cwd: basePath, all: true }
)
console.log(logs)
}
const circularReferenceCheck = async (srcFile) => {
const parser = new OpenAPIParser()
await parser.validate(srcFile, {
dereference: {
circular: "ignore",
},
})
if (parser.$refs.circular) {
console.log(`🔴 Unhandled circular references - ${srcFile}`)
const circularRefs = [...parser.$refs.circularRefs]
circularRefs.sort()
console.log(circularRefs)
}
}
const generateReference = async (srcFile, apiType) => {
const outDir = path.resolve(docsApiPath, `${apiType}`)
await fs.rm(outDir, { recursive: true, force: true })
const { all: logs } = await execa(
"redocly",
["split", srcFile, `--outDir=${outDir}`],
{ cwd: basePath, all: true }
)
console.log(logs)
}
void (async () => {
await run()
})()