Skip to content
This repository has been archived by the owner on Mar 19, 2024. It is now read-only.

feat: new @scalar/mock-server package #32

Merged
merged 4 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/slow-schools-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@scalar/mock-server": patch
"@scalar/cli": patch
---

feat: new @scalar/mock-server package
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@
"bump": "CI=true pnpm run test && pnpm changeset version",
"build": "pnpm -r build"
},
"dependencies": {},
"devDependencies": {
"turbo": "^1.12.3",
"vitest": "^1.2.2",
"@changesets/cli": "^2.27.1",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6"
"prettier-plugin-tailwindcss": "^0.5.6",
"turbo": "^1.12.4",
"vitest": "^1.2.2"
}
}
8 changes: 7 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"swagger",
"cli"
],
"repository": {
"type": "git",
"url": "https://github.com/scalar/cli.git",
"directory": "packages/cli"
},
"engines": {
"node": ">=18"
},
Expand All @@ -32,9 +37,10 @@
"@hono/node-server": "^1.7.0",
"@parcel/watcher": "^2.4.0",
"@scalar/api-reference": "^1.16.1",
"@scalar/mock-server": "workspace:*",
"@seriousme/openapi-schema-validator": "^2.1.6",
"commander": "^12.0.0",
"hono": "^3.12.12",
"hono": "^4.0.2",
"kleur": "^4.1.5",
"openapi-types": "^12.1.3",
"prettier": "^3.2.5",
Expand Down
190 changes: 102 additions & 88 deletions packages/cli/src/commands/mock/MockCommand.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { serve } from '@hono/node-server'
import { getExampleFromSchema } from '@scalar/api-reference'
import { createMockServer } from '@scalar/mock-server'
import { Command } from 'commander'
import { Hono } from 'hono'
import type { Context } from 'hono'
import kleur from 'kleur'
import type { OpenAPI } from 'openapi-types'

import {
getMethodColor,
getOperationByMethodAndPath,
loadOpenApiFile,
useGivenFileOrConfiguration,
watchFile,
Expand All @@ -25,110 +24,125 @@ export function MockCommand() {
fileArgument: string,
{ watch, port }: { watch?: boolean; port?: number },
) => {
// Server instance
let server: ReturnType<typeof serve> = null

// Configuration
const file = useGivenFileOrConfiguration(fileArgument)

let schema = (await loadOpenApiFile(file))
// Load OpenAPI file
let specification = (await loadOpenApiFile(file))
.specification as OpenAPI.Document

// watch file for changes
// Watch OpenAPI file for changes
if (watch) {
await watchFile(file, async () => {
console.log(
kleur.bold().white('[INFO]'),
kleur.grey('Mock Server was updated.'),
)
schema = (await loadOpenApiFile(file))
specification = (await loadOpenApiFile(file))
.specification as OpenAPI.Document
})
}

console.log(kleur.bold().white('Available Paths'))
console.log()

if (
schema?.paths === undefined ||
Object.keys(schema?.paths).length === 0
) {
console.log(
kleur.bold().yellow('[WARN]'),
kleur.grey('Couldn’t find any paths in the OpenAPI file.'),
)
}

// loop through all paths
for (const path in schema?.paths ?? []) {
// loop through all methods
for (const method in schema.paths?.[path]) {
console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${path}`)}`,
)
}
server.close()
server = await bootServer({
openapi: specification,
port,
})
})
}

console.log()
// Show all paths from the specification
printAvailablePaths(specification)

const app = new Hono()

app.all('/*', (c) => {
const { method, path } = c.req

const operation = getOperationByMethodAndPath(schema, method, path)

console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${path}`)}`,
`${kleur.grey('→')} ${
operation?.operationId
? kleur.white(operation.operationId)
: kleur.red('[ERROR] 404 Not Found')
}`,
)

if (!operation) {
return c.text('Not found', 404)
}

// if (!operation) {
// return c.text('Method not allowed', 405)
// }

const jsonResponseConfiguration =
operation.responses['200'].content['application/json']

const response = jsonResponseConfiguration.example
? jsonResponseConfiguration.example
: jsonResponseConfiguration.schema
? getExampleFromSchema(jsonResponseConfiguration.schema, {
emptyString: '…',
})
: null

return c.json(response)
// Listen for requests
server = await bootServer({
openapi: specification,
port,
})
},
)

serve(
{
fetch: app.fetch,
port: port ?? 3000,
},
(info) => {
console.log(
`${kleur.bold().green('➜ Mock Server')} ${kleur.white(
'listening on',
)} ${kleur.cyan(`http://localhost:${info.port}`)}`,
)
console.log()
},
return cmd
}

async function bootServer({
openapi,
port,
}: {
openapi: OpenAPI.Document
port?: number
}) {
const app = await createMockServer({
openapi,
onRequest,
})

return serve(
{
fetch: app.fetch,
port: port ?? 3000,
},
(info) => {
console.log(
`${kleur.bold().green('➜ Mock Server')} ${kleur.white(
'listening on',
)} ${kleur.cyan(`http://localhost:${info.port}`)}`,
)
console.log()
},
)
}

return cmd
function printAvailablePaths(specification: OpenAPI.Document) {
console.log(kleur.bold().white('Available Paths'))
console.log()

if (
specification?.paths === undefined ||
Object.keys(specification?.paths).length === 0
) {
console.log(
kleur.bold().yellow('[WARN]'),
kleur.grey('Couldn’t find any paths in the OpenAPI file.'),
)
}

// loop through all paths
for (const path in specification?.paths ?? []) {
// loop through all methods
for (const method in specification.paths?.[path]) {
console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${path}`)}`,
)
}
}

console.log()
}

function onRequest({
context,
operation,
}: {
context: Context
operation: OpenAPI.Operation
}) {
const { method } = context.req
console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${context.req.path}`)}`,
`${kleur.grey('→')} ${
operation?.operationId
? kleur.white(operation.operationId)
: kleur.red('[ERROR] 404 Not Found')
}`,
)
}
4 changes: 4 additions & 0 deletions packages/cli/src/utils/readFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import fs from 'node:fs'

export function readFile(file: string) {
try {
if (fs.existsSync(file) === false) {
return undefined
}

return fs.readFileSync(file, 'utf8')
} catch (err) {
console.error(err)
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/utils/useGivenFileOrConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function useGivenFileOrConfiguration(file?: string) {

// Try to load the configuration
try {
const configuration = JSON.parse(readFile(CONFIG_FILE))
// check if file exists

const content = readFile(CONFIG_FILE)
const configuration = JSON.parse(content)

if (configuration?.reference?.file) {
return configuration.reference.file
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"module": "esnext",
"allowSyntheticDefaultImports": true,
"outDir": "dist",
"sourceMap": false
"sourceMap": false,
"paths": {
"@scalar/mock-server": ["../packages/mock-server/*"]
}
},
"include": ["src/**/*", "./package.json", "tests/**/*"]
"include": ["src/**/*", "./package.json", "tests/**/*", "vite.config.ts"]
}
13 changes: 13 additions & 0 deletions packages/cli/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import path from 'node:path'
import { defineConfig } from 'vitest/config'

export default defineConfig({
resolve: {
alias: [
{
find: '@scalar/mock-server',
replacement: path.resolve(__dirname, '../mock-server/src/index.ts'),
},
],
},
})
62 changes: 62 additions & 0 deletions packages/mock-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Scalar Mock Server

A server returning fake data based on an OpenAPI specification

## Installation

```bash
npm install @scalar/mock-server
```

## Usage

```ts
import { serve } from '@hono/node-server'
import { createMockServer } from '@scalar/mock-server'

// Your OpenAPI specification
const openapi = {
openapi: '3.1.0',
info: {
title: 'Hello World',
version: '1.0.0',
},
paths: {
'/foobar': {
get: {
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
example: {
foo: 'bar',
},
},
},
},
},
},
},
},
}

// Create the mocked routes
const app = await createMockServer({
openapi,
onRequest({ context, operation }) {
console.log(context.req.method, context.req.path)
},
})

// Start the server
serve(
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(`Listening on http://localhost:${info.port}`)
},
)
```
Loading