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](https://github.com/medusajs/medusa/blob/0adb0d9ff96087613b7a634cbc97a7e301b01121/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:
1 parent
1bfbe27b9b
commit
8137061908
20 files changed
+613
-82708
No files matched your search
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
.env
|
||||
*.sql
|
||||
@@ -0,0 +1,3 @@
|
||||
# @medusajs/oas-cli
|
||||
|
||||
## 0.1.0
|
||||
@@ -0,0 +1,72 @@
|
||||
# medusa-oas-cli - 0.1.0 - experimental
|
||||
|
||||
A command-line tool for all OpenAPI Specifications (OAS) related tooling.
|
||||
|
||||
## Install
|
||||
|
||||
`yarn add --dev @medusajs/medusa-oas-cli`
|
||||
|
||||
Install in the global namespace is not yet supported.
|
||||
~~`npm install -g @medusajs/medusa-oas-cli`~~
|
||||
|
||||
## Configuration / First time setup
|
||||
|
||||
N/A
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
yarn medusa-oas <command>
|
||||
```
|
||||
|
||||
### Command - `oas`
|
||||
|
||||
This command will scan the `@medusajs/medusa` package in order to extract JSDoc OAS into a json file.
|
||||
|
||||
By default, the command will output the two files `admin.oas.json` and `store.oas.json` in the same directory that the
|
||||
command was run.
|
||||
|
||||
Invalid OAS with throw an error and will prevent the files from being outputted.
|
||||
|
||||
#### `--type <string>`
|
||||
|
||||
Specify which API OAS to create. Accepts `all`, `admin`, `store`.
|
||||
Defaults to `all`.
|
||||
|
||||
```bash
|
||||
yarn medusa-oas oas --type admin
|
||||
```
|
||||
|
||||
#### `--out-dir <path>`
|
||||
|
||||
Specify in which directory should the files be outputted. It accepts a relative or absolute path.
|
||||
If the directory doesn't exist, it will be created. Defaults to `./`.
|
||||
|
||||
```bash
|
||||
yarm medusa-oas oas --out-dir
|
||||
```
|
||||
|
||||
#### `--paths <paths...>`
|
||||
|
||||
Allows passing additional directory paths to crawl for JSDoc OAS and include in the generated OAS.
|
||||
It accepts multiple entries.
|
||||
|
||||
```bash
|
||||
yarn medusa-oas oas --paths ~/medusa-server/src
|
||||
```
|
||||
|
||||
#### `--dry-run`
|
||||
|
||||
Will package the OAS but will not output file. Useful for validating OAS.
|
||||
|
||||
```bash
|
||||
yarn medusa-oas oas --dry-run
|
||||
```
|
||||
|
||||
#### `--force`
|
||||
|
||||
Ignore OAS errors and attempt to output generated OAS files.
|
||||
|
||||
```bash
|
||||
yarn medusa-oas oas --force
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@medusajs/medusa-oas-cli",
|
||||
"version": "0.1.0",
|
||||
"description": "OAS CLI",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"medusa-oas": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/oas/medusa-oas-cli"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||
"build": "tsc --build",
|
||||
"cli": "ts-node src/index.ts",
|
||||
"test": "jest --passWithNoTests",
|
||||
"test:unit": "jest --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/medusa": "*",
|
||||
"@readme/openapi-parser": "^2.4.0",
|
||||
"commander": "^10.0.0",
|
||||
"openapi3-ts": "^3.1.2",
|
||||
"swagger-inline": "^6.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import * as path from "path"
|
||||
import { lstat, mkdir, writeFile } from "fs/promises"
|
||||
import swaggerInline from "swagger-inline"
|
||||
import OpenAPIParser from "@readme/openapi-parser"
|
||||
import { OpenAPIObject } from "openapi3-ts"
|
||||
import { Command, Option, OptionValues } from "commander"
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
// Medusa core package directory
|
||||
const medusaPackagePath = path.dirname(
|
||||
require.resolve("@medusajs/medusa/package.json")
|
||||
)
|
||||
|
||||
type ApiType = "store" | "admin"
|
||||
|
||||
/**
|
||||
* 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(["all", "admin", "store"])
|
||||
.default("all"),
|
||||
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("-F, --force", "Ignore OAS validation and output OAS files."),
|
||||
]
|
||||
|
||||
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))
|
||||
return command
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
export async function execute(cliParams: OptionValues) {
|
||||
/**
|
||||
* Process CLI options
|
||||
*/
|
||||
const dryRun = !!cliParams.dryRun
|
||||
const force = !!cliParams.force
|
||||
|
||||
const apiTypesToExport =
|
||||
cliParams.type === "all" ? ["store", "admin"] : [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}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command execution
|
||||
*/
|
||||
if (!dryRun) {
|
||||
await mkdir(outDir, { recursive: true })
|
||||
}
|
||||
|
||||
for (const apiType of apiTypesToExport) {
|
||||
console.log(`🟣 Generating OAS - ${apiType}`)
|
||||
const oas = await getOASFromCodebase(apiType as ApiType, additionalPaths)
|
||||
await validateOAS(oas, apiType as ApiType, force)
|
||||
if (!dryRun) {
|
||||
await exportOASToJSON(oas, apiType as ApiType, outDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
async function getOASFromCodebase(
|
||||
apiType: ApiType,
|
||||
additionalPaths: string[] = []
|
||||
): Promise<OpenAPIObject> {
|
||||
const gen = await swaggerInline(
|
||||
[
|
||||
path.resolve(medusaPackagePath, "dist", "models"),
|
||||
path.resolve(medusaPackagePath, "dist", "types"),
|
||||
path.resolve(medusaPackagePath, "dist", "api/middlewares"),
|
||||
path.resolve(medusaPackagePath, "dist", `api/routes/${apiType}`),
|
||||
...additionalPaths,
|
||||
],
|
||||
{
|
||||
base: path.resolve(
|
||||
medusaPackagePath,
|
||||
"oas",
|
||||
`${apiType}-spec3-base.yaml`
|
||||
),
|
||||
format: ".json",
|
||||
}
|
||||
)
|
||||
|
||||
return await OpenAPIParser.parse(JSON.parse(gen))
|
||||
}
|
||||
|
||||
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> {
|
||||
return (await lstat(path.resolve(dirPath))).isDirectory()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
import { Command } from "commander"
|
||||
import { getCommand as oasGetCommand } from "./command-oas"
|
||||
|
||||
const run = async () => {
|
||||
const program = new Command()
|
||||
|
||||
/**
|
||||
* Alias to command-oas.ts
|
||||
*/
|
||||
program.addCommand(oasGetCommand())
|
||||
|
||||
/**
|
||||
* Run CLI
|
||||
*/
|
||||
await program.parseAsync()
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
await run()
|
||||
})()
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2019"
|
||||
],
|
||||
"target": "es2019",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true // to use ES5 specific tooling
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/tests/*"
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user