Skip to content

Commit edc00d0

Browse files
committed
feat: file format and main config
1 parent d727001 commit edc00d0

File tree

6 files changed

+160
-65
lines changed

6 files changed

+160
-65
lines changed

README.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,81 @@
11
# outdoc
2-
Generate OpenAPI document from local testing
2+
[![Version](http://img.shields.io/npm/v/outdoc.svg)](https://www.npmjs.org/package/outdoc)
3+
[![npm download][download-image]][download-url]
4+
5+
[download-image]: https://img.shields.io/npm/dm/outdoc.svg?style=flat-square
6+
[download-url]: https://npmjs.org/package/outdoc
7+
8+
Auto generate OpenAPI document from local testing
9+
10+
## Installation
11+
12+
```bash
13+
$ npm install outdoc -D
14+
```
15+
16+
## Configuration
17+
Check if the field `main` in your package.json pointing to the file where the node server exported.
18+
19+
If not, add `output.main` pointing to the file, e.g.:
20+
21+
```json
22+
{
23+
...
24+
"outdoc": {
25+
"main": "./server/index.js"
26+
}
27+
}
28+
```
29+
30+
## Usage
31+
32+
```bash
33+
$ npx outdoc [test command] [options]
34+
```
35+
36+
Usually it could be:
37+
38+
```bash
39+
$ npx outdoc npm test -t project-name
40+
```
41+
And it will generate an api.yaml in your root folder by default
42+
43+
## Options
44+
45+
```
46+
-o, --output file path of the generated doc, format supports json and yaml, default: api.yaml
47+
-t, --title <string> title of the api document, default: API Document
48+
-v, --version <string> version of the api document, default: 1.0.0
49+
-e, --email <string> contact information
50+
-h, --help display help for command
51+
```
52+
53+
54+
## Typescript projects
55+
Add `output.main` in your package.json pointing to the file where the nodejs server exported, e.g.:
56+
57+
```json
58+
{
59+
...
60+
"outdoc": {
61+
"main": "./src/app.ts"
62+
}
63+
}
64+
65+
```
66+
afte that you can run the script as usual
67+
68+
69+
## Behind the screen
70+
71+
Outdoc make use the node module `async_hooks` to understand all the HTTP request to the nodejs server.
72+
73+
When you running the e2e testing, you are like telling outdoc "ok, this is a 200 request if you pass in such request body", "and this endpoint can return 403 with a code 100", etc. Outdoc will generate the api doc based on that.
74+
75+
So if you wanna have a completed API doc, you need
76+
1. Writing e2e test covering all the cases of your API.
77+
2. Running e2e test with real http request, that means testing tools like supertest is a fit, but fastify.inject won't work.
78+
79+
## License
80+
81+
MIT

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
{
22
"name": "outdoc",
3-
"version": "0.0.19",
3+
"version": "0.1.0",
44
"description": "Auto-generate OpenAPI document for Node.js service from the local testing",
55
"main": "lib/index.js",
6-
"typings": "lib/index.d.ts",
76
"bin": {
87
"outdoc": "./bin/outdoc.js"
98
},
@@ -49,7 +48,7 @@
4948
"dependencies": {
5049
"commander": "^9.4.0",
5150
"content-type": "^1.0.4",
52-
"js-yaml": "^4.1.0",
51+
"json-to-pretty-yaml": "^1.2.2",
5352
"qs": "^6.11.0"
5453
}
5554
}

src/APIGenerator.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { mkdir, writeFile } from 'fs/promises';
22
import path from 'path';
3+
import YAML from 'json-to-pretty-yaml';
34

45
import type { OpenAPIV3_1 } from 'openapi-types';
56
import type { API_URL, APICollectorInterface } from './APICollector.interface';
@@ -11,7 +12,7 @@ type GenDocOpts = {
1112
email?: string
1213
}
1314

14-
const SUPPORTED_FORMAT = ['json', 'yaml'];
15+
const SUPPORTED_FORMAT = ['.json', '.yaml'];
1516

1617
export default class APIGenerator {
1718
public static async generate (
@@ -37,10 +38,23 @@ export default class APIGenerator {
3738
paths
3839
};
3940

40-
const output = opts.output || 'api.json';
41+
const output = opts.output || 'api.yaml';
4142
const fileFormat = path.extname(output);
42-
// TODO: change format based on fileFormat
43+
if (!SUPPORTED_FORMAT.includes(fileFormat)) {
44+
throw new Error(`${fileFormat} file not supported`)
45+
}
46+
4347
await mkdir(path.dirname(output), { recursive: true });
44-
await writeFile(output, JSON.stringify(apiDoc, null, 2));
48+
switch (fileFormat) {
49+
case ".json": {
50+
await writeFile(output, JSON.stringify(apiDoc, null, 2));
51+
break
52+
}
53+
case ".yaml": {
54+
const yamlData = YAML.stringify(apiDoc)
55+
await writeFile(output, yamlData);
56+
break
57+
}
58+
}
4559
}
4660
}

src/RequestHook.ts

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -31,71 +31,71 @@ export default class RequestHook {
3131
public static getInjectedCodes (): string {
3232
return `
3333
const async_hooks = require('async_hooks')
34-
const asyncHook = async_hooks.createHook({ init });
35-
asyncHook.enable();
36-
37-
function init(asyncId, type, triggerAsyncId, resource) {
38-
if (type === "TickObject" && resource.args) {
39-
const className = resource.args?.[0]?.constructor.name;
34+
const asyncHook = async_hooks.createHook({
35+
init: (asyncId, type, triggerAsyncId, resource) => {
36+
if (type === "TickObject" && resource.args) {
37+
const className = resource.args?.[0]?.constructor.name;
4038
41-
// Response body data
42-
if (className === "Object") {
43-
const arg = resource.args[0]
44-
if (arg?.stream?.server && arg?.state?.buffered) {
45-
const dataItem = arg.state.buffered.find(item => {
46-
if (!item) return false
47-
return ['buffer', 'utf-8'].includes(item.encoding)
48-
})
49-
if (dataItem) {
50-
const chunk = dataItem.encoding === 'buffer'
51-
? dataItem.chunk.toString()
52-
: dataItem.chunk
53-
const res = {
54-
asyncId,
55-
data: {
56-
encoding: dataItem.encoding,
57-
chunk
39+
// Response body data
40+
if (className === "Object") {
41+
const arg = resource.args[0]
42+
if (arg?.stream?.server && arg?.state?.buffered) {
43+
const dataItem = arg.state.buffered.find(item => {
44+
if (!item) return false
45+
return ['buffer', 'utf-8'].includes(item.encoding)
46+
})
47+
if (dataItem) {
48+
const chunk = dataItem.encoding === 'buffer'
49+
? dataItem.chunk.toString()
50+
: dataItem.chunk
51+
const res = {
52+
asyncId,
53+
data: {
54+
encoding: dataItem.encoding,
55+
chunk
56+
}
5857
}
58+
console.log("${PREFIX_RESPONSE_BODY_DATA}" + JSON.stringify(res))
5959
}
60-
console.log("${PREFIX_RESPONSE_BODY_DATA}" + JSON.stringify(res))
6160
}
6261
}
63-
}
6462
65-
// Server response
66-
if (className === "ServerResponse") {
67-
const arg = resource.args[0];
68-
const res = {
69-
triggerAsyncId,
70-
data: {
71-
_header: arg._header,
72-
statusCode: arg.statusCode,
73-
statusMessage: arg.statusMessage,
74-
req: {
75-
rawHeaders: arg.req.rawHeaders,
76-
url: arg.req.url,
77-
method: arg.req.method,
78-
params: arg.req.params,
79-
query: arg.req.query,
80-
baseUrl: arg.req.baseUrl,
81-
originalUrl: arg.req.originalUrl,
82-
body: arg.req.body
63+
// Server response
64+
if (className === "ServerResponse") {
65+
const arg = resource.args[0];
66+
const res = {
67+
triggerAsyncId,
68+
data: {
69+
_header: arg._header,
70+
statusCode: arg.statusCode,
71+
statusMessage: arg.statusMessage,
72+
req: {
73+
rawHeaders: arg.req.rawHeaders,
74+
url: arg.req.url,
75+
method: arg.req.method,
76+
params: arg.req.params,
77+
query: arg.req.query,
78+
baseUrl: arg.req.baseUrl,
79+
originalUrl: arg.req.originalUrl,
80+
body: arg.req.body
81+
}
8382
}
8483
}
85-
}
86-
if (arg.req._readableState?.buffer?.head?.data) {
87-
res.data.req._readableState = {
88-
buffer: {
89-
head: {
90-
data: arg.req._readableState.buffer.head.data.toString()
84+
if (arg.req._readableState?.buffer?.head?.data) {
85+
res.data.req._readableState = {
86+
buffer: {
87+
head: {
88+
data: arg.req._readableState.buffer.head.data.toString()
89+
}
9190
}
9291
}
9392
}
93+
console.log("${PREFIX_SERVER_RESPONSE}" + JSON.stringify(res))
9494
}
95-
console.log("${PREFIX_SERVER_RESPONSE}" + JSON.stringify(res))
9695
}
9796
}
98-
}
97+
});
98+
asyncHook.enable();
9999
`;
100100
}
101101

src/decs.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module "json-to-pretty-yaml"

src/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@ export async function runner (
2828
const projectCWD = process.cwd();
2929
const packageJSONStr = await fsPromises.readFile(projectCWD + '/package.json', 'utf8')
3030
const packageJSON = JSON.parse(packageJSONStr)
31-
// TODO
32-
// if (!json || !json.main) throw new Error('TODO error')
33-
const mainFilePath = `${projectCWD}/${packageJSON.main}`
31+
const mainFilePath = packageJSON?.outdoc?.main || packageJSON?.main
32+
if (!mainFilePath) throw new Error('Please define main or outdoc.main in package.json')
33+
34+
const mainFileAbsolutePath = `${projectCWD}/${mainFilePath}`
3435
const apiCollector = new APICollector();
3536
const requestHook = new RequestHook(apiCollector);
3637

3738
const injectedCodes = RequestHook.getInjectedCodes()
38-
await fsPromises.copyFile(mainFilePath, projectCWD + "/outdoc_tmp_file")
39-
await fsPromises.writeFile(mainFilePath, injectedCodes, { flag: "a" })
39+
await fsPromises.copyFile(mainFileAbsolutePath, projectCWD + "/outdoc_tmp_file")
40+
await fsPromises.writeFile(mainFileAbsolutePath, injectedCodes, { flag: "a" })
41+
await fsPromises.appendFile(mainFileAbsolutePath, "// @ts-nocheck")
4042

4143
const childProcess = spawn(args[0], args.slice(1), { stdio: ["inherit", "pipe", "inherit"] });
4244

@@ -77,7 +79,7 @@ export async function runner (
7779
})
7880

7981
childProcess.on('close', async (code) => {
80-
await fsPromises.copyFile(projectCWD + "/outdoc_tmp_file", mainFilePath)
82+
await fsPromises.copyFile(projectCWD + "/outdoc_tmp_file", mainFileAbsolutePath)
8183
await fsPromises.rm(projectCWD + "/outdoc_tmp_file")
8284

8385
if (code === 0) {

0 commit comments

Comments
 (0)