From 9757f14b59f24b90b2e1668f676a87859fd35fc1 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sat, 16 Jan 2021 15:30:50 +0100 Subject: [PATCH 01/41] init changes --- bin/swagger-jsdoc.js | 14 ++++++-------- index.js | 2 +- package.json | 3 ++- src/lib.js | 4 ++-- src/specification.js | 12 ++++++------ src/utils.js | 20 +++++++++++--------- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/bin/swagger-jsdoc.js b/bin/swagger-jsdoc.js index 9a44a425..0d63a065 100755 --- a/bin/swagger-jsdoc.js +++ b/bin/swagger-jsdoc.js @@ -1,15 +1,13 @@ #!/usr/bin/env node -const fs = require('fs'); -const path = require('path'); -const program = require('commander'); +import fs from 'fs'; +import { extname } from 'path'; +import program from 'commander'; -const pkg = require('../package.json'); -const swaggerJsdoc = require('..'); -const { loadDefinition } = require('../src/utils'); +import swaggerJsdoc from '../src/lib.js'; +import { loadDefinition } from '../src/utils.js'; program - .version(pkg.version) .usage('[options] ') .option( '-d, --definition ', @@ -77,7 +75,7 @@ fs.writeFileSync( swaggerJsdoc({ swaggerDefinition, apis: program.args, - format: path.extname(output || ''), + format: extname(output || ''), }), null, 2 diff --git a/index.js b/index.js index 768706a7..671631f8 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./src/lib'); +export * from './src/lib'; diff --git a/package.json b/package.json index d14b4129..b97a62f9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test:js": "jest --verbose", "test": "run-p test:* -cn" }, - "main": "index.js", + "type": "module", + "exports": "./index.js", "bin": { "swagger-jsdoc": "./bin/swagger-jsdoc.js" }, diff --git a/src/lib.js b/src/lib.js index 8098870f..a5e26be3 100644 --- a/src/lib.js +++ b/src/lib.js @@ -1,4 +1,4 @@ -const { build } = require('./specification'); +import { build } from './specification.js'; /** * Generates the specification. @@ -10,7 +10,7 @@ const { build } = require('./specification'); * @param {array} options.apis * @returns {object} Output specification */ -module.exports = (options) => { +export default (options) => { if (!options) { throw new Error(`Missing or invalid input: 'options' is required`); } diff --git a/src/specification.js b/src/specification.js index eb8f4648..dd8f13db 100644 --- a/src/specification.js +++ b/src/specification.js @@ -1,14 +1,14 @@ -const doctrine = require('doctrine'); -const parser = require('swagger-parser'); -const YAML = require('yaml'); +import doctrine from 'doctrine'; +import parser from 'swagger-parser'; +import YAML from 'yaml'; -const { +import { hasEmptyProperty, convertGlobPaths, extractAnnotations, extractYamlFromJsDoc, isTagPresentInTags, -} = require('./utils'); +} from './utils.js'; /** * Prepare the swagger/openapi specification object. @@ -279,4 +279,4 @@ function build(options) { return finalize(specification, options); } -module.exports = { prepare, build, organize, finalize, format }; +export { prepare, build, organize, finalize, format }; diff --git a/src/utils.js b/src/utils.js index 6de35c1d..e6b8cf33 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ -const fs = require('fs'); -const path = require('path'); -const glob = require('glob'); +import fs from 'fs'; +import path from 'path'; +import glob from 'glob'; /** * Converts an array of globs to full paths @@ -129,9 +129,11 @@ function loadDefinition(defPath, swaggerDefinition) { return loader(); } -module.exports.convertGlobPaths = convertGlobPaths; -module.exports.hasEmptyProperty = hasEmptyProperty; -module.exports.extractYamlFromJsDoc = extractYamlFromJsDoc; -module.exports.extractAnnotations = extractAnnotations; -module.exports.isTagPresentInTags = isTagPresentInTags; -module.exports.loadDefinition = loadDefinition; +export { + convertGlobPaths, + hasEmptyProperty, + extractAnnotations, + extractYamlFromJsDoc, + isTagPresentInTags, + loadDefinition, +}; From 2cc36acd7ee4f78922b8c7ab289fe323001fa18e Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Thu, 21 Jan 2021 17:11:34 +0100 Subject: [PATCH 02/41] update loaders --- .nvmrc | 2 +- bin/swagger-jsdoc.js | 88 +++++++++++++++++++------------------------- src/utils.js | 40 ++++++++++++-------- 3 files changed, 63 insertions(+), 67 deletions(-) diff --git a/.nvmrc b/.nvmrc index e1fcd1ea..518633e1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/erbium +lts/fermium diff --git a/bin/swagger-jsdoc.js b/bin/swagger-jsdoc.js index 0d63a065..76c04020 100755 --- a/bin/swagger-jsdoc.js +++ b/bin/swagger-jsdoc.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -import fs from 'fs'; import { extname } from 'path'; +import { writeFile } from 'fs/promises'; import program from 'commander'; import swaggerJsdoc from '../src/lib.js'; @@ -18,7 +18,6 @@ program if (!process.argv.slice(2).length) { program.help(); - process.exit(); } const { definition, output } = program; @@ -26,60 +25,49 @@ const { definition, output } = program; if (!definition) { console.log('Definition file is required.'); program.help(); - process.exit(); } -let swaggerDefinition; - try { - swaggerDefinition = loadDefinition( - definition, - fs.readFileSync(definition, 'utf-8') - ); -} catch (error) { - console.log( - `Error while loading definition file '${definition}':\n${error.message}` - ); - process.exit(); -} + const swaggerDefinition = await loadDefinition(definition); -// Check for info object in the definition. -if (!('info' in swaggerDefinition)) { - console.log('Definition file should contain an info object!'); - console.log('More at http://swagger.io/specification/#infoObject'); - process.exit(); -} + if (!('info' in swaggerDefinition)) { + console.log('Definition file should contain an info object!'); + console.log('More at http://swagger.io/specification/#infoObject'); + process.exit(); + } -// Check for title and version properties in the info object. -if ( - !('title' in swaggerDefinition.info) || - !('version' in swaggerDefinition.info) -) { - console.log('The title and version properties are required!'); - console.log('More at http://swagger.io/specification/#infoObject'); - process.exit(); -} + if ( + !('title' in swaggerDefinition.info) || + !('version' in swaggerDefinition.info) + ) { + console.log('The title and version properties are required!'); + console.log('More at http://swagger.io/specification/#infoObject'); + process.exit(); + } -// Continue only if arguments provided. -if (!program.args.length) { - console.log('You must provide sources for reading API files.'); - console.log( - 'Either add filenames as arguments, or add an "apis" key in your configuration.' + if (!program.args.length) { + console.log('Input files are required!'); + console.log( + 'More at https://github.com/Surnet/swagger-jsdoc/blob/master/docs/CLI.md#input-files' + ); + process.exit(); + } + + await writeFile( + output || 'swagger.json', + JSON.stringify( + swaggerJsdoc({ + swaggerDefinition, + apis: program.args, + format: extname(output || ''), + }), + null, + 2 + ) ); + + console.log('Swagger specification is ready.'); +} catch (error) { + console.log(`Definition file error':\n${error.message}`); process.exit(); } - -fs.writeFileSync( - output || 'swagger.json', - JSON.stringify( - swaggerJsdoc({ - swaggerDefinition, - apis: program.args, - format: extname(output || ''), - }), - null, - 2 - ) -); - -console.log('Swagger specification is ready.'); diff --git a/src/utils.js b/src/utils.js index e6b8cf33..60ee2a1d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,8 @@ import fs from 'fs'; -import path from 'path'; +import { readFile } from 'fs/promises'; +import { extname, resolve } from 'path'; import glob from 'glob'; +import { parse } from 'yaml'; /** * Converts an array of globs to full paths @@ -51,7 +53,7 @@ function extractYamlFromJsDoc(jsDocComment) { */ function extractAnnotations(filePath, encoding = 'utf8') { const fileContent = fs.readFileSync(filePath, { encoding }); - const ext = path.extname(filePath); + const ext = extname(filePath); const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm; const csDocRegex = /###([\s\S]*?)###/gm; const yaml = []; @@ -99,28 +101,34 @@ function isTagPresentInTags(tag, tags) { } /** - * Get an object of the definition file configuration. - * @param {string} defPath - * @param {object} swaggerDefinition + * @param {string} definitionPath */ -function loadDefinition(defPath, swaggerDefinition) { - const resolvedPath = path.resolve(defPath); - const extName = path.extname(resolvedPath); - - // eslint-disable-next-line - const loadJs = () => require(resolvedPath); - const loadJson = () => JSON.parse(swaggerDefinition); - // eslint-disable-next-line - const loadYaml = () => require('yaml').parse(swaggerDefinition); +function loadDefinition(definitionPath) { + const loadESMJs = async () => { + try { + const m = await import(resolve(definitionPath)); + return m.default; + } catch (error) { + throw error; + } + }; + const loadJson = async () => { + const fileContents = await readFile(definitionPath); + return JSON.parse(fileContents); + }; + const loadYaml = async () => { + const fileContents = await readFile(definitionPath); + return parse(String(fileContents)); + }; const LOADERS = { - '.js': loadJs, + '.js': loadESMJs, '.json': loadJson, '.yml': loadYaml, '.yaml': loadYaml, }; - const loader = LOADERS[extName]; + const loader = LOADERS[extname(definitionPath)]; if (loader === undefined) { throw new Error('Definition file should be .js, .json, .yml or .yaml'); From 07bd5f2bbb2ead271966bde03a74a1db4141f589 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sat, 23 Jan 2021 16:24:06 +0100 Subject: [PATCH 03/41] update example app --- examples/app/app.js | 16 ++++++++-------- examples/app/app.spec.js | 6 +++--- examples/app/routes.js | 2 +- examples/app/routes2.js | 2 +- index.js | 3 ++- package.json | 6 +++++- src/lib.js | 4 +++- src/utils.js | 13 ++++++------- 8 files changed, 29 insertions(+), 23 deletions(-) diff --git a/examples/app/app.js b/examples/app/app.js index a64585bf..257e5110 100644 --- a/examples/app/app.js +++ b/examples/app/app.js @@ -2,11 +2,11 @@ /* eslint import/no-extraneous-dependencies: 0 */ // Dependencies -const express = require('express'); -const bodyParser = require('body-parser'); -const routes = require('./routes'); -const routes2 = require('./routes2'); -const swaggerJsdoc = require('../..'); +import express from 'express'; +import bodyParser from 'body-parser'; +import { setup as setupRoute1 } from './routes.js'; +import { setup as setupRoute2 } from './routes2.js'; +import swaggerJsdoc from '../../src/lib.js'; const PORT = process.env.PORT || 3000; @@ -53,8 +53,8 @@ app.get('/api-docs.json', (req, res) => { }); // Set up the routes -routes.setup(app); -routes2.setup(app); +setupRoute1(app); +setupRoute2(app); // Start the server const server = app.listen(PORT, () => { @@ -64,4 +64,4 @@ const server = app.listen(PORT, () => { console.log('Example app listening at http://%s:%s', host, port); }); -module.exports = { app, server }; +export { app, server }; diff --git a/examples/app/app.spec.js b/examples/app/app.spec.js index 92d1d6dd..0d09240f 100644 --- a/examples/app/app.spec.js +++ b/examples/app/app.spec.js @@ -1,6 +1,6 @@ -const request = require('supertest'); -const { app, server } = require('./app'); -const swaggerSpec = require('./swagger-spec.json'); +import request from 'supertest'; +import { app, server } from './app.js'; +import swaggerSpec from './swagger-spec.json'; describe('Example application written in swagger specification (v2)', () => { it('should be healthy', async () => { diff --git a/examples/app/routes.js b/examples/app/routes.js index 9883a459..5a125d35 100644 --- a/examples/app/routes.js +++ b/examples/app/routes.js @@ -1,7 +1,7 @@ /* istanbul ignore file */ // Sets up the routes. -module.exports.setup = (app) => { +export const setup = (app) => { /** * @swagger * /: diff --git a/examples/app/routes2.js b/examples/app/routes2.js index 7592b344..6f096bed 100644 --- a/examples/app/routes2.js +++ b/examples/app/routes2.js @@ -1,6 +1,6 @@ /* istanbul ignore file */ -module.exports.setup = (app) => { +export const setup = (app) => { /** * @swagger * /hello: diff --git a/index.js b/index.js index 671631f8..e6504456 100644 --- a/index.js +++ b/index.js @@ -1 +1,2 @@ -export * from './src/lib'; +import lib from './src/lib.js'; +export default lib; diff --git a/package.json b/package.json index b97a62f9..f4d61bfa 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start": "node examples/app/app.js", "lint": "eslint .", "test:lint": "eslint .", - "test:js": "jest --verbose", + "test:js": "NODE_OPTIONS=--experimental-vm-modules jest", "test": "run-p test:* -cn" }, "type": "module", @@ -17,6 +17,10 @@ "bin": { "swagger-jsdoc": "./bin/swagger-jsdoc.js" }, + "jest": { + "verbose": true, + "transform": {} + }, "dependencies": { "commander": "6.2.0", "doctrine": "3.0.0", diff --git a/src/lib.js b/src/lib.js index a5e26be3..d89d1a1c 100644 --- a/src/lib.js +++ b/src/lib.js @@ -10,7 +10,7 @@ import { build } from './specification.js'; * @param {array} options.apis * @returns {object} Output specification */ -export default (options) => { +const lib = (options) => { if (!options) { throw new Error(`Missing or invalid input: 'options' is required`); } @@ -29,3 +29,5 @@ export default (options) => { return build(options); }; + +export default lib; diff --git a/src/utils.js b/src/utils.js index 60ee2a1d..ffda3502 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,7 @@ -import fs from 'fs'; -import { readFile } from 'fs/promises'; +import { promises as fsp, readFileSync } from 'fs'; import { extname, resolve } from 'path'; import glob from 'glob'; -import { parse } from 'yaml'; +import yaml from 'yaml'; /** * Converts an array of globs to full paths @@ -52,7 +51,7 @@ function extractYamlFromJsDoc(jsDocComment) { * @returns {{jsdoc: array, yaml: array}} JSDoc comments and Yaml files */ function extractAnnotations(filePath, encoding = 'utf8') { - const fileContent = fs.readFileSync(filePath, { encoding }); + const fileContent = readFileSync(filePath, { encoding }); const ext = extname(filePath); const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm; const csDocRegex = /###([\s\S]*?)###/gm; @@ -113,12 +112,12 @@ function loadDefinition(definitionPath) { } }; const loadJson = async () => { - const fileContents = await readFile(definitionPath); + const fileContents = await fsp.readFile(definitionPath); return JSON.parse(fileContents); }; const loadYaml = async () => { - const fileContents = await readFile(definitionPath); - return parse(String(fileContents)); + const fileContents = await fsp.readFile(definitionPath); + return yaml.parse(String(fileContents)); }; const LOADERS = { From 7f1bfe6cfb828796a2c5d427251f7802a118b76a Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sat, 23 Jan 2021 16:33:49 +0100 Subject: [PATCH 04/41] update import statements in cli spec --- test/__snapshots__/cli.spec.js.snap | 3 --- test/cli.spec.js | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/test/__snapshots__/cli.spec.js.snap b/test/__snapshots__/cli.spec.js.snap index fb4a19cf..aa27cc0c 100644 --- a/test/__snapshots__/cli.spec.js.snap +++ b/test/__snapshots__/cli.spec.js.snap @@ -4,7 +4,6 @@ exports[`CLI module help menu is default fallback when no arguments 1`] = ` "Usage: swagger-jsdoc [options] Options: - -V, --version output the version number -d, --definition Input swagger definition. -o, --output [swaggerSpec.json] Output swagger specification. -h, --help display help for command @@ -15,7 +14,6 @@ exports[`CLI module help menu works 1`] = ` "Usage: swagger-jsdoc [options] Options: - -V, --version output the version number -d, --definition Input swagger definition. -o, --output [swaggerSpec.json] Output swagger specification. -h, --help display help for command @@ -60,7 +58,6 @@ exports[`CLI module should require a definition file 1`] = ` Usage: swagger-jsdoc [options] Options: - -V, --version output the version number -d, --definition Input swagger definition. -o, --output [swaggerSpec.json] Output swagger specification. -h, --help display help for command diff --git a/test/cli.spec.js b/test/cli.spec.js index 5d61052f..28507657 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -1,6 +1,6 @@ -const fs = require('fs'); -const { promisify } = require('util'); -const { exec } = require('child_process'); +import fs from 'fs'; +import { promisify } from 'util'; +import { exec } from 'child_process'; const sh = promisify(exec); const dir = process.env.PWD; From 4c096a7354a3231dbfdf8b4066e8fdee0626267c Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 24 Jan 2021 10:30:42 +0100 Subject: [PATCH 05/41] move test files to fixtures --- .npmignore | 1 - .prettierignore | 4 +- src/utils.js | 8 +-- test/__snapshots__/cli.spec.js.snap | 4 +- test/cli.spec.js | 51 +++++++++++-------- test/{files/v2 => fixtures}/api_definition.js | 0 .../v2 => fixtures}/api_definition.json | 0 .../v2 => fixtures}/api_definition.yaml | 0 .../v2 => fixtures}/deprecated_routes.js | 0 test/fixtures/empty-file.js | 0 .../{empty-file.coffee => empty.coffee} | 0 test/fixtures/empty_export.js | 3 ++ .../empty_file.js} | 0 .../v2/external => fixtures/merge}/one.yml | 0 .../v2/external => fixtures/merge}/two.yml | 0 .../{files/v2 => fixtures}/swaggerObject.json | 0 .../v2 => fixtures}/wrong-yaml-identation.js | 0 .../v2 => fixtures}/wrong_definition.js | 0 test/{files/v2 => fixtures}/wrong_syntax.json | 0 test/{files/v2 => fixtures}/wrong_syntax.yaml | 0 test/lib.spec.js | 2 +- test/specification.spec.js | 2 +- test/utils.spec.js | 6 +-- 23 files changed, 42 insertions(+), 39 deletions(-) rename test/{files/v2 => fixtures}/api_definition.js (100%) rename test/{files/v2 => fixtures}/api_definition.json (100%) rename test/{files/v2 => fixtures}/api_definition.yaml (100%) rename test/{files/v2 => fixtures}/deprecated_routes.js (100%) delete mode 100644 test/fixtures/empty-file.js rename test/fixtures/{empty-file.coffee => empty.coffee} (100%) create mode 100644 test/fixtures/empty_export.js rename test/{files/v2/empty_definition.js => fixtures/empty_file.js} (100%) rename test/{files/v2/external => fixtures/merge}/one.yml (100%) rename test/{files/v2/external => fixtures/merge}/two.yml (100%) rename test/{files/v2 => fixtures}/swaggerObject.json (100%) rename test/{files/v2 => fixtures}/wrong-yaml-identation.js (100%) rename test/{files/v2 => fixtures}/wrong_definition.js (100%) rename test/{files/v2 => fixtures}/wrong_syntax.json (100%) rename test/{files/v2 => fixtures}/wrong_syntax.yaml (100%) diff --git a/.npmignore b/.npmignore index ce0fb4e6..e34561fa 100644 --- a/.npmignore +++ b/.npmignore @@ -7,7 +7,6 @@ .c9 .nvm .eslintrc.js -external.jsdoc examples/ docs/ jsdoc/ diff --git a/.prettierignore b/.prettierignore index 6f445362..9c129625 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ -test/files/v2/wrong_syntax.json -test/files/v2/wrong_syntax.yaml \ No newline at end of file +test/fixtures/wrong_syntax.json +test/fixtures/wrong_syntax.yaml \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index ffda3502..e63c1994 100644 --- a/src/utils.js +++ b/src/utils.js @@ -104,12 +104,8 @@ function isTagPresentInTags(tag, tags) { */ function loadDefinition(definitionPath) { const loadESMJs = async () => { - try { - const m = await import(resolve(definitionPath)); - return m.default; - } catch (error) { - throw error; - } + const m = await import(resolve(definitionPath)); + return m.default | {}; }; const loadJson = async () => { const fileContents = await fsp.readFile(definitionPath); diff --git a/test/__snapshots__/cli.spec.js.snap b/test/__snapshots__/cli.spec.js.snap index aa27cc0c..1fe41ffb 100644 --- a/test/__snapshots__/cli.spec.js.snap +++ b/test/__snapshots__/cli.spec.js.snap @@ -21,13 +21,13 @@ Options: `; exports[`CLI module should reject definition file with invalid JSON syntax 1`] = ` -"Error while loading definition file 'test/files/v2/wrong_syntax.json': +"Error while loading definition file 'test/fixtures/wrong_syntax.json': Unexpected token t in JSON at position 18 " `; exports[`CLI module should reject definition file with invalid YAML syntax 1`] = ` -"Error while loading definition file 'test/files/v2/wrong_syntax.yaml': +"Error while loading definition file 'test/fixtures/wrong_syntax.yaml': The !!! tag handle is non-default and was not declared. at line 2, column 3: !!!title: Hello World diff --git a/test/cli.spec.js b/test/cli.spec.js index 28507657..fa205c0d 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -1,4 +1,4 @@ -import fs from 'fs'; +import fs from 'fs/promises'; import { promisify } from 'util'; import { exec } from 'child_process'; @@ -22,13 +22,18 @@ describe('CLI module', () => { expect(result.stdout).toMatchSnapshot(); }); + it('should require a default export', async () => { + const result = await sh(`${bin} -d test/fixtures/empty_file.js`); + expect(result.stdout).toMatchSnapshot(); + }); + it('should require an info object in the definition', async () => { - const result = await sh(`${bin} -d test/files/v2/empty_definition.js`); + const result = await sh(`${bin} -d test/fixtures/empty_export.js`); expect(result.stdout).toMatchSnapshot(); }); it('should require title and version in the info object', async () => { - const result = await sh(`${bin} -d test/files/v2/wrong_definition.js`); + const result = await sh(`${bin} -d test/fixtures/wrong_definition.js`); expect(result.stdout).toMatchSnapshot(); }); @@ -42,16 +47,16 @@ describe('CLI module', () => { `${bin} -d examples/app/swaggerDefinition.js examples/app/routes.js` ); expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = fs.statSync('swagger.json'); + const specification = await fs.stat('swagger.json'); expect(specification.nlink).not.toBe(0); }); it('should create swagger.json by default when the API input is from definition file', async () => { const result = await sh( - `${bin} -d test/files/v2/api_definition.js examples/app/routes.js` + `${bin} -d test/fixtures/api_definition.js examples/app/routes.js` ); expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = fs.statSync('swagger.json'); + const specification = await fs.stat('swagger.json'); expect(specification.nlink).not.toBe(0); }); @@ -60,7 +65,7 @@ describe('CLI module', () => { `${bin} -d examples/app/swaggerDefinition.js -o customSpec.json examples/app/routes.js` ); expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = fs.statSync('customSpec.json'); + const specification = await fs.stat('customSpec.json'); expect(specification.nlink).not.toBe(0); }); @@ -69,50 +74,50 @@ describe('CLI module', () => { `${bin} -d examples/app/swaggerDefinition.js -o customSpec.yaml examples/app/routes.js` ); expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = fs.statSync('customSpec.yaml'); + const specification = await fs.stat('customSpec.yaml'); expect(specification.nlink).not.toBe(0); }); it('should allow a JavaScript definition file', async () => { const result = await sh( - `${bin} -d test/files/v2/api_definition.js examples/app/routes.js` + `${bin} -d test/fixtures/api_definition.js examples/app/routes.js` ); expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = fs.statSync('swagger.json'); + const specification = await fs.stat('swagger.json'); expect(specification.nlink).not.toBe(0); }); it('should allow a JSON definition file', async () => { const result = await sh( - `${bin} -d test/files/v2/api_definition.json examples/app/routes.js` + `${bin} -d test/fixtures/api_definition.json examples/app/routes.js` ); expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = fs.statSync('swagger.json'); + const specification = await fs.stat('swagger.json'); expect(specification.nlink).not.toBe(0); }); it('should allow a YAML definition file', async () => { const result = await sh( - `${bin} -d test/files/v2/api_definition.yaml examples/app/routes.js` + `${bin} -d test/fixtures/api_definition.yaml examples/app/routes.js` ); expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = fs.statSync('swagger.json'); + const specification = await fs.stat('swagger.json'); expect(specification.nlink).not.toBe(0); }); it('should reject definition file with invalid YAML syntax', async () => { - const result = await sh(`${bin} -d test/files/v2/wrong_syntax.yaml`); + const result = await sh(`${bin} -d test/fixtures/wrong_syntax.yaml`); expect(result.stdout).toMatchSnapshot(); }); it('should reject definition file with invalid JSON syntax', async () => { - const result = await sh(`${bin} -d test/files/v2/wrong_syntax.json`); + const result = await sh(`${bin} -d test/fixtures/wrong_syntax.json`); expect(result.stdout).toMatchSnapshot(); }); it('should report YAML documents with errors', async () => { const result = await sh( - `${bin} -d examples/app/swaggerDefinition.js test/files/v2/wrong-yaml-identation.js` + `${bin} -d examples/app/swaggerDefinition.js test/fixtures/wrong-yaml-identation.js` ); expect(result.stdout).toContain( @@ -121,10 +126,12 @@ describe('CLI module', () => { expect(result.stderr).toMatchSnapshot(); }); - afterAll(() => { - fs.unlinkSync(`${dir}/swagger.json`); - fs.unlinkSync(`${dir}/customSpec.json`); - fs.unlinkSync(`${dir}/customSpec.yaml`); - fs.unlinkSync(`${dir}/customSpec.yml`); + afterAll(async () => { + await Promise.all([ + fs.unlink(`${dir}/swagger.json`), + fs.unlink(`${dir}/customSpec.json`), + fs.unlink(`${dir}/customSpec.yaml`), + fs.unlink(`${dir}/customSpec.yml`), + ]); }); }); diff --git a/test/files/v2/api_definition.js b/test/fixtures/api_definition.js similarity index 100% rename from test/files/v2/api_definition.js rename to test/fixtures/api_definition.js diff --git a/test/files/v2/api_definition.json b/test/fixtures/api_definition.json similarity index 100% rename from test/files/v2/api_definition.json rename to test/fixtures/api_definition.json diff --git a/test/files/v2/api_definition.yaml b/test/fixtures/api_definition.yaml similarity index 100% rename from test/files/v2/api_definition.yaml rename to test/fixtures/api_definition.yaml diff --git a/test/files/v2/deprecated_routes.js b/test/fixtures/deprecated_routes.js similarity index 100% rename from test/files/v2/deprecated_routes.js rename to test/fixtures/deprecated_routes.js diff --git a/test/fixtures/empty-file.js b/test/fixtures/empty-file.js deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/empty-file.coffee b/test/fixtures/empty.coffee similarity index 100% rename from test/fixtures/empty-file.coffee rename to test/fixtures/empty.coffee diff --git a/test/fixtures/empty_export.js b/test/fixtures/empty_export.js new file mode 100644 index 00000000..ca261f18 --- /dev/null +++ b/test/fixtures/empty_export.js @@ -0,0 +1,3 @@ +/* istanbul ignore file */ + +export default {}; diff --git a/test/files/v2/empty_definition.js b/test/fixtures/empty_file.js similarity index 100% rename from test/files/v2/empty_definition.js rename to test/fixtures/empty_file.js diff --git a/test/files/v2/external/one.yml b/test/fixtures/merge/one.yml similarity index 100% rename from test/files/v2/external/one.yml rename to test/fixtures/merge/one.yml diff --git a/test/files/v2/external/two.yml b/test/fixtures/merge/two.yml similarity index 100% rename from test/files/v2/external/two.yml rename to test/fixtures/merge/two.yml diff --git a/test/files/v2/swaggerObject.json b/test/fixtures/swaggerObject.json similarity index 100% rename from test/files/v2/swaggerObject.json rename to test/fixtures/swaggerObject.json diff --git a/test/files/v2/wrong-yaml-identation.js b/test/fixtures/wrong-yaml-identation.js similarity index 100% rename from test/files/v2/wrong-yaml-identation.js rename to test/fixtures/wrong-yaml-identation.js diff --git a/test/files/v2/wrong_definition.js b/test/fixtures/wrong_definition.js similarity index 100% rename from test/files/v2/wrong_definition.js rename to test/fixtures/wrong_definition.js diff --git a/test/files/v2/wrong_syntax.json b/test/fixtures/wrong_syntax.json similarity index 100% rename from test/files/v2/wrong_syntax.json rename to test/fixtures/wrong_syntax.json diff --git a/test/files/v2/wrong_syntax.yaml b/test/fixtures/wrong_syntax.yaml similarity index 100% rename from test/files/v2/wrong_syntax.yaml rename to test/fixtures/wrong_syntax.yaml diff --git a/test/lib.spec.js b/test/lib.spec.js index 292a8a02..805f98c0 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -84,7 +84,7 @@ describe('Main lib module', () => { it('should support multiple paths', () => { let testObject = { swaggerDefinition: {}, - apis: ['./**/*/external/*.yml'], + apis: ['./test/fixtures/merge/*.yml'], }; testObject = swaggerJsdoc(testObject); diff --git a/test/specification.spec.js b/test/specification.spec.js index 148881e2..f1067b0f 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -1,5 +1,5 @@ const specModule = require('../src/specification'); -const swaggerObject = require('./files/v2/swaggerObject.json'); +const swaggerObject = require('./fixtures/swaggerObject.json'); describe('Specification module', () => { describe('organize', () => { diff --git a/test/utils.spec.js b/test/utils.spec.js index 89848b37..b3b50a42 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -70,9 +70,7 @@ describe('Utilities module', () => { it('should return empty arrays from empty coffeescript files/syntax', () => { expect( - utils.extractAnnotations( - require.resolve('./fixtures/empty-file.coffee') - ) + utils.extractAnnotations(require.resolve('./fixtures/empty.coffee')) ).toEqual({ yaml: [], jsdoc: [], @@ -81,7 +79,7 @@ describe('Utilities module', () => { it('should extract jsdoc comments from empty javascript files/syntax', () => { expect( - utils.extractAnnotations(require.resolve('./fixtures/empty-file.js')) + utils.extractAnnotations(require.resolve('./fixtures/empty.js')) ).toEqual({ yaml: [], jsdoc: [], From f0e5470160c12dc18530f1b7bc75af1a7cb459ce Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 24 Jan 2021 10:37:12 +0100 Subject: [PATCH 06/41] move v3 test files to fixtures --- test/{files => fixtures}/v3/README.md | 0 test/{files => fixtures}/v3/callback/api.js | 0 test/{files => fixtures}/v3/callback/openapi.json | 0 test/{files => fixtures}/v3/links/api.js | 0 test/{files => fixtures}/v3/links/openapi.json | 0 test/{files => fixtures}/v3/petstore/api.js | 0 test/{files => fixtures}/v3/petstore/openapi.json | 0 test/lib.spec.js | 2 +- 8 files changed, 1 insertion(+), 1 deletion(-) rename test/{files => fixtures}/v3/README.md (100%) rename test/{files => fixtures}/v3/callback/api.js (100%) rename test/{files => fixtures}/v3/callback/openapi.json (100%) rename test/{files => fixtures}/v3/links/api.js (100%) rename test/{files => fixtures}/v3/links/openapi.json (100%) rename test/{files => fixtures}/v3/petstore/api.js (100%) rename test/{files => fixtures}/v3/petstore/openapi.json (100%) diff --git a/test/files/v3/README.md b/test/fixtures/v3/README.md similarity index 100% rename from test/files/v3/README.md rename to test/fixtures/v3/README.md diff --git a/test/files/v3/callback/api.js b/test/fixtures/v3/callback/api.js similarity index 100% rename from test/files/v3/callback/api.js rename to test/fixtures/v3/callback/api.js diff --git a/test/files/v3/callback/openapi.json b/test/fixtures/v3/callback/openapi.json similarity index 100% rename from test/files/v3/callback/openapi.json rename to test/fixtures/v3/callback/openapi.json diff --git a/test/files/v3/links/api.js b/test/fixtures/v3/links/api.js similarity index 100% rename from test/files/v3/links/api.js rename to test/fixtures/v3/links/api.js diff --git a/test/files/v3/links/openapi.json b/test/fixtures/v3/links/openapi.json similarity index 100% rename from test/files/v3/links/openapi.json rename to test/fixtures/v3/links/openapi.json diff --git a/test/files/v3/petstore/api.js b/test/fixtures/v3/petstore/api.js similarity index 100% rename from test/files/v3/petstore/api.js rename to test/fixtures/v3/petstore/api.js diff --git a/test/files/v3/petstore/openapi.json b/test/fixtures/v3/petstore/openapi.json similarity index 100% rename from test/files/v3/petstore/openapi.json rename to test/fixtures/v3/petstore/openapi.json diff --git a/test/lib.spec.js b/test/lib.spec.js index 805f98c0..7c548f56 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -177,7 +177,7 @@ describe('Main lib module', () => { officialExamples.forEach((example) => { it(`Example: ${example}`, () => { const title = `Sample specification testing ${example}`; - const examplePath = `${__dirname}/files/v3/${example}`; + const examplePath = `${__dirname}/fixtures/v3/${example}`; // eslint-disable-next-line const referenceSpecification = require(path.resolve( From 36c10a849cd51e16afa249b5edd3e47f72e6901e Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 24 Jan 2021 11:54:18 +0100 Subject: [PATCH 07/41] refactor existing tests for utils --- src/utils.js | 2 +- test/utils.spec.js | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/utils.js b/src/utils.js index e63c1994..4de2f17e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -132,7 +132,7 @@ function loadDefinition(definitionPath) { return loader(); } -export { +export default { convertGlobPaths, hasEmptyProperty, extractAnnotations, diff --git a/test/utils.spec.js b/test/utils.spec.js index b3b50a42..dc0db662 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -1,6 +1,9 @@ /* eslint no-unused-expressions: 0 */ +import utils from '../src/utils.js'; -const utils = require('../src/utils'); +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; +const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Utilities module', () => { describe('hasEmptyProperty', () => { @@ -22,7 +25,9 @@ describe('Utilities module', () => { describe('extractAnnotations', () => { it('should extract jsdoc comments by default', () => { expect( - utils.extractAnnotations(require.resolve('../examples/app/routes2.js')) + utils.extractAnnotations( + resolve(__dirname, '../examples/app/routes2.js') + ) ).toEqual({ yaml: [], jsdoc: [ @@ -34,7 +39,7 @@ describe('Utilities module', () => { it('should extract data from YAML files', () => { expect( utils.extractAnnotations( - require.resolve('../examples/app/parameters.yaml') + resolve(__dirname, '../examples/app/parameters.yaml') ) ).toEqual({ yaml: [ @@ -45,7 +50,7 @@ describe('Utilities module', () => { expect( utils.extractAnnotations( - require.resolve('../examples/app/parameters.yml') + resolve(__dirname, '../examples/app/parameters.yml') ) ).toEqual({ yaml: [ @@ -58,7 +63,7 @@ describe('Utilities module', () => { it('should extract jsdoc comments from coffeescript files/syntax', () => { expect( utils.extractAnnotations( - require.resolve('../examples/app/route.coffee') + resolve(__dirname, '../examples/app/route.coffee') ) ).toEqual({ yaml: [], @@ -70,7 +75,7 @@ describe('Utilities module', () => { it('should return empty arrays from empty coffeescript files/syntax', () => { expect( - utils.extractAnnotations(require.resolve('./fixtures/empty.coffee')) + utils.extractAnnotations(resolve(__dirname, './fixtures/empty.coffee')) ).toEqual({ yaml: [], jsdoc: [], @@ -79,7 +84,7 @@ describe('Utilities module', () => { it('should extract jsdoc comments from empty javascript files/syntax', () => { expect( - utils.extractAnnotations(require.resolve('./fixtures/empty.js')) + utils.extractAnnotations(resolve(__dirname, './fixtures/empty_file.js')) ).toEqual({ yaml: [], jsdoc: [], From 223dfcd61b14ead196fd2082ca953fcd87c81333 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 24 Jan 2021 12:07:26 +0100 Subject: [PATCH 08/41] add named exports to utils --- src/utils.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils.js b/src/utils.js index 4de2f17e..a0836107 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,7 +8,7 @@ import yaml from 'yaml'; * @param {array} globs - Array of globs and/or normal paths * @return {array} Array of fully-qualified paths */ -function convertGlobPaths(globs) { +export function convertGlobPaths(globs) { return globs .map((globString) => glob.sync(globString)) .reduce((previous, current) => previous.concat(current), []); @@ -19,7 +19,7 @@ function convertGlobPaths(globs) { * @param {object} obj - the object to check * @returns {boolean} */ -function hasEmptyProperty(obj) { +export function hasEmptyProperty(obj) { return Object.keys(obj) .map((key) => obj[key]) .every( @@ -34,7 +34,7 @@ function hasEmptyProperty(obj) { * @param {object} jsDocComment - Single item of JSDoc comments from doctrine.parse * @returns {array} YAML parts */ -function extractYamlFromJsDoc(jsDocComment) { +export function extractYamlFromJsDoc(jsDocComment) { const yamlParts = []; for (const tag of jsDocComment.tags) { @@ -50,7 +50,7 @@ function extractYamlFromJsDoc(jsDocComment) { * @param {string} filePath * @returns {{jsdoc: array, yaml: array}} JSDoc comments and Yaml files */ -function extractAnnotations(filePath, encoding = 'utf8') { +export function extractAnnotations(filePath, encoding = 'utf8') { const fileContent = readFileSync(filePath, { encoding }); const ext = extname(filePath); const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm; @@ -92,7 +92,7 @@ function extractAnnotations(filePath, encoding = 'utf8') { * @param {array} tags * @returns {boolean} */ -function isTagPresentInTags(tag, tags) { +export function isTagPresentInTags(tag, tags) { const match = tags.find((targetTag) => tag.name === targetTag.name); if (match) return true; @@ -102,7 +102,7 @@ function isTagPresentInTags(tag, tags) { /** * @param {string} definitionPath */ -function loadDefinition(definitionPath) { +export function loadDefinition(definitionPath) { const loadESMJs = async () => { const m = await import(resolve(definitionPath)); return m.default | {}; From 8f433ed4fa8a4b2f5ca564a9846de36e23bc7333 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 24 Jan 2021 12:35:51 +0100 Subject: [PATCH 09/41] refactor examples tests --- examples/extensions/extensions.spec.js | 8 ++++++-- .../yaml-anchors-aliases/yaml-anchors-aliases.spec.js | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/extensions/extensions.spec.js b/examples/extensions/extensions.spec.js index 4479cbae..8bb9e185 100644 --- a/examples/extensions/extensions.spec.js +++ b/examples/extensions/extensions.spec.js @@ -1,5 +1,9 @@ -const swaggerJsdoc = require('../..'); -const referenceSpecification = require('./reference-specification.json'); +import swaggerJsdoc from '../../src/lib.js'; +import { readFile } from 'fs/promises'; + +const referenceSpecification = JSON.parse( + await readFile(new URL('./reference-specification.json', import.meta.url)) +); describe('Example for using extensions', () => { it('should support x-webhooks', () => { diff --git a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js index 9cd0c3af..82af610f 100644 --- a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js +++ b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js @@ -1,5 +1,9 @@ -const swaggerJsdoc = require('../..'); -const referenceSpecification = require('./reference-specification.json'); +import swaggerJsdoc from '../../src/lib.js'; +import { readFile } from 'fs/promises'; + +const referenceSpecification = JSON.parse( + await readFile(new URL('./reference-specification.json', import.meta.url)) +); describe('Example for using anchors and aliases in YAML documents', () => { it('should handle references in a separate YAML file', () => { From 433f8ca78500df98b6e786dbf39b1be8f74aa514 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 24 Jan 2021 12:45:47 +0100 Subject: [PATCH 10/41] refactor examples tests --- examples/app/app.spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/app/app.spec.js b/examples/app/app.spec.js index 0d09240f..50879b7b 100644 --- a/examples/app/app.spec.js +++ b/examples/app/app.spec.js @@ -1,6 +1,10 @@ +import { readFile } from 'fs/promises'; import request from 'supertest'; import { app, server } from './app.js'; -import swaggerSpec from './swagger-spec.json'; + +const swaggerSpec = JSON.parse( + await readFile(new URL('./swagger-spec.json', import.meta.url)) +); describe('Example application written in swagger specification (v2)', () => { it('should be healthy', async () => { From 18d10f1f585fdc59e1f3354790cfccd6766063bb Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Mon, 25 Jan 2021 16:19:34 +0100 Subject: [PATCH 11/41] reading json files --- bin/swagger-jsdoc.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/swagger-jsdoc.js b/bin/swagger-jsdoc.js index 76c04020..b099a41d 100755 --- a/bin/swagger-jsdoc.js +++ b/bin/swagger-jsdoc.js @@ -1,13 +1,18 @@ #!/usr/bin/env node import { extname } from 'path'; -import { writeFile } from 'fs/promises'; +import { readFile, writeFile } from 'fs/promises'; import program from 'commander'; import swaggerJsdoc from '../src/lib.js'; import { loadDefinition } from '../src/utils.js'; +const pkg = JSON.parse( + await readFile(new URL('../package.json', import.meta.url)) +); + program + .version(pkg.version) .usage('[options] ') .option( '-d, --definition ', From de1a74794fc3330fc18d95cddd3eba44049b8bf2 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Mon, 25 Jan 2021 17:07:56 +0100 Subject: [PATCH 12/41] update existing test suites --- package.json | 16 +- src/specification.js | 14 +- test/__snapshots__/cli.spec.js.snap | 3 + test/fixtures/swaggerObject.json | 21 -- test/lib.spec.js | 19 +- test/specification.spec.js | 25 ++- yarn.lock | 326 +++++++++++++--------------- 7 files changed, 204 insertions(+), 220 deletions(-) delete mode 100644 test/fixtures/swaggerObject.json diff --git a/package.json b/package.json index f4d61bfa..f5cb2a03 100644 --- a/package.json +++ b/package.json @@ -30,20 +30,20 @@ }, "devDependencies": { "body-parser": "1.19.0", - "eslint": "7.14.0", + "eslint": "7.18.0", "eslint-config-airbnb-base": "14.2.1", "eslint-config-prettier": "6.15.0", "eslint-loader": "4.0.2", "eslint-plugin-import": "2.22.1", - "eslint-plugin-jest": "^24.1.0", - "eslint-plugin-prettier": "3.1.4", + "eslint-plugin-jest": "24.1.3", + "eslint-plugin-prettier": "3.3.1", "express": "4.17.1", - "husky": "4.3.0", - "jest": "^26.6.1", - "lint-staged": "10.5.2", + "husky": "4.3.8", + "jest": "26.6.3", + "lint-staged": "10.5.3", "npm-run-all": "4.1.5", - "prettier": "2.2.0", - "supertest": "6.0.1" + "prettier": "2.2.1", + "supertest": "6.1.2" }, "license": "MIT", "homepage": "https://github.com/Surnet/swagger-jsdoc", diff --git a/src/specification.js b/src/specification.js index dd8f13db..0554e6bd 100644 --- a/src/specification.js +++ b/src/specification.js @@ -16,7 +16,7 @@ import { * @param {object} definition - The `definition` or `swaggerDefinition` from options. * @returns {object} swaggerObject */ -function prepare(definition) { +export function prepare(definition) { let version; const swaggerObject = JSON.parse(JSON.stringify(definition)); const specificationTemplate = { @@ -59,7 +59,7 @@ function prepare(definition) { * @param {object} obj * @param {string} ext */ -function format(swaggerObject, ext) { +export function format(swaggerObject, ext) { if (ext === '.yml' || ext === '.yaml') { return YAML.stringify(swaggerObject); } @@ -72,7 +72,7 @@ function format(swaggerObject, ext) { * @param {object} swaggerObject * @returns {object} swaggerObject */ -function clean(swaggerObject) { +export function clean(swaggerObject) { for (const prop of [ 'definitions', 'responses', @@ -93,7 +93,7 @@ function clean(swaggerObject) { * @param {object} swaggerObject - Swagger object from parsing the api files. * @returns {object} The specification. */ -function finalize(swaggerObject, options) { +export function finalize(swaggerObject, options) { let specification = swaggerObject; parser.parse(swaggerObject, (err, api) => { @@ -114,7 +114,7 @@ function finalize(swaggerObject, options) { * @param {object} annotation * @param {string} property */ -function organize(swaggerObject, annotation, property) { +export function organize(swaggerObject, annotation, property) { // Root property on purpose. // @see https://github.com/OAI/OpenAPI-Specification/blob/master/proposals/002_Webhooks.md#proposed-solution if (property === 'x-webhooks') { @@ -170,7 +170,7 @@ function organize(swaggerObject, annotation, property) { * @param {object} options * @returns {object} swaggerObject */ -function build(options) { +export function build(options) { YAML.defaultOptions.keepCstNodes = true; // Get input definition and prepare the specification's skeleton @@ -279,4 +279,4 @@ function build(options) { return finalize(specification, options); } -export { prepare, build, organize, finalize, format }; +export default { prepare, build, organize, finalize, format }; diff --git a/test/__snapshots__/cli.spec.js.snap b/test/__snapshots__/cli.spec.js.snap index 1fe41ffb..99aefb4f 100644 --- a/test/__snapshots__/cli.spec.js.snap +++ b/test/__snapshots__/cli.spec.js.snap @@ -4,6 +4,7 @@ exports[`CLI module help menu is default fallback when no arguments 1`] = ` "Usage: swagger-jsdoc [options] Options: + -V, --version output the version number -d, --definition Input swagger definition. -o, --output [swaggerSpec.json] Output swagger specification. -h, --help display help for command @@ -14,6 +15,7 @@ exports[`CLI module help menu works 1`] = ` "Usage: swagger-jsdoc [options] Options: + -V, --version output the version number -d, --definition Input swagger definition. -o, --output [swaggerSpec.json] Output swagger specification. -h, --help display help for command @@ -58,6 +60,7 @@ exports[`CLI module should require a definition file 1`] = ` Usage: swagger-jsdoc [options] Options: + -V, --version output the version number -d, --definition Input swagger definition. -o, --output [swaggerSpec.json] Output swagger specification. -h, --help display help for command diff --git a/test/fixtures/swaggerObject.json b/test/fixtures/swaggerObject.json deleted file mode 100644 index d6bf24df..00000000 --- a/test/fixtures/swaggerObject.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "info": { - "title": "Hello World", - "version": "1.0.0", - "description": "A sample API" - }, - "host": "localhost:3000", - "basePath": "/", - "swagger": "2.0", - "schemes": [], - "consumes": [], - "produces": [], - "paths": {}, - "definitions": {}, - "responses": {}, - "parameters": {}, - "securityDefinitions": {}, - "security": {}, - "tags": [], - "externalDocs": {} -} diff --git a/test/lib.spec.js b/test/lib.spec.js index 7c548f56..520f3298 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -1,6 +1,10 @@ -const path = require('path'); +import { promises as fs } from 'fs'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { jest } from '@jest/globals'; -const swaggerJsdoc = require('../src/lib'); +import swaggerJsdoc from '../src/lib.js'; +const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Main lib module', () => { describe('General', () => { @@ -175,14 +179,15 @@ describe('Main lib module', () => { }); officialExamples.forEach((example) => { - it(`Example: ${example}`, () => { + it(`Example: ${example}`, async () => { const title = `Sample specification testing ${example}`; const examplePath = `${__dirname}/fixtures/v3/${example}`; - // eslint-disable-next-line - const referenceSpecification = require(path.resolve( - `${examplePath}/openapi.json` - )); + const referenceSpecification = JSON.parse( + await fs.readFile( + new URL(`${examplePath}/openapi.json`, import.meta.url) + ) + ); const definition = { openapi: '3.0.0', diff --git a/test/specification.spec.js b/test/specification.spec.js index f1067b0f..953423ed 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -1,5 +1,26 @@ -const specModule = require('../src/specification'); -const swaggerObject = require('./fixtures/swaggerObject.json'); +import specModule from '../src/specification.js'; + +const swaggerObject = { + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + host: 'localhost:3000', + basePath: '/', + swagger: '2.0', + schemes: [], + consumes: [], + produces: [], + paths: {}, + definitions: {}, + responses: {}, + parameters: {}, + securityDefinitions: {}, + security: {}, + tags: [], + externalDocs: {}, +}; describe('Specification module', () => { describe('organize', () => { diff --git a/yarn.lock b/yarn.lock index 89806695..1efb6f29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -310,10 +310,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@eslint/eslintrc@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" - integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== +"@eslint/eslintrc@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" + integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -322,7 +322,7 @@ ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -727,7 +727,7 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.2.0: +acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== @@ -755,7 +755,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -765,6 +765,16 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" + integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -777,17 +787,12 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: dependencies: type-fest "^0.11.0" -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -889,11 +894,6 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -985,11 +985,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -1015,15 +1010,6 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bl@>=4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" - integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1088,14 +1074,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -1569,11 +1547,6 @@ emittery@^0.7.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -1743,17 +1716,17 @@ eslint-plugin-import@2.22.1: resolve "^1.17.0" tsconfig-paths "^3.9.0" -eslint-plugin-jest@^24.1.0: +eslint-plugin-jest@24.1.3: version "24.1.3" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.1.3.tgz#fa3db864f06c5623ff43485ca6c0e8fc5fe8ba0c" integrity sha512-dNGGjzuEzCE3d5EPZQ/QGtmlMotqnYWD/QpCZ1UuZlrMAdhG5rldh0N0haCvhGnUkSeuORS5VNROwF9Hrgn3Lg== dependencies: "@typescript-eslint/experimental-utils" "^4.0.1" -eslint-plugin-prettier@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz#168ab43154e2ea57db992a2cd097c828171f75c2" - integrity sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg== +eslint-plugin-prettier@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7" + integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ== dependencies: prettier-linter-helpers "^1.0.0" @@ -1782,13 +1755,13 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@7.14.0: - version "7.14.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.14.0.tgz#2d2cac1d28174c510a97b377f122a5507958e344" - integrity sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA== +eslint@7.18.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67" + integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ== dependencies: "@babel/code-frame" "^7.0.0" - "@eslint/eslintrc" "^0.2.1" + "@eslint/eslintrc" "^0.3.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -1798,10 +1771,10 @@ eslint@7.14.0: eslint-scope "^5.1.1" eslint-utils "^2.1.0" eslint-visitor-keys "^2.0.0" - espree "^7.3.0" + espree "^7.3.1" esquery "^1.2.0" esutils "^2.0.2" - file-entry-cache "^5.0.1" + file-entry-cache "^6.0.0" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" globals "^12.1.0" @@ -1812,7 +1785,7 @@ eslint@7.14.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -1821,7 +1794,7 @@ eslint@7.14.0: semver "^7.2.1" strip-ansi "^6.0.0" strip-json-comments "^3.1.0" - table "^5.2.3" + table "^6.0.4" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -1834,6 +1807,15 @@ espree@^7.3.0: acorn-jsx "^5.2.0" eslint-visitor-keys "^1.3.0" +espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -2074,12 +2056,12 @@ figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== +file-entry-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" + integrity sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA== dependencies: - flat-cache "^2.0.1" + flat-cache "^3.0.4" fill-range@^4.0.0: version "4.0.0" @@ -2135,26 +2117,33 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - semver-regex "^2.0.0" + locate-path "^6.0.0" + path-exists "^4.0.0" -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== +find-versions@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" + integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ== dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" + semver-regex "^3.1.2" -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== for-in@^1.0.2: version "1.0.2" @@ -2467,18 +2456,18 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -husky@4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.0.tgz#0b2ec1d66424e9219d359e26a51c58ec5278f0de" - integrity sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA== +husky@4.3.8: + version "4.3.8" + resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" + integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow== dependencies: chalk "^4.0.0" ci-info "^2.0.0" compare-versions "^3.6.0" cosmiconfig "^7.0.0" - find-versions "^3.2.0" + find-versions "^4.0.0" opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" + pkg-dir "^5.0.0" please-upgrade-node "^3.2.0" slash "^3.0.0" which-pm-runs "^1.0.0" @@ -2490,11 +2479,6 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -2539,7 +2523,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2661,11 +2645,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -3200,7 +3179,7 @@ jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^26.6.1: +jest@26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== @@ -3279,6 +3258,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -3380,10 +3364,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@10.5.2: - version "10.5.2" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.2.tgz#acfaa0093af3262aee3130b2e22438941530bdd1" - integrity sha512-e8AYR1TDlzwB8VVd38Xu2lXDZf6BcshVqKVuBQThDJRaJLobqKnpbm4dkwJ2puypQNbLr9KF/9mfA649mAGvjA== +lint-staged@10.5.3: + version "10.5.3" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.3.tgz#c682838b3eadd4c864d1022da05daa0912fb1da5" + integrity sha512-TanwFfuqUBLufxCc3RUtFEkFraSPNR3WzWcGF39R3f2J7S9+iF9W0KTVLfSy09lYGmZS5NDCxjNvhGMSJyFCWg== dependencies: chalk "^4.1.0" cli-truncate "^2.1.0" @@ -3459,6 +3443,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -3474,7 +3465,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -3547,7 +3538,7 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@1.1.2, methods@^1.1.2, methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -3626,13 +3617,6 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@^0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3904,6 +3888,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -3918,6 +3909,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -4082,6 +4080,13 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-dir@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -4111,10 +4116,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.0.tgz#8a03c7777883b29b37fb2c4348c66a78e980418b" - integrity sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw== +prettier@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== pretty-format@^26.6.2: version "26.6.2" @@ -4245,7 +4250,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -4329,6 +4334,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -4382,14 +4392,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -4471,10 +4474,10 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== +semver-regex@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" + integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0: version "5.7.1" @@ -4589,15 +4592,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - slice-ansi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" @@ -4776,15 +4770,6 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" @@ -4835,13 +4820,6 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -strip-ansi@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -4874,7 +4852,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -superagent@6.1.0: +superagent@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== @@ -4891,13 +4869,13 @@ superagent@6.1.0: readable-stream "^3.6.0" semver "^7.3.2" -supertest@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.0.1.tgz#f6b54370de85c45d6557192c8d7df604ca2c9e18" - integrity sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g== +supertest@6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.2.tgz#1679c234b139eed93911c185b67f689348fc2453" + integrity sha512-hZ8bu3TebxCYQ40mF6/2ou58EEG5jxo1AbsE1vprqXo3emkmqbQMcQrF7acsQteOjYlkExSvYOAQ/feTE9n7uA== dependencies: - methods "1.1.2" - superagent "6.1.0" + methods "^1.1.2" + superagent "^6.1.0" supports-color@^5.3.0: version "5.5.0" @@ -4933,15 +4911,15 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== +table@^6.0.4: + version "6.0.7" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" + integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" + ajv "^7.0.2" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" terminal-link@^2.0.0: version "2.1.1" @@ -5340,13 +5318,6 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - ws@^7.2.3: version "7.4.0" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" @@ -5402,6 +5373,11 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + z-schema@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-4.2.3.tgz#85f7eea7e6d4fe59a483462a98f511bd78fe9882" From cb07714fde82463e0a4a5da51719157bbe08a515 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Tue, 26 Jan 2021 15:47:07 +0100 Subject: [PATCH 13/41] use createRequire() --- .eslintrc.js => .eslintrc.cjs | 0 examples/app/app.spec.js | 7 +++---- examples/extensions/extensions.spec.js | 7 +++---- .../yaml-anchors-aliases/yaml-anchors-aliases.spec.js | 7 +++---- test/lib.spec.js | 10 ++++------ 5 files changed, 13 insertions(+), 18 deletions(-) rename .eslintrc.js => .eslintrc.cjs (100%) diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/examples/app/app.spec.js b/examples/app/app.spec.js index 50879b7b..744fb918 100644 --- a/examples/app/app.spec.js +++ b/examples/app/app.spec.js @@ -1,10 +1,9 @@ -import { readFile } from 'fs/promises'; +import { createRequire } from 'module'; import request from 'supertest'; import { app, server } from './app.js'; -const swaggerSpec = JSON.parse( - await readFile(new URL('./swagger-spec.json', import.meta.url)) -); +const require = createRequire(import.meta.url); +const swaggerSpec = require('./swagger-spec.json'); describe('Example application written in swagger specification (v2)', () => { it('should be healthy', async () => { diff --git a/examples/extensions/extensions.spec.js b/examples/extensions/extensions.spec.js index 8bb9e185..eac8872d 100644 --- a/examples/extensions/extensions.spec.js +++ b/examples/extensions/extensions.spec.js @@ -1,9 +1,8 @@ +import { createRequire } from 'module'; import swaggerJsdoc from '../../src/lib.js'; -import { readFile } from 'fs/promises'; -const referenceSpecification = JSON.parse( - await readFile(new URL('./reference-specification.json', import.meta.url)) -); +const require = createRequire(import.meta.url); +const referenceSpecification = require('./reference-specification.json'); describe('Example for using extensions', () => { it('should support x-webhooks', () => { diff --git a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js index 82af610f..5b369b45 100644 --- a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js +++ b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js @@ -1,9 +1,8 @@ +import { createRequire } from 'module'; import swaggerJsdoc from '../../src/lib.js'; -import { readFile } from 'fs/promises'; -const referenceSpecification = JSON.parse( - await readFile(new URL('./reference-specification.json', import.meta.url)) -); +const require = createRequire(import.meta.url); +const referenceSpecification = require('./reference-specification.json'); describe('Example for using anchors and aliases in YAML documents', () => { it('should handle references in a separate YAML file', () => { diff --git a/test/lib.spec.js b/test/lib.spec.js index 520f3298..26945b37 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -1,9 +1,10 @@ -import { promises as fs } from 'fs'; import { dirname } from 'path'; +import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { jest } from '@jest/globals'; import swaggerJsdoc from '../src/lib.js'; +const require = createRequire(import.meta.url); const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Main lib module', () => { @@ -183,11 +184,8 @@ describe('Main lib module', () => { const title = `Sample specification testing ${example}`; const examplePath = `${__dirname}/fixtures/v3/${example}`; - const referenceSpecification = JSON.parse( - await fs.readFile( - new URL(`${examplePath}/openapi.json`, import.meta.url) - ) - ); + const referenceSpecification = require(`${examplePath}/openapi.json`, import.meta + .url); const definition = { openapi: '3.0.0', From 394c335434c732e84ce7066b1ff02a23746cf77e Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Tue, 26 Jan 2021 16:26:02 +0100 Subject: [PATCH 14/41] remove airbnb-base because no babel and transpilation --- .eslintrc.cjs | 10 ++- bin/swagger-jsdoc.js | 120 ++++++++++++++-------------- examples/app/app.js | 1 - package.json | 1 - src/specification.js | 2 - test/__snapshots__/cli.spec.js.snap | 6 ++ test/specification.spec.js | 18 ++--- yarn.lock | 27 +------ 8 files changed, 88 insertions(+), 97 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index bff9857c..b1ae70e2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,7 +1,15 @@ module.exports = { root: true, + env: { + es6: true, + node: true, + }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 11, + }, extends: [ - 'airbnb-base', + 'eslint:recommended', 'plugin:prettier/recommended', 'plugin:jest/recommended', ], diff --git a/bin/swagger-jsdoc.js b/bin/swagger-jsdoc.js index b099a41d..c40f4db4 100755 --- a/bin/swagger-jsdoc.js +++ b/bin/swagger-jsdoc.js @@ -1,78 +1,82 @@ #!/usr/bin/env node import { extname } from 'path'; -import { readFile, writeFile } from 'fs/promises'; +import { createRequire } from 'module'; +import { writeFile } from 'fs/promises'; import program from 'commander'; import swaggerJsdoc from '../src/lib.js'; import { loadDefinition } from '../src/utils.js'; -const pkg = JSON.parse( - await readFile(new URL('../package.json', import.meta.url)) -); +const require = createRequire(import.meta.url); +const pkg = require('../package.json'); -program - .version(pkg.version) - .usage('[options] ') - .option( - '-d, --definition ', - 'Input swagger definition.' - ) - .option('-o, --output [swaggerSpec.json]', 'Output swagger specification.') - .parse(process.argv); +const executeBinary = async () => { + program + .version(pkg.version) + .usage('[options] ') + .option( + '-d, --definition ', + 'Input swagger definition.' + ) + .option('-o, --output [swaggerSpec.json]', 'Output swagger specification.') + .parse(process.argv); -if (!process.argv.slice(2).length) { - program.help(); -} + if (!process.argv.slice(2).length) { + program.help(); + } -const { definition, output } = program; + const { definition, output } = program; -if (!definition) { - console.log('Definition file is required.'); - program.help(); -} + if (!definition) { + console.log('Definition file is required.'); + program.help(); + } -try { - const swaggerDefinition = await loadDefinition(definition); + try { + const swaggerDefinition = await loadDefinition(definition); - if (!('info' in swaggerDefinition)) { - console.log('Definition file should contain an info object!'); - console.log('More at http://swagger.io/specification/#infoObject'); - process.exit(); - } + if (!('info' in swaggerDefinition)) { + console.log('Definition file should contain an info object!'); + console.log('More at http://swagger.io/specification/#infoObject'); + process.exit(); + } - if ( - !('title' in swaggerDefinition.info) || - !('version' in swaggerDefinition.info) - ) { - console.log('The title and version properties are required!'); - console.log('More at http://swagger.io/specification/#infoObject'); - process.exit(); - } + if ( + !('title' in swaggerDefinition.info) || + !('version' in swaggerDefinition.info) + ) { + console.log('The title and version properties are required!'); + console.log('More at http://swagger.io/specification/#infoObject'); + process.exit(); + } + + if (!program.args.length) { + console.log('Input files are required!'); + console.log( + 'More at https://github.com/Surnet/swagger-jsdoc/blob/master/docs/CLI.md#input-files' + ); + process.exit(); + } - if (!program.args.length) { - console.log('Input files are required!'); - console.log( - 'More at https://github.com/Surnet/swagger-jsdoc/blob/master/docs/CLI.md#input-files' + await writeFile( + output || 'swagger.json', + JSON.stringify( + swaggerJsdoc({ + swaggerDefinition, + apis: program.args, + format: extname(output || ''), + }), + null, + 2 + ) ); + + console.log('Swagger specification is ready.'); + } catch (error) { + console.log(`Definition file error':\n${error.message}`); process.exit(); } +}; - await writeFile( - output || 'swagger.json', - JSON.stringify( - swaggerJsdoc({ - swaggerDefinition, - apis: program.args, - format: extname(output || ''), - }), - null, - 2 - ) - ); - - console.log('Swagger specification is ready.'); -} catch (error) { - console.log(`Definition file error':\n${error.message}`); - process.exit(); -} +executeBinary(); diff --git a/examples/app/app.js b/examples/app/app.js index 257e5110..dc69ad46 100644 --- a/examples/app/app.js +++ b/examples/app/app.js @@ -1,5 +1,4 @@ /* istanbul ignore file */ -/* eslint import/no-extraneous-dependencies: 0 */ // Dependencies import express from 'express'; diff --git a/package.json b/package.json index f5cb2a03..21306f84 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "devDependencies": { "body-parser": "1.19.0", "eslint": "7.18.0", - "eslint-config-airbnb-base": "14.2.1", "eslint-config-prettier": "6.15.0", "eslint-loader": "4.0.2", "eslint-plugin-import": "2.22.1", diff --git a/src/specification.js b/src/specification.js index 0554e6bd..92a5be6b 100644 --- a/src/specification.js +++ b/src/specification.js @@ -278,5 +278,3 @@ export function build(options) { return finalize(specification, options); } - -export default { prepare, build, organize, finalize, format }; diff --git a/test/__snapshots__/cli.spec.js.snap b/test/__snapshots__/cli.spec.js.snap index 99aefb4f..47d43bba 100644 --- a/test/__snapshots__/cli.spec.js.snap +++ b/test/__snapshots__/cli.spec.js.snap @@ -55,6 +55,12 @@ YAMLSemanticError: Implicit map keys need to be followed by map values at line 3 " `; +exports[`CLI module should require a default export 1`] = ` +"Definition file error': +Cannot use 'in' operator to search for 'info' in 0 +" +`; + exports[`CLI module should require a definition file 1`] = ` "Definition file is required. Usage: swagger-jsdoc [options] diff --git a/test/specification.spec.js b/test/specification.spec.js index 953423ed..cc2891be 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -1,4 +1,4 @@ -import specModule from '../src/specification.js'; +import { organize, format } from '../src/specification.js'; const swaggerObject = { info: { @@ -25,7 +25,7 @@ const swaggerObject = { describe('Specification module', () => { describe('organize', () => { it('should be a function', () => { - expect(typeof specModule.organize).toBe('function'); + expect(typeof organize).toBe('function'); }); it('should handle "definitions"', () => { @@ -44,7 +44,7 @@ describe('Specification module', () => { }, }, }; - specModule.organize(swaggerObject, annotation, 'definitions'); + organize(swaggerObject, annotation, 'definitions'); expect(swaggerObject.definitions).toEqual({ testDefinition: { required: ['username', 'password'], @@ -69,7 +69,7 @@ describe('Specification module', () => { }, }, }; - specModule.organize(swaggerObject, annotation, 'parameters'); + organize(swaggerObject, annotation, 'parameters'); expect(swaggerObject.parameters).toEqual({ testParameter: { name: 'limit', @@ -92,7 +92,7 @@ describe('Specification module', () => { }, }, }; - specModule.organize(swaggerObject, annotation, 'securityDefinitions'); + organize(swaggerObject, annotation, 'securityDefinitions'); expect(swaggerObject.securityDefinitions).toEqual({ basicAuth: { type: 'basic', @@ -110,7 +110,7 @@ describe('Specification module', () => { }, }, }; - specModule.organize(swaggerObject, annotation, 'responses'); + organize(swaggerObject, annotation, 'responses'); expect(swaggerObject.responses).toEqual({ IllegalInput: { description: 'Illegal input for operation.' }, }); @@ -119,12 +119,12 @@ describe('Specification module', () => { describe('format', () => { it('should not modify input object when no format specified', () => { - expect(specModule.format({ foo: 'bar' })).toEqual({ foo: 'bar' }); + expect(format({ foo: 'bar' })).toEqual({ foo: 'bar' }); }); it('should support yaml', () => { - expect(specModule.format({ foo: 'bar' }, '.yaml')).toEqual('foo: bar\n'); - expect(specModule.format({ foo: 'bar' }, '.yml')).toEqual('foo: bar\n'); + expect(format({ foo: 'bar' }, '.yaml')).toEqual('foo: bar\n'); + expect(format({ foo: 'bar' }, '.yml')).toEqual('foo: bar\n'); }); }); }); diff --git a/yarn.lock b/yarn.lock index 1efb6f29..6e7597e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1284,11 +1284,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -confusing-browser-globals@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" - integrity sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA== - contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" @@ -1583,7 +1578,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1: version "1.17.7" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== @@ -1654,15 +1649,6 @@ escodegen@^1.14.1: optionalDependencies: source-map "~0.6.1" -eslint-config-airbnb-base@14.2.1: - version "14.2.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" - integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== - dependencies: - confusing-browser-globals "^1.0.10" - object.assign "^4.1.2" - object.entries "^1.1.2" - eslint-config-prettier@6.15.0: version "6.15.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9" @@ -3778,7 +3764,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.1, object.assign@^4.1.2: +object.assign@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -3788,15 +3774,6 @@ object.assign@^4.1.1, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.entries@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" - integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - has "^1.0.3" - object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" From 470b8c9acfd2d28ce748479cbdc7f01f01c0d255 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Wed, 27 Jan 2021 17:42:11 +0100 Subject: [PATCH 15/41] start refactor definition loader --- src/utils.js | 31 ++++--- .../example.cjs} | 0 test/fixtures/swaggerDefinition/example.js | 10 ++ .../example.json} | 0 .../example.yaml} | 0 test/utils.spec.js | 92 +++++++++++++++---- 6 files changed, 100 insertions(+), 33 deletions(-) rename test/fixtures/{api_definition.js => swaggerDefinition/example.cjs} (100%) create mode 100644 test/fixtures/swaggerDefinition/example.js rename test/fixtures/{api_definition.json => swaggerDefinition/example.json} (100%) rename test/fixtures/{api_definition.yaml => swaggerDefinition/example.yaml} (100%) diff --git a/src/utils.js b/src/utils.js index a0836107..b5f5e668 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ import { promises as fsp, readFileSync } from 'fs'; -import { extname, resolve } from 'path'; +import { createRequire } from 'module'; +import { extname } from 'path'; import glob from 'glob'; import yaml from 'yaml'; @@ -102,11 +103,17 @@ export function isTagPresentInTags(tag, tags) { /** * @param {string} definitionPath */ -export function loadDefinition(definitionPath) { - const loadESMJs = async () => { - const m = await import(resolve(definitionPath)); +export async function loadDefinition(definitionPath) { + const loadESM = async () => { + const m = await import(definitionPath); + + console.log('m', m); return m.default | {}; }; + const loadCJS = () => { + const require = createRequire(import.meta.url); + return require(definitionPath); + }; const loadJson = async () => { const fileContents = await fsp.readFile(definitionPath); return JSON.parse(fileContents); @@ -117,7 +124,8 @@ export function loadDefinition(definitionPath) { }; const LOADERS = { - '.js': loadESMJs, + '.js': loadESM, + '.cjs': loadCJS, '.json': loadJson, '.yml': loadYaml, '.yaml': loadYaml, @@ -129,14 +137,7 @@ export function loadDefinition(definitionPath) { throw new Error('Definition file should be .js, .json, .yml or .yaml'); } - return loader(); -} + const result = await loader(); -export default { - convertGlobPaths, - hasEmptyProperty, - extractAnnotations, - extractYamlFromJsDoc, - isTagPresentInTags, - loadDefinition, -}; + return result; +} diff --git a/test/fixtures/api_definition.js b/test/fixtures/swaggerDefinition/example.cjs similarity index 100% rename from test/fixtures/api_definition.js rename to test/fixtures/swaggerDefinition/example.cjs diff --git a/test/fixtures/swaggerDefinition/example.js b/test/fixtures/swaggerDefinition/example.js new file mode 100644 index 00000000..70e26759 --- /dev/null +++ b/test/fixtures/swaggerDefinition/example.js @@ -0,0 +1,10 @@ +/* istanbul ignore file */ + +export default { + info: { + // API informations (required) + title: 'Hello World', // Title (required) + version: '1.0.0', // Version (required) + description: 'A sample API', // Description (optional) + }, +}; diff --git a/test/fixtures/api_definition.json b/test/fixtures/swaggerDefinition/example.json similarity index 100% rename from test/fixtures/api_definition.json rename to test/fixtures/swaggerDefinition/example.json diff --git a/test/fixtures/api_definition.yaml b/test/fixtures/swaggerDefinition/example.yaml similarity index 100% rename from test/fixtures/api_definition.yaml rename to test/fixtures/swaggerDefinition/example.yaml diff --git a/test/utils.spec.js b/test/utils.spec.js index dc0db662..206313e8 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -1,5 +1,9 @@ /* eslint no-unused-expressions: 0 */ -import utils from '../src/utils.js'; +import { + extractAnnotations, + hasEmptyProperty, + loadDefinition, +} from '../src/utils.js'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; @@ -14,20 +18,18 @@ describe('Utilities module', () => { const validB = { foo: ['¯_(ツ)_/¯'] }; const validC = { foo: '¯_(ツ)_/¯' }; - expect(utils.hasEmptyProperty(invalidA)).toBe(true); - expect(utils.hasEmptyProperty(invalidB)).toBe(true); - expect(utils.hasEmptyProperty(validA)).toBe(false); - expect(utils.hasEmptyProperty(validB)).toBe(false); - expect(utils.hasEmptyProperty(validC)).toBe(false); + expect(hasEmptyProperty(invalidA)).toBe(true); + expect(hasEmptyProperty(invalidB)).toBe(true); + expect(hasEmptyProperty(validA)).toBe(false); + expect(hasEmptyProperty(validB)).toBe(false); + expect(hasEmptyProperty(validC)).toBe(false); }); }); describe('extractAnnotations', () => { it('should extract jsdoc comments by default', () => { expect( - utils.extractAnnotations( - resolve(__dirname, '../examples/app/routes2.js') - ) + extractAnnotations(resolve(__dirname, '../examples/app/routes2.js')) ).toEqual({ yaml: [], jsdoc: [ @@ -38,7 +40,7 @@ describe('Utilities module', () => { it('should extract data from YAML files', () => { expect( - utils.extractAnnotations( + extractAnnotations( resolve(__dirname, '../examples/app/parameters.yaml') ) ).toEqual({ @@ -49,9 +51,7 @@ describe('Utilities module', () => { }); expect( - utils.extractAnnotations( - resolve(__dirname, '../examples/app/parameters.yml') - ) + extractAnnotations(resolve(__dirname, '../examples/app/parameters.yml')) ).toEqual({ yaml: [ 'parameters:\n username:\n name: username\n description: Username to use for login.\n in: formData\n required: true\n type: string\n', @@ -62,9 +62,7 @@ describe('Utilities module', () => { it('should extract jsdoc comments from coffeescript files/syntax', () => { expect( - utils.extractAnnotations( - resolve(__dirname, '../examples/app/route.coffee') - ) + extractAnnotations(resolve(__dirname, '../examples/app/route.coffee')) ).toEqual({ yaml: [], jsdoc: [ @@ -75,7 +73,7 @@ describe('Utilities module', () => { it('should return empty arrays from empty coffeescript files/syntax', () => { expect( - utils.extractAnnotations(resolve(__dirname, './fixtures/empty.coffee')) + extractAnnotations(resolve(__dirname, './fixtures/empty.coffee')) ).toEqual({ yaml: [], jsdoc: [], @@ -84,11 +82,69 @@ describe('Utilities module', () => { it('should extract jsdoc comments from empty javascript files/syntax', () => { expect( - utils.extractAnnotations(resolve(__dirname, './fixtures/empty_file.js')) + extractAnnotations(resolve(__dirname, './fixtures/empty_file.js')) ).toEqual({ yaml: [], jsdoc: [], }); }); }); + + describe('loadDefinition', () => { + const example = './fixtures/swaggerDefinition/example'; + + it('should throw on bad input', async () => { + await expect(loadDefinition('bad/path/to/nowhere')).rejects.toThrow( + 'Definition file should be .js, .json, .yml or .yaml' + ); + }); + + it('should support .json', async () => { + const def = resolve(__dirname, `${example}.json`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .yaml', async () => { + const def = resolve(__dirname, `${example}.yaml`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .cjs (commonjs)', async () => { + const def = resolve(__dirname, `${example}.cjs`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .js (ESM)', async () => { + const def = resolve(__dirname, `${example}.js`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + }); }); From dd0ff8a83f5c7a857c2a3cded65a425a238988f7 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Thu, 28 Jan 2021 16:55:58 +0100 Subject: [PATCH 16/41] feat: complete loadDefinition --- src/utils.js | 17 ++++++++++------- test/fixtures/swaggerDefinition/example.mjs | 10 ++++++++++ test/utils.spec.js | 20 ++++++++++++++++---- 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 test/fixtures/swaggerDefinition/example.mjs diff --git a/src/utils.js b/src/utils.js index b5f5e668..84d6f328 100644 --- a/src/utils.js +++ b/src/utils.js @@ -104,11 +104,9 @@ export function isTagPresentInTags(tag, tags) { * @param {string} definitionPath */ export async function loadDefinition(definitionPath) { - const loadESM = async () => { - const m = await import(definitionPath); - - console.log('m', m); - return m.default | {}; + const loadModule = async () => { + const esmodule = await import(definitionPath); + return esmodule.default; }; const loadCJS = () => { const require = createRequire(import.meta.url); @@ -124,7 +122,8 @@ export async function loadDefinition(definitionPath) { }; const LOADERS = { - '.js': loadESM, + '.js': loadModule, + '.mjs': loadModule, '.cjs': loadCJS, '.json': loadJson, '.yml': loadYaml, @@ -134,7 +133,11 @@ export async function loadDefinition(definitionPath) { const loader = LOADERS[extname(definitionPath)]; if (loader === undefined) { - throw new Error('Definition file should be .js, .json, .yml or .yaml'); + throw new Error( + `Definition file should be any of the following: ${Object.keys( + LOADERS + ).join(', ')}` + ); } const result = await loader(); diff --git a/test/fixtures/swaggerDefinition/example.mjs b/test/fixtures/swaggerDefinition/example.mjs new file mode 100644 index 00000000..70e26759 --- /dev/null +++ b/test/fixtures/swaggerDefinition/example.mjs @@ -0,0 +1,10 @@ +/* istanbul ignore file */ + +export default { + info: { + // API informations (required) + title: 'Hello World', // Title (required) + version: '1.0.0', // Version (required) + description: 'A sample API', // Description (optional) + }, +}; diff --git a/test/utils.spec.js b/test/utils.spec.js index 206313e8..8054257f 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -95,7 +95,7 @@ describe('Utilities module', () => { it('should throw on bad input', async () => { await expect(loadDefinition('bad/path/to/nowhere')).rejects.toThrow( - 'Definition file should be .js, .json, .yml or .yaml' + 'Definition file should be any of the following: .js, .mjs, .cjs, .json, .yml, .yaml' ); }); @@ -123,7 +123,19 @@ describe('Utilities module', () => { }); }); - it('should support .cjs (commonjs)', async () => { + it('should support .js', async () => { + const def = resolve(__dirname, `${example}.js`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .cjs', async () => { const def = resolve(__dirname, `${example}.cjs`); const result = await loadDefinition(def); expect(result).toEqual({ @@ -135,8 +147,8 @@ describe('Utilities module', () => { }); }); - it('should support .js (ESM)', async () => { - const def = resolve(__dirname, `${example}.js`); + it('should support .mjs', async () => { + const def = resolve(__dirname, `${example}.mjs`); const result = await loadDefinition(def); expect(result).toEqual({ info: { From f6ac1fea3971222ef6637e8c57ef4e3a8b3312ea Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 31 Jan 2021 10:40:47 +0100 Subject: [PATCH 17/41] start deleting the cli --- bin/swagger-jsdoc.js | 18 ++---------------- src/utils.js | 24 +++++++++++++++++++++++ test/cli.spec.js | 2 +- test/utils.spec.js | 45 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/bin/swagger-jsdoc.js b/bin/swagger-jsdoc.js index c40f4db4..ec496ccf 100755 --- a/bin/swagger-jsdoc.js +++ b/bin/swagger-jsdoc.js @@ -2,9 +2,10 @@ import { extname } from 'path'; import { createRequire } from 'module'; -import { writeFile } from 'fs/promises'; +import { promises as fs } from 'fs'; import program from 'commander'; +const { writeFile } = fs; import swaggerJsdoc from '../src/lib.js'; import { loadDefinition } from '../src/utils.js'; @@ -36,21 +37,6 @@ const executeBinary = async () => { try { const swaggerDefinition = await loadDefinition(definition); - if (!('info' in swaggerDefinition)) { - console.log('Definition file should contain an info object!'); - console.log('More at http://swagger.io/specification/#infoObject'); - process.exit(); - } - - if ( - !('title' in swaggerDefinition.info) || - !('version' in swaggerDefinition.info) - ) { - console.log('The title and version properties are required!'); - console.log('More at http://swagger.io/specification/#infoObject'); - process.exit(); - } - if (!program.args.length) { console.log('Input files are required!'); console.log( diff --git a/src/utils.js b/src/utils.js index 84d6f328..78140314 100644 --- a/src/utils.js +++ b/src/utils.js @@ -144,3 +144,27 @@ export async function loadDefinition(definitionPath) { return result; } + +/** + * @param {object} swaggerDefinition + */ +export function validateDefinition(swaggerDefinition) { + if (!swaggerDefinition) { + throw new Error('Swagger definition object is required'); + } + + if (!swaggerDefinition.info) { + throw new Error('Definition file should contain an info object!'); + } + + if ( + !('title' in swaggerDefinition.info) || + !('version' in swaggerDefinition.info) + ) { + throw new Error( + 'Definition info object requires title and version properties!' + ); + } + + return true; +} diff --git a/test/cli.spec.js b/test/cli.spec.js index fa205c0d..77c226c9 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -1,4 +1,4 @@ -import fs from 'fs/promises'; +import { promises as fs } from 'fs'; import { promisify } from 'util'; import { exec } from 'child_process'; diff --git a/test/utils.spec.js b/test/utils.spec.js index 8054257f..54a6bdc2 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -3,6 +3,7 @@ import { extractAnnotations, hasEmptyProperty, loadDefinition, + validateDefinition, } from '../src/utils.js'; import { dirname, resolve } from 'path'; @@ -159,4 +160,48 @@ describe('Utilities module', () => { }); }); }); + + describe('validateDefinition', () => { + it('should throw on bad input', () => { + expect(() => { + validateDefinition(); + }).toThrow('Swagger definition object is required'); + }); + + it(`should throw on missing 'info' property`, () => { + expect(() => { + validateDefinition({}); + }).toThrow('Definition file should contain an info object!'); + }); + + it(`should throw on missing 'title' and 'version' properties in the info object`, () => { + expect(() => { + validateDefinition({ info: {} }); + }).toThrow( + 'Definition info object requires title and version properties!' + ); + + expect(() => { + validateDefinition({ info: { title: '' } }); + }).toThrow( + 'Definition info object requires title and version properties!' + ); + + expect(() => { + validateDefinition({ info: { version: '' } }); + }).toThrow( + 'Definition info object requires title and version properties!' + ); + + expect(() => { + validateDefinition({ info: { version: '', title: '' } }); + }).not.toThrow(); + }); + + it('should return true on valid input', () => { + expect(validateDefinition({ info: { version: '', title: '' } })).toBe( + true + ); + }); + }); }); From f226343dbd4578e4e6607323b3ecaea3e7cd060f Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Mon, 1 Feb 2021 17:32:14 +0100 Subject: [PATCH 18/41] start moving the cli to examples --- bin/swagger-jsdoc.js | 68 -------------- docs/CLI.md | 61 ------------- docs/README.md | 1 - examples/cli/cli.js | 27 ++++++ examples/cli/cli.spec.js | 27 ++++++ package.json | 5 +- test/__snapshots__/cli.spec.js.snap | 92 ------------------- test/cli.spec.js | 137 ---------------------------- 8 files changed, 55 insertions(+), 363 deletions(-) delete mode 100755 bin/swagger-jsdoc.js delete mode 100644 docs/CLI.md create mode 100755 examples/cli/cli.js create mode 100644 examples/cli/cli.spec.js delete mode 100644 test/__snapshots__/cli.spec.js.snap delete mode 100644 test/cli.spec.js diff --git a/bin/swagger-jsdoc.js b/bin/swagger-jsdoc.js deleted file mode 100755 index ec496ccf..00000000 --- a/bin/swagger-jsdoc.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node - -import { extname } from 'path'; -import { createRequire } from 'module'; -import { promises as fs } from 'fs'; -import program from 'commander'; - -const { writeFile } = fs; -import swaggerJsdoc from '../src/lib.js'; -import { loadDefinition } from '../src/utils.js'; - -const require = createRequire(import.meta.url); -const pkg = require('../package.json'); - -const executeBinary = async () => { - program - .version(pkg.version) - .usage('[options] ') - .option( - '-d, --definition ', - 'Input swagger definition.' - ) - .option('-o, --output [swaggerSpec.json]', 'Output swagger specification.') - .parse(process.argv); - - if (!process.argv.slice(2).length) { - program.help(); - } - - const { definition, output } = program; - - if (!definition) { - console.log('Definition file is required.'); - program.help(); - } - - try { - const swaggerDefinition = await loadDefinition(definition); - - if (!program.args.length) { - console.log('Input files are required!'); - console.log( - 'More at https://github.com/Surnet/swagger-jsdoc/blob/master/docs/CLI.md#input-files' - ); - process.exit(); - } - - await writeFile( - output || 'swagger.json', - JSON.stringify( - swaggerJsdoc({ - swaggerDefinition, - apis: program.args, - format: extname(output || ''), - }), - null, - 2 - ) - ); - - console.log('Swagger specification is ready.'); - } catch (error) { - console.log(`Definition file error':\n${error.message}`); - process.exit(); - } -}; - -executeBinary(); diff --git a/docs/CLI.md b/docs/CLI.md deleted file mode 100644 index 5ced1eca..00000000 --- a/docs/CLI.md +++ /dev/null @@ -1,61 +0,0 @@ -# Command line interface - -The CLI is a thin wrapper around the library Node API. It's available with the same name of the package when installed globally: - -```bash -swagger-jsdoc -``` - -Or through the standard ways provided by your package manager: - -```bash -yarn swagger-jsdoc -``` - -## Usage - -Print the help menu: - -```bash -swagger-jsdoc -h -``` - -### Definition file - -Set with `--definition` (or `-d`) flag: - -```bash -swagger-jsdoc -d swaggerDefinition.js -``` - -Acceptable file extensions: `.js`, `.json`, `.yml`, `.yaml`. - -### Input files - -Set through arguments. - -One by one: - -```bash -swagger-jsdoc -d swaggerDefinition.js route1.js route2.js component1.yaml component2.yaml -``` - -Multiple with a pattern: - -```bash -swagger-jsdoc -d swaggerDefinition.js route*.js component*.yaml -``` - -[Glob patterns](https://github.com/isaacs/node-glob) are acceptable to match multiple files with same extension `*.js`, `*.php`, etc. or patterns selecting files in nested folders as `**/*.js`, `**/*.php`, etc. - -Paths are relative to the current working directory. - -### Output file (optional) - -The output is `swagger.json` by default, but can be changed: - -```bash -swagger-jsdoc -d swaggerDefinition.js route1.js -o my_spec.json -``` - -When output file extension is `.yaml` or `.yml`, the specification will be parsed and saved in YAML format. diff --git a/docs/README.md b/docs/README.md index 8a49f000..91416c4d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,6 @@ Quick-start: - [Getting started](./GETTING-STARTED.md) -- [CLI](./CLI.md) - [Examples](../examples) Before you submit an issue: diff --git a/examples/cli/cli.js b/examples/cli/cli.js new file mode 100755 index 00000000..f0c78984 --- /dev/null +++ b/examples/cli/cli.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import { promises as fs } from 'fs'; + +import swaggerJsdoc from '../../src/lib.js'; +import { loadDefinition } from '../../src/utils.js'; + +// @see https://nodejs.org/en/knowledge/command-line/how-to-parse-command-line-arguments/ +const args = process.argv.slice(2); + +// extract definition file: it's always only 1 +const definition = args.splice( + args.findIndex((i) => i === '--definition'), + 2 +)[1]; +const swaggerDefinition = await loadDefinition(definition); + +// remove --apis flag +args.splice(0, 1); + +// the rest of this example can be treated as the contents of the --apis +const apis = args; + +// call the node api +const spec = swaggerJsdoc({ swaggerDefinition, apis }); + +// and save the result to your preferred place and format +await fs.writeFile('swagger.json', JSON.stringify(spec, null, 2)); diff --git a/examples/cli/cli.spec.js b/examples/cli/cli.spec.js new file mode 100644 index 00000000..324095a8 --- /dev/null +++ b/examples/cli/cli.spec.js @@ -0,0 +1,27 @@ +import { promises as fs } from 'fs'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { promisify } from 'util'; +import { exec } from 'child_process'; + +const sh = promisify(exec); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const bin = `node ${__dirname}/cli.js`; + +describe('Example command line application', () => { + it('should require a definition file', async () => { + const result = await sh(`${bin} wrongDefinition`); + console.log('result', result); + }); + + it('should produce results matching reference specification', async () => { + const result = await sh( + `${bin} --definition examples/app/swaggerDefinition.js --apis examples/app/parameters.* examples/app/route*` + ); + console.log('result', result); + }); + + afterAll(async () => { + await Promise.all([fs.unlink(`${dir}/swagger.json`)]); + }); +}); diff --git a/package.json b/package.json index 21306f84..e7704c48 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "swagger-jsdoc", "description": "Generates swagger doc based on JSDoc", - "version": "6.0.1", + "version": "7.0.0", "engines": { "node": ">=12.0.0" }, @@ -14,9 +14,6 @@ }, "type": "module", "exports": "./index.js", - "bin": { - "swagger-jsdoc": "./bin/swagger-jsdoc.js" - }, "jest": { "verbose": true, "transform": {} diff --git a/test/__snapshots__/cli.spec.js.snap b/test/__snapshots__/cli.spec.js.snap deleted file mode 100644 index 47d43bba..00000000 --- a/test/__snapshots__/cli.spec.js.snap +++ /dev/null @@ -1,92 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CLI module help menu is default fallback when no arguments 1`] = ` -"Usage: swagger-jsdoc [options] - -Options: - -V, --version output the version number - -d, --definition Input swagger definition. - -o, --output [swaggerSpec.json] Output swagger specification. - -h, --help display help for command -" -`; - -exports[`CLI module help menu works 1`] = ` -"Usage: swagger-jsdoc [options] - -Options: - -V, --version output the version number - -d, --definition Input swagger definition. - -o, --output [swaggerSpec.json] Output swagger specification. - -h, --help display help for command -" -`; - -exports[`CLI module should reject definition file with invalid JSON syntax 1`] = ` -"Error while loading definition file 'test/fixtures/wrong_syntax.json': -Unexpected token t in JSON at position 18 -" -`; - -exports[`CLI module should reject definition file with invalid YAML syntax 1`] = ` -"Error while loading definition file 'test/fixtures/wrong_syntax.yaml': -The !!! tag handle is non-default and was not declared. at line 2, column 3: - - !!!title: Hello World - ^^^^^^^^^^^^^^^^^^^^^… - -" -`; - -exports[`CLI module should report YAML documents with errors 1`] = ` -"Here's the report: - - - YAMLSyntaxError: All collection items must start at the same column at line 1, column 1: - -/invalid_yaml: -^^^^^^^^^^^^^^… - -YAMLSemanticError: Implicit map keys need to be followed by map values at line 3, column 3: - - bar - ^^^ - -" -`; - -exports[`CLI module should require a default export 1`] = ` -"Definition file error': -Cannot use 'in' operator to search for 'info' in 0 -" -`; - -exports[`CLI module should require a definition file 1`] = ` -"Definition file is required. -Usage: swagger-jsdoc [options] - -Options: - -V, --version output the version number - -d, --definition Input swagger definition. - -o, --output [swaggerSpec.json] Output swagger specification. - -h, --help display help for command -" -`; - -exports[`CLI module should require an info object in the definition 1`] = ` -"Definition file should contain an info object! -More at http://swagger.io/specification/#infoObject -" -`; - -exports[`CLI module should require arguments with jsDoc data about an API 1`] = ` -"You must provide sources for reading API files. -Either add filenames as arguments, or add an \\"apis\\" key in your configuration. -" -`; - -exports[`CLI module should require title and version in the info object 1`] = ` -"The title and version properties are required! -More at http://swagger.io/specification/#infoObject -" -`; diff --git a/test/cli.spec.js b/test/cli.spec.js deleted file mode 100644 index 77c226c9..00000000 --- a/test/cli.spec.js +++ /dev/null @@ -1,137 +0,0 @@ -import { promises as fs } from 'fs'; -import { promisify } from 'util'; -import { exec } from 'child_process'; - -const sh = promisify(exec); -const dir = process.env.PWD; -const bin = `${dir}/bin/swagger-jsdoc.js`; - -describe('CLI module', () => { - it('help menu is default fallback when no arguments', async () => { - const result = await sh(`${bin}`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('help menu works', async () => { - const result = await sh(`${bin} -h`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should require a definition file', async () => { - const result = await sh(`${bin} wrongDefinition`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should require a default export', async () => { - const result = await sh(`${bin} -d test/fixtures/empty_file.js`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should require an info object in the definition', async () => { - const result = await sh(`${bin} -d test/fixtures/empty_export.js`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should require title and version in the info object', async () => { - const result = await sh(`${bin} -d test/fixtures/wrong_definition.js`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should require arguments with jsDoc data about an API', async () => { - const result = await sh(`${bin} -d examples/app/swaggerDefinition.js`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should create swagger.json by default when the API input is good', async () => { - const result = await sh( - `${bin} -d examples/app/swaggerDefinition.js examples/app/routes.js` - ); - expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = await fs.stat('swagger.json'); - expect(specification.nlink).not.toBe(0); - }); - - it('should create swagger.json by default when the API input is from definition file', async () => { - const result = await sh( - `${bin} -d test/fixtures/api_definition.js examples/app/routes.js` - ); - expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = await fs.stat('swagger.json'); - expect(specification.nlink).not.toBe(0); - }); - - it('should accept custom configuration for output specification', async () => { - const result = await sh( - `${bin} -d examples/app/swaggerDefinition.js -o customSpec.json examples/app/routes.js` - ); - expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = await fs.stat('customSpec.json'); - expect(specification.nlink).not.toBe(0); - }); - - it('should create a YAML swagger spec when a custom output configuration with a .yaml extension is used', async () => { - const result = await sh( - `${bin} -d examples/app/swaggerDefinition.js -o customSpec.yaml examples/app/routes.js` - ); - expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = await fs.stat('customSpec.yaml'); - expect(specification.nlink).not.toBe(0); - }); - - it('should allow a JavaScript definition file', async () => { - const result = await sh( - `${bin} -d test/fixtures/api_definition.js examples/app/routes.js` - ); - expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = await fs.stat('swagger.json'); - expect(specification.nlink).not.toBe(0); - }); - - it('should allow a JSON definition file', async () => { - const result = await sh( - `${bin} -d test/fixtures/api_definition.json examples/app/routes.js` - ); - expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = await fs.stat('swagger.json'); - expect(specification.nlink).not.toBe(0); - }); - - it('should allow a YAML definition file', async () => { - const result = await sh( - `${bin} -d test/fixtures/api_definition.yaml examples/app/routes.js` - ); - expect(result.stdout).toBe('Swagger specification is ready.\n'); - const specification = await fs.stat('swagger.json'); - expect(specification.nlink).not.toBe(0); - }); - - it('should reject definition file with invalid YAML syntax', async () => { - const result = await sh(`${bin} -d test/fixtures/wrong_syntax.yaml`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should reject definition file with invalid JSON syntax', async () => { - const result = await sh(`${bin} -d test/fixtures/wrong_syntax.json`); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should report YAML documents with errors', async () => { - const result = await sh( - `${bin} -d examples/app/swaggerDefinition.js test/fixtures/wrong-yaml-identation.js` - ); - - expect(result.stdout).toContain( - 'Not all input has been taken into account at your final specification.' - ); - expect(result.stderr).toMatchSnapshot(); - }); - - afterAll(async () => { - await Promise.all([ - fs.unlink(`${dir}/swagger.json`), - fs.unlink(`${dir}/customSpec.json`), - fs.unlink(`${dir}/customSpec.yaml`), - fs.unlink(`${dir}/customSpec.yml`), - ]); - }); -}); From 9403667087905a0e762576ff553728feef1b2ff9 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Wed, 3 Feb 2021 16:56:09 +0100 Subject: [PATCH 19/41] remove commander --- .eslintrc.cjs | 2 +- examples/cli/cli.js | 42 +++++--- examples/cli/cli.spec.js | 21 ++-- examples/cli/reference-specification.json | 126 ++++++++++++++++++++++ package.json | 4 - src/utils.js | 10 +- test/lib.spec.js | 2 +- yarn.lock | 12 +-- 8 files changed, 182 insertions(+), 37 deletions(-) create mode 100644 examples/cli/reference-specification.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b1ae70e2..aa9f7390 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = { }, parserOptions: { sourceType: 'module', - ecmaVersion: 11, + ecmaVersion: 12, }, extends: [ 'eslint:recommended', diff --git a/examples/cli/cli.js b/examples/cli/cli.js index f0c78984..ea2fde8c 100755 --- a/examples/cli/cli.js +++ b/examples/cli/cli.js @@ -1,27 +1,39 @@ #!/usr/bin/env node import { promises as fs } from 'fs'; +import { pathToFileURL } from 'url'; -import swaggerJsdoc from '../../src/lib.js'; import { loadDefinition } from '../../src/utils.js'; +import swaggerJsdoc from '../../src/lib.js'; +// Handle CLI arguments in your preferred way. // @see https://nodejs.org/en/knowledge/command-line/how-to-parse-command-line-arguments/ const args = process.argv.slice(2); -// extract definition file: it's always only 1 -const definition = args.splice( - args.findIndex((i) => i === '--definition'), - 2 -)[1]; -const swaggerDefinition = await loadDefinition(definition); +// Extract definition file. +// It's always only 1. +// The definition loader requires an absolute specifier with file:/// +const definitionUrl = pathToFileURL( + args.splice( + args.findIndex((i) => i === '--definition'), + 2 + )[1] +); + +// Because "Parsing error: Cannot use keyword 'await' outside an async function" +(async () => { + const swaggerDefinition = await loadDefinition(definitionUrl.href); -// remove --apis flag -args.splice(0, 1); + // Extract apis + // remove --apis flag + args.splice(0, 1); + // the rest of this example can be treated as the contents of the --apis + const apis = args; -// the rest of this example can be treated as the contents of the --apis -const apis = args; + // Use the library + const spec = await swaggerJsdoc({ swaggerDefinition, apis }); -// call the node api -const spec = swaggerJsdoc({ swaggerDefinition, apis }); + // Save specification place and format + await fs.writeFile('swagger.json', JSON.stringify(spec, null, 2)); -// and save the result to your preferred place and format -await fs.writeFile('swagger.json', JSON.stringify(spec, null, 2)); + console.log('Specification has been created successfully!'); +})(); diff --git a/examples/cli/cli.spec.js b/examples/cli/cli.spec.js index 324095a8..1f02826c 100644 --- a/examples/cli/cli.spec.js +++ b/examples/cli/cli.spec.js @@ -1,5 +1,5 @@ import { promises as fs } from 'fs'; -import { dirname } from 'path'; +import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; import { promisify } from 'util'; import { exec } from 'child_process'; @@ -9,19 +9,22 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const bin = `node ${__dirname}/cli.js`; describe('Example command line application', () => { - it('should require a definition file', async () => { - const result = await sh(`${bin} wrongDefinition`); - console.log('result', result); - }); - it('should produce results matching reference specification', async () => { const result = await sh( - `${bin} --definition examples/app/swaggerDefinition.js --apis examples/app/parameters.* examples/app/route*` + `${bin} --definition test/fixtures/swaggerDefinition/example.js --apis examples/app/parameters.* examples/app/route*` + ); + expect(result.stderr).toBe(''); + expect(result.stdout).toBe( + 'Specification has been created successfully!\n' + ); + const refSpec = await fs.readFile( + resolve(__dirname, './reference-specification.json') ); - console.log('result', result); + const resSpec = await fs.readFile(`${process.cwd()}/swagger.json`); + expect(resSpec).toEqual(refSpec); }); afterAll(async () => { - await Promise.all([fs.unlink(`${dir}/swagger.json`)]); + await fs.unlink(`${process.cwd()}/swagger.json`); }); }); diff --git a/examples/cli/reference-specification.json b/examples/cli/reference-specification.json new file mode 100644 index 00000000..31caf99b --- /dev/null +++ b/examples/cli/reference-specification.json @@ -0,0 +1,126 @@ +{ + "info": { + "title": "Hello World", + "version": "1.0.0", + "description": "A sample API" + }, + "swagger": "2.0", + "paths": { + "/login": { + "post": { + "description": "Login to the application", + "tags": ["Users", "Login"], + "produces": ["application/json"], + "parameters": [ + { + "$ref": "#/parameters/username" + }, + { + "name": "password", + "description": "User's password.", + "in": "formData", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "login", + "schema": { + "type": "object", + "$ref": "#/definitions/Login" + } + } + } + } + }, + "/hello": { + "get": { + "description": "Returns the homepage", + "responses": { + "200": { + "description": "hello world" + } + } + } + }, + "/": { + "get": { + "description": "Returns the homepage", + "responses": { + "200": { + "description": "hello world" + } + } + } + }, + "/users": { + "get": { + "description": "Returns users", + "tags": ["Users"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "users" + } + } + }, + "post": { + "description": "Returns users", + "tags": ["Users"], + "produces": ["application/json"], + "parameters": [ + { + "$ref": "#/parameters/username" + } + ], + "responses": { + "200": { + "description": "users" + } + } + } + } + }, + "definitions": { + "Login": { + "required": ["username", "password"], + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "path": { + "type": "string" + } + } + } + }, + "responses": {}, + "parameters": { + "username": { + "name": "username", + "description": "Username to use for login.", + "in": "formData", + "required": true, + "type": "string" + } + }, + "securityDefinitions": {}, + "tags": [ + { + "name": "Users", + "description": "User management and login" + }, + { + "name": "Login", + "description": "Login" + }, + { + "name": "Accounts", + "description": "Accounts" + } + ] +} diff --git a/package.json b/package.json index e7704c48..5f1c4fb8 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "transform": {} }, "dependencies": { - "commander": "6.2.0", "doctrine": "3.0.0", "glob": "7.1.6", "swagger-parser": "10.0.2", @@ -56,9 +55,6 @@ "bugs": { "url": "https://github.com/Surnet/swagger-jsdoc/issues" }, - "resolutions": { - "minimist": ">=1.2.3" - }, "husky": { "hooks": { "pre-commit": "lint-staged" diff --git a/src/utils.js b/src/utils.js index 78140314..6d476130 100644 --- a/src/utils.js +++ b/src/utils.js @@ -101,10 +101,18 @@ export function isTagPresentInTags(tag, tags) { } /** - * @param {string} definitionPath + * @param {string} definitionPath path to the swaggerDefinition */ export async function loadDefinition(definitionPath) { const loadModule = async () => { + /** + * Load ESM module + * @see https://nodejs.org/api/esm.html + * + * `definitionPath` will be treated as an absolute specifier. + * The relative and bare specifiers would be based on assumptions which are not stable. + * For example, if path from cli `examples/app/parameters.*` goes in, it will be assumed as bare, which is wrong. + */ const esmodule = await import(definitionPath); return esmodule.default; }; diff --git a/test/lib.spec.js b/test/lib.spec.js index 26945b37..30464437 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -180,7 +180,7 @@ describe('Main lib module', () => { }); officialExamples.forEach((example) => { - it(`Example: ${example}`, async () => { + it(`Example: ${example}`, () => { const title = `Sample specification testing ${example}`; const examplePath = `${__dirname}/fixtures/v3/${example}`; diff --git a/yarn.lock b/yarn.lock index 6e7597e7..4da0a775 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1254,16 +1254,16 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@6.2.0, commander@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" - integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== - commander@^2.7.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3590,7 +3590,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@>=1.2.3, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== From 2ce964ec837ba5c1eb883084f14dadb66a89cc34 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Wed, 3 Feb 2021 17:12:11 +0100 Subject: [PATCH 20/41] retry ci --- .github/workflows/ci.yml | 15 +-------------- test/utils.spec.js | 8 +++++--- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1f412f4..b4935fe6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x] + node-version: [14.x, latest] steps: - uses: actions/checkout@v2 @@ -24,16 +24,3 @@ jobs: run: yarn install --frozen-lockfile - name: Run tests run: yarn test - - # publish: - # name: publish - # needs: tests - # runs-on: ubuntu-latest - # if: github.event_name == 'push' && github.ref == 'refs/heads/master' - # steps: - # - uses: actions/checkout@v2 - # - name: Publish - # uses: mikeal/merge-release@master - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/test/utils.spec.js b/test/utils.spec.js index 54a6bdc2..f4a97eb1 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -6,8 +6,8 @@ import { validateDefinition, } from '../src/utils.js'; +import { pathToFileURL, fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; -import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Utilities module', () => { @@ -126,7 +126,8 @@ describe('Utilities module', () => { it('should support .js', async () => { const def = resolve(__dirname, `${example}.js`); - const result = await loadDefinition(def); + const fileUrl = pathToFileURL(def); + const result = await loadDefinition(fileUrl.href); expect(result).toEqual({ info: { title: 'Hello World', @@ -150,7 +151,8 @@ describe('Utilities module', () => { it('should support .mjs', async () => { const def = resolve(__dirname, `${example}.mjs`); - const result = await loadDefinition(def); + const fileUrl = pathToFileURL(def); + const result = await loadDefinition(fileUrl.href); expect(result).toEqual({ info: { title: 'Hello World', From 5c37df5ee0459f2bda4701330d0dee1898c4fcb3 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Wed, 3 Feb 2021 17:18:27 +0100 Subject: [PATCH 21/41] retry ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4935fe6..a9ff9fe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [14.x, latest] + node-version: [14.x, 15.x] steps: - uses: actions/checkout@v2 From f55bb6134252a0d27e7882ddf2b51349e29e0f7f Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Wed, 3 Feb 2021 17:30:43 +0100 Subject: [PATCH 22/41] still inconsistent behavior, unfortunately --- examples/app/swaggerDefinition.js | 14 -------------- examples/cli/cli.spec.js | 10 +++++----- test/utils.spec.js | 8 +++----- 3 files changed, 8 insertions(+), 24 deletions(-) delete mode 100644 examples/app/swaggerDefinition.js diff --git a/examples/app/swaggerDefinition.js b/examples/app/swaggerDefinition.js deleted file mode 100644 index 39576128..00000000 --- a/examples/app/swaggerDefinition.js +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -const host = `http://${process.env.IP}:${process.env.PORT}`; - -module.exports = { - info: { - // API informations (required) - title: 'Hello World', // Title (required) - version: '1.0.0', // Version (required) - description: 'A sample API', // Description (optional) - }, - host, // Host (optional) - basePath: '/', // Base path (optional) -}; diff --git a/examples/cli/cli.spec.js b/examples/cli/cli.spec.js index 1f02826c..c60d52c3 100644 --- a/examples/cli/cli.spec.js +++ b/examples/cli/cli.spec.js @@ -1,10 +1,12 @@ import { promises as fs } from 'fs'; -import { dirname, resolve } from 'path'; +import { createRequire } from 'module'; +import { dirname } from 'path'; import { fileURLToPath } from 'url'; import { promisify } from 'util'; import { exec } from 'child_process'; const sh = promisify(exec); +const require = createRequire(import.meta.url); const __dirname = dirname(fileURLToPath(import.meta.url)); const bin = `node ${__dirname}/cli.js`; @@ -17,10 +19,8 @@ describe('Example command line application', () => { expect(result.stdout).toBe( 'Specification has been created successfully!\n' ); - const refSpec = await fs.readFile( - resolve(__dirname, './reference-specification.json') - ); - const resSpec = await fs.readFile(`${process.cwd()}/swagger.json`); + const refSpec = require('./reference-specification.json'); + const resSpec = require(`${process.cwd()}/swagger.json`); expect(resSpec).toEqual(refSpec); }); diff --git a/test/utils.spec.js b/test/utils.spec.js index f4a97eb1..a282370f 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -6,7 +6,7 @@ import { validateDefinition, } from '../src/utils.js'; -import { pathToFileURL, fileURLToPath } from 'url'; +import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -126,8 +126,7 @@ describe('Utilities module', () => { it('should support .js', async () => { const def = resolve(__dirname, `${example}.js`); - const fileUrl = pathToFileURL(def); - const result = await loadDefinition(fileUrl.href); + const result = await loadDefinition(def); expect(result).toEqual({ info: { title: 'Hello World', @@ -151,8 +150,7 @@ describe('Utilities module', () => { it('should support .mjs', async () => { const def = resolve(__dirname, `${example}.mjs`); - const fileUrl = pathToFileURL(def); - const result = await loadDefinition(fileUrl.href); + const result = await loadDefinition(def); expect(result).toEqual({ info: { title: 'Hello World', From c089c3eca73c0705b489f3081f7fcdcedbe2b2c5 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Thu, 4 Feb 2021 08:48:47 +0100 Subject: [PATCH 23/41] run without caching --- .github/workflows/ci.yml | 2 +- examples/cli/cli.js | 17 +++++++++++------ package.json | 2 +- src/utils.js | 8 -------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9ff9fe5..7a429a9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: CI -on: [push, pull_request] +on: [push] jobs: audit: diff --git a/examples/cli/cli.js b/examples/cli/cli.js index ea2fde8c..4667a174 100755 --- a/examples/cli/cli.js +++ b/examples/cli/cli.js @@ -5,18 +5,23 @@ import { pathToFileURL } from 'url'; import { loadDefinition } from '../../src/utils.js'; import swaggerJsdoc from '../../src/lib.js'; -// Handle CLI arguments in your preferred way. -// @see https://nodejs.org/en/knowledge/command-line/how-to-parse-command-line-arguments/ +/** + * Handle CLI arguments in your preferred way. + * @see https://nodejs.org/en/knowledge/command-line/how-to-parse-command-line-arguments/ + */ const args = process.argv.slice(2); -// Extract definition file. -// It's always only 1. -// The definition loader requires an absolute specifier with file:/// +/** + * Extract definition + * Pass an absolute specifier with file:/// to the loader. + * The relative and bare specifiers would be based on assumptions which are not stable. + * For example, if path from cli `examples/app/parameters.*` goes in, it will be assumed as bare, which is wrong. + */ const definitionUrl = pathToFileURL( args.splice( args.findIndex((i) => i === '--definition'), 2 - )[1] + )[1] // Definition file is always only one. ); // Because "Parsing error: Cannot use keyword 'await' outside an async function" diff --git a/package.json b/package.json index 5f1c4fb8..01b93e0a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start": "node examples/app/app.js", "lint": "eslint .", "test:lint": "eslint .", - "test:js": "NODE_OPTIONS=--experimental-vm-modules jest", + "test:js": "NODE_OPTIONS=--experimental-vm-modules jest --no-cache", "test": "run-p test:* -cn" }, "type": "module", diff --git a/src/utils.js b/src/utils.js index 6d476130..1175b561 100644 --- a/src/utils.js +++ b/src/utils.js @@ -105,14 +105,6 @@ export function isTagPresentInTags(tag, tags) { */ export async function loadDefinition(definitionPath) { const loadModule = async () => { - /** - * Load ESM module - * @see https://nodejs.org/api/esm.html - * - * `definitionPath` will be treated as an absolute specifier. - * The relative and bare specifiers would be based on assumptions which are not stable. - * For example, if path from cli `examples/app/parameters.*` goes in, it will be assumed as bare, which is wrong. - */ const esmodule = await import(definitionPath); return esmodule.default; }; From 7c5136a2968a34348dce7d327eed8a1f5c3e33b6 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 7 Feb 2021 12:03:23 +0100 Subject: [PATCH 24/41] upgrade deps --- package.json | 8 ++++---- yarn.lock | 39 ++++++++++++++++----------------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 01b93e0a..63c63397 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ }, "devDependencies": { "body-parser": "1.19.0", - "eslint": "7.18.0", - "eslint-config-prettier": "6.15.0", + "eslint": "7.19.0", + "eslint-config-prettier": "7.2.0", "eslint-loader": "4.0.2", "eslint-plugin-import": "2.22.1", "eslint-plugin-jest": "24.1.3", @@ -35,10 +35,10 @@ "express": "4.17.1", "husky": "4.3.8", "jest": "26.6.3", - "lint-staged": "10.5.3", + "lint-staged": "10.5.4", "npm-run-all": "4.1.5", "prettier": "2.2.1", - "supertest": "6.1.2" + "supertest": "6.1.3" }, "license": "MIT", "homepage": "https://github.com/Surnet/swagger-jsdoc", diff --git a/yarn.lock b/yarn.lock index 4da0a775..9d5df0e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1649,12 +1649,10 @@ escodegen@^1.14.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@6.15.0: - version "6.15.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9" - integrity sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw== - dependencies: - get-stdin "^6.0.0" +eslint-config-prettier@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9" + integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg== eslint-import-resolver-node@^0.3.4: version "0.3.4" @@ -1741,10 +1739,10 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@7.18.0: - version "7.18.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67" - integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ== +eslint@7.19.0: + version "7.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.19.0.tgz#6719621b196b5fad72e43387981314e5d0dc3f41" + integrity sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg== dependencies: "@babel/code-frame" "^7.0.0" "@eslint/eslintrc" "^0.3.0" @@ -2239,11 +2237,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -3350,10 +3343,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@10.5.3: - version "10.5.3" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.3.tgz#c682838b3eadd4c864d1022da05daa0912fb1da5" - integrity sha512-TanwFfuqUBLufxCc3RUtFEkFraSPNR3WzWcGF39R3f2J7S9+iF9W0KTVLfSy09lYGmZS5NDCxjNvhGMSJyFCWg== +lint-staged@10.5.4: + version "10.5.4" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.4.tgz#cd153b5f0987d2371fc1d2847a409a2fe705b665" + integrity sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg== dependencies: chalk "^4.1.0" cli-truncate "^2.1.0" @@ -4846,10 +4839,10 @@ superagent@^6.1.0: readable-stream "^3.6.0" semver "^7.3.2" -supertest@6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.2.tgz#1679c234b139eed93911c185b67f689348fc2453" - integrity sha512-hZ8bu3TebxCYQ40mF6/2ou58EEG5jxo1AbsE1vprqXo3emkmqbQMcQrF7acsQteOjYlkExSvYOAQ/feTE9n7uA== +supertest@6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.3.tgz#3f49ea964514c206c334073e8dc4e70519c7403f" + integrity sha512-v2NVRyP73XDewKb65adz+yug1XMtmvij63qIWHZzSX8tp6wiq6xBLUy4SUAd2NII6wIipOmHT/FD9eicpJwdgQ== dependencies: methods "^1.1.2" superagent "^6.1.0" From bda840f26eb3ad1e7bec53fe40a94fb7c31f9d69 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 7 Feb 2021 12:17:09 +0100 Subject: [PATCH 25/41] remove unused files --- .prettierignore | 2 -- test/fixtures/deprecated_routes.js | 17 ----------------- .../{empty.coffee => empty/example.coffee} | 0 .../{empty_file.js => empty/example.js} | 0 test/fixtures/empty_export.js | 3 --- test/fixtures/wrong-yaml-identation.js | 12 ------------ test/fixtures/wrong_definition.js | 9 --------- test/fixtures/wrong_syntax.json | 8 -------- test/fixtures/wrong_syntax.yaml | 5 ----- test/utils.spec.js | 6 ++++-- 10 files changed, 4 insertions(+), 58 deletions(-) delete mode 100644 .prettierignore delete mode 100644 test/fixtures/deprecated_routes.js rename test/fixtures/{empty.coffee => empty/example.coffee} (100%) rename test/fixtures/{empty_file.js => empty/example.js} (100%) delete mode 100644 test/fixtures/empty_export.js delete mode 100644 test/fixtures/wrong-yaml-identation.js delete mode 100644 test/fixtures/wrong_definition.js delete mode 100644 test/fixtures/wrong_syntax.json delete mode 100644 test/fixtures/wrong_syntax.yaml diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 9c129625..00000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -test/fixtures/wrong_syntax.json -test/fixtures/wrong_syntax.yaml \ No newline at end of file diff --git a/test/fixtures/deprecated_routes.js b/test/fixtures/deprecated_routes.js deleted file mode 100644 index ac583668..00000000 --- a/test/fixtures/deprecated_routes.js +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -module.exports.setup = function (app) { - /** - * @swagger - * /deprecated: - * get: - * description: Returns a string - * path: '/deprecated' - * responses: - * 200: - * description: deprecated path - */ - app.get('/deprecated', (req, res) => { - res.send('Deprecated "path" property!'); - }); -}; diff --git a/test/fixtures/empty.coffee b/test/fixtures/empty/example.coffee similarity index 100% rename from test/fixtures/empty.coffee rename to test/fixtures/empty/example.coffee diff --git a/test/fixtures/empty_file.js b/test/fixtures/empty/example.js similarity index 100% rename from test/fixtures/empty_file.js rename to test/fixtures/empty/example.js diff --git a/test/fixtures/empty_export.js b/test/fixtures/empty_export.js deleted file mode 100644 index ca261f18..00000000 --- a/test/fixtures/empty_export.js +++ /dev/null @@ -1,3 +0,0 @@ -/* istanbul ignore file */ - -export default {}; diff --git a/test/fixtures/wrong-yaml-identation.js b/test/fixtures/wrong-yaml-identation.js deleted file mode 100644 index 2ced73ba..00000000 --- a/test/fixtures/wrong-yaml-identation.js +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -module.exports = (app) => { - /** - * @swagger - * - * /invalid_yaml: - * - foo - * bar - */ - app.get('/invalid_yaml', () => {}); -}; diff --git a/test/fixtures/wrong_definition.js b/test/fixtures/wrong_definition.js deleted file mode 100644 index 16109e56..00000000 --- a/test/fixtures/wrong_definition.js +++ /dev/null @@ -1,9 +0,0 @@ -/* istanbul ignore file */ - -const host = `http://${process.env.IP}:${process.env.PORT}`; - -module.exports = { - info: {}, - host, // Host (optional) - basePath: '/', // Base path (optional) -}; diff --git a/test/fixtures/wrong_syntax.json b/test/fixtures/wrong_syntax.json deleted file mode 100644 index 178fceda..00000000 --- a/test/fixtures/wrong_syntax.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "info": { - title: "Hello World", - "version": "1.0.0", - "description": "A sample API" - }, - "apis": ["./**/*/routes.js"] -} diff --git a/test/fixtures/wrong_syntax.yaml b/test/fixtures/wrong_syntax.yaml deleted file mode 100644 index 6e6bf19b..00000000 --- a/test/fixtures/wrong_syntax.yaml +++ /dev/null @@ -1,5 +0,0 @@ -info: - !!!title: Hello World - version: 1.0.0 - description: A sample API -apis: [./**/*/routes.js] diff --git a/test/utils.spec.js b/test/utils.spec.js index a282370f..c63fc660 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -74,7 +74,9 @@ describe('Utilities module', () => { it('should return empty arrays from empty coffeescript files/syntax', () => { expect( - extractAnnotations(resolve(__dirname, './fixtures/empty.coffee')) + extractAnnotations( + resolve(__dirname, './fixtures/empty/example.coffee') + ) ).toEqual({ yaml: [], jsdoc: [], @@ -83,7 +85,7 @@ describe('Utilities module', () => { it('should extract jsdoc comments from empty javascript files/syntax', () => { expect( - extractAnnotations(resolve(__dirname, './fixtures/empty_file.js')) + extractAnnotations(resolve(__dirname, './fixtures/empty/example.js')) ).toEqual({ yaml: [], jsdoc: [], From 41b28fc0701f86d12dfb25c77e2531821f3a5e4c Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 7 Feb 2021 14:40:11 +0100 Subject: [PATCH 26/41] move validation in one place --- examples/merge.js | 10 ++++++++ src/lib.js | 18 ++------------- src/utils.js | 36 +++++++++++++++++++---------- test/lib.spec.js | 36 ----------------------------- test/utils.spec.js | 57 ++++++++++++++++++++++++++++++---------------- 5 files changed, 73 insertions(+), 84 deletions(-) create mode 100644 examples/merge.js diff --git a/examples/merge.js b/examples/merge.js new file mode 100644 index 00000000..39ce3ea7 --- /dev/null +++ b/examples/merge.js @@ -0,0 +1,10 @@ +import swaggerJsdoc from '../src/lib.js'; + +let testObject = { + swaggerDefinition: {}, + apis: ['./test/fixtures/merge/*.yml'], +}; + +testObject = swaggerJsdoc(testObject); + +console.log('testObject', testObject); diff --git a/src/lib.js b/src/lib.js index d89d1a1c..c03b5744 100644 --- a/src/lib.js +++ b/src/lib.js @@ -1,4 +1,5 @@ import { build } from './specification.js'; +import { validateOptions } from './utils.js'; /** * Generates the specification. @@ -11,22 +12,7 @@ import { build } from './specification.js'; * @returns {object} Output specification */ const lib = (options) => { - if (!options) { - throw new Error(`Missing or invalid input: 'options' is required`); - } - - if (!options.swaggerDefinition && !options.definition) { - throw new Error( - `Missing or invalid input: 'options.swaggerDefinition' or 'options.definition' is required` - ); - } - - if (!options.apis || !Array.isArray(options.apis)) { - throw new Error( - `Missing or invalid input: 'options.apis' is required and it should be an array.` - ); - } - + validateOptions(options); return build(options); }; diff --git a/src/utils.js b/src/utils.js index 1175b561..1228d3a7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -146,25 +146,37 @@ export async function loadDefinition(definitionPath) { } /** - * @param {object} swaggerDefinition + * @param {object} options + * @returns {object} the original input if valid, throws otherwise */ -export function validateDefinition(swaggerDefinition) { - if (!swaggerDefinition) { - throw new Error('Swagger definition object is required'); +export function validateOptions(options) { + if (!options) { + throw new Error(`'options' parameter is required!`); } - if (!swaggerDefinition.info) { - throw new Error('Definition file should contain an info object!'); + if (!options.swaggerDefinition && !options.definition) { + throw new Error( + `'options.swaggerDefinition' or 'options.definition' is required!` + ); } - if ( - !('title' in swaggerDefinition.info) || - !('version' in swaggerDefinition.info) - ) { + const def = options.swaggerDefinition || options.definition; + + if (!def.info) { throw new Error( - 'Definition info object requires title and version properties!' + `Swagger definition ('options.swaggerDefinition') should contain an info object!` ); } - return true; + if (!('title' in def.info) || !('version' in def.info)) { + throw new Error( + `Swagger definition info object ('options.swaggerDefinition.info') requires title and version properties!` + ); + } + + if (!options.apis || !Array.isArray(options.apis)) { + throw new Error(`'options.apis' is required and it should be an array!`); + } + + return options; } diff --git a/test/lib.spec.js b/test/lib.spec.js index 30464437..d48cdd5d 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -1,7 +1,6 @@ import { dirname } from 'path'; import { createRequire } from 'module'; import { fileURLToPath } from 'url'; -import { jest } from '@jest/globals'; import swaggerJsdoc from '../src/lib.js'; const require = createRequire(import.meta.url); @@ -51,44 +50,9 @@ describe('Main lib module', () => { }); }); - describe('Error handling', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should require options input', () => { - expect(() => { - swaggerJsdoc(); - }).toThrow(`Missing or invalid input: 'options' is required`); - }); - - it('should require a definition input', () => { - expect(() => { - swaggerJsdoc({}); - }).toThrow( - `Missing or invalid input: 'options.swaggerDefinition' or 'options.definition' is required` - ); - }); - - it('should require an api files input', () => { - expect(() => { - swaggerJsdoc({ definition: {} }); - }).toThrow( - `Missing or invalid input: 'options.apis' is required and it should be an array.` - ); - - expect(() => { - swaggerJsdoc({ definition: {}, apis: {} }); - }).toThrow( - `Missing or invalid input: 'options.apis' is required and it should be an array.` - ); - }); - }); - describe('Specification v2: Swagger', () => { it('should support multiple paths', () => { let testObject = { - swaggerDefinition: {}, apis: ['./test/fixtures/merge/*.yml'], }; diff --git a/test/utils.spec.js b/test/utils.spec.js index c63fc660..9aa329b1 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -3,7 +3,7 @@ import { extractAnnotations, hasEmptyProperty, loadDefinition, - validateDefinition, + validateOptions, } from '../src/utils.js'; import { fileURLToPath } from 'url'; @@ -163,47 +163,64 @@ describe('Utilities module', () => { }); }); - describe('validateDefinition', () => { + describe('validateOptions', () => { + it('should throw on empty input', () => { + expect(() => { + validateOptions(); + }).toThrow("'options' parameter is required!"); + }); + it('should throw on bad input', () => { expect(() => { - validateDefinition(); - }).toThrow('Swagger definition object is required'); + validateOptions({}); + }).toThrow( + `'options.swaggerDefinition' or 'options.definition' is required!` + ); }); it(`should throw on missing 'info' property`, () => { expect(() => { - validateDefinition({}); - }).toThrow('Definition file should contain an info object!'); + const options = { swaggerDefinition: {} }; + validateOptions(options); + }).toThrow( + `Swagger definition ('options.swaggerDefinition') should contain an info object!` + ); }); it(`should throw on missing 'title' and 'version' properties in the info object`, () => { expect(() => { - validateDefinition({ info: {} }); + validateOptions({ swaggerDefinition: { info: {} } }); }).toThrow( - 'Definition info object requires title and version properties!' + `Swagger definition info object ('options.swaggerDefinition.info') requires title and version properties!` ); expect(() => { - validateDefinition({ info: { title: '' } }); + validateOptions({ swaggerDefinition: { info: { title: '' } } }); }).toThrow( - 'Definition info object requires title and version properties!' + `Swagger definition info object ('options.swaggerDefinition.info') requires title and version properties!` ); expect(() => { - validateDefinition({ info: { version: '' } }); + validateOptions({ swaggerDefinition: { info: { version: '' } } }); }).toThrow( - 'Definition info object requires title and version properties!' + `Swagger definition info object ('options.swaggerDefinition.info') requires title and version properties!` ); - - expect(() => { - validateDefinition({ info: { version: '', title: '' } }); - }).not.toThrow(); }); - it('should return true on valid input', () => { - expect(validateDefinition({ info: { version: '', title: '' } })).toBe( - true - ); + it(`should throw on missing 'apis' property`, () => { + expect(() => { + validateOptions({ + swaggerDefinition: { info: { version: '', title: '' } }, + }); + }).toThrow(`'options.apis' is required and it should be an array!`); + }); + + it('should return original options on valid input', () => { + const options = { + swaggerDefinition: { info: { version: '', title: '' } }, + apis: [], + }; + expect(validateOptions(options)).toEqual(options); }); }); }); From c1b7613040981d9b47e47cace6d29892c46db0cc Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Mon, 8 Feb 2021 11:26:19 +0100 Subject: [PATCH 27/41] refactor organize function --- examples/merge.js | 10 --- src/specification.js | 108 ++++++++++++++------------- test/fixtures/merge/one.yml | 5 -- test/fixtures/merge/two.yml | 5 -- test/lib.spec.js | 28 ------- test/specification.spec.js | 143 ++++++++++++++++++++++-------------- 6 files changed, 144 insertions(+), 155 deletions(-) delete mode 100644 examples/merge.js delete mode 100644 test/fixtures/merge/one.yml delete mode 100644 test/fixtures/merge/two.yml diff --git a/examples/merge.js b/examples/merge.js deleted file mode 100644 index 39ce3ea7..00000000 --- a/examples/merge.js +++ /dev/null @@ -1,10 +0,0 @@ -import swaggerJsdoc from '../src/lib.js'; - -let testObject = { - swaggerDefinition: {}, - apis: ['./test/fixtures/merge/*.yml'], -}; - -testObject = swaggerJsdoc(testObject); - -console.log('testObject', testObject); diff --git a/src/specification.js b/src/specification.js index 92a5be6b..cf30c07a 100644 --- a/src/specification.js +++ b/src/specification.js @@ -111,59 +111,65 @@ export function finalize(swaggerObject, options) { /** * @param {object} swaggerObject - * @param {object} annotation - * @param {string} property + * @param {Array} annotations + * @returns {object} swaggerObject */ -export function organize(swaggerObject, annotation, property) { - // Root property on purpose. - // @see https://github.com/OAI/OpenAPI-Specification/blob/master/proposals/002_Webhooks.md#proposed-solution - if (property === 'x-webhooks') { - swaggerObject[property] = annotation[property]; - } +export function organize(swaggerObject, annotations) { + for (const annotation of annotations) { + for (const property in annotation) { + // Root property on purpose. + // @see https://github.com/OAI/OpenAPI-Specification/blob/master/proposals/002_Webhooks.md#proposed-solution + if (property === 'x-webhooks') { + swaggerObject[property] = annotation[property]; + } - // Other extensions can be in varying places depending on different vendors and opinions. - // The following return makes it so that they are not put in `paths` in the last case. - // New specific extensions will need to be handled on case-by-case if to be included in `paths`. - if (property.startsWith('x-')) return; - - const commonProperties = [ - 'components', - 'consumes', - 'produces', - 'paths', - 'schemas', - 'securityDefinitions', - 'responses', - 'parameters', - 'definitions', - ]; - - if (commonProperties.includes(property)) { - for (const definition of Object.keys(annotation[property])) { - swaggerObject[property][definition] = { - ...swaggerObject[property][definition], - ...annotation[property][definition], - }; - } - } else if (property === 'tags') { - const { tags } = annotation; + // Other extensions can be in varying places depending on different vendors and opinions. + // The following return makes it so that they are not put in `paths` in the last case. + // New specific extensions will need to be handled on case-by-case if to be included in `paths`. + if (property.startsWith('x-')) continue; + + const commonProperties = [ + 'components', + 'consumes', + 'produces', + 'paths', + 'schemas', + 'securityDefinitions', + 'responses', + 'parameters', + 'definitions', + ]; + + if (commonProperties.includes(property)) { + for (const definition of Object.keys(annotation[property])) { + swaggerObject[property][definition] = { + ...swaggerObject[property][definition], + ...annotation[property][definition], + }; + } + } else if (property === 'tags') { + const { tags } = annotation; - if (Array.isArray(tags)) { - for (const tag of tags) { - if (!isTagPresentInTags(tag, swaggerObject.tags)) { - swaggerObject.tags.push(tag); + if (Array.isArray(tags)) { + for (const tag of tags) { + if (!isTagPresentInTags(tag, swaggerObject.tags)) { + swaggerObject.tags.push(tag); + } + } + } else if (!isTagPresentInTags(tags, swaggerObject.tags)) { + swaggerObject.tags.push(tags); } + } else { + // Paths which are not defined as "paths" property, starting with a slash "/" + swaggerObject.paths[property] = { + ...swaggerObject.paths[property], + ...annotation[property], + }; } - } else if (!isTagPresentInTags(tags, swaggerObject.tags)) { - swaggerObject.tags.push(tags); } - } else { - // Paths which are not defined as "paths" property, starting with a slash "/" - swaggerObject.paths[property] = { - ...swaggerObject.paths[property], - ...annotation[property], - }; } + + return swaggerObject; } /** @@ -269,12 +275,10 @@ export function build(options) { } } - for (const document of yamlDocsReady) { - const parsedDoc = document.toJSON(); - for (const property in parsedDoc) { - organize(specification, parsedDoc, property); - } - } + organize( + specification, + yamlDocsReady.map((doc) => doc.toJSON()) + ); return finalize(specification, options); } diff --git a/test/fixtures/merge/one.yml b/test/fixtures/merge/one.yml deleted file mode 100644 index 8c9bb281..00000000 --- a/test/fixtures/merge/one.yml +++ /dev/null @@ -1,5 +0,0 @@ -responses: - api: - foo: - 200: - description: OK diff --git a/test/fixtures/merge/two.yml b/test/fixtures/merge/two.yml deleted file mode 100644 index 62a4bbd7..00000000 --- a/test/fixtures/merge/two.yml +++ /dev/null @@ -1,5 +0,0 @@ -responses: - api: - bar: - 200: - description: OK diff --git a/test/lib.spec.js b/test/lib.spec.js index d48cdd5d..c4c35cbe 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -8,10 +8,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Main lib module', () => { describe('General', () => { - it('should be a function', () => { - expect(typeof swaggerJsdoc).toBe('function'); - }); - it('should support custom encoding', () => { const result = swaggerJsdoc({ swaggerDefinition: { @@ -50,30 +46,6 @@ describe('Main lib module', () => { }); }); - describe('Specification v2: Swagger', () => { - it('should support multiple paths', () => { - let testObject = { - apis: ['./test/fixtures/merge/*.yml'], - }; - - testObject = swaggerJsdoc(testObject); - expect(testObject).toEqual({ - swagger: '2.0', - paths: {}, - definitions: {}, - responses: { - api: { - foo: { 200: { description: 'OK' } }, - bar: { 200: { description: 'OK' } }, - }, - }, - parameters: {}, - securityDefinitions: {}, - tags: [], - }); - }); - }); - describe('Specification v3: OpenAPI', () => { const officialExamples = ['callback', 'links', 'petstore']; diff --git a/test/specification.spec.js b/test/specification.spec.js index cc2891be..7e28683f 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -4,48 +4,72 @@ const swaggerObject = { info: { title: 'Hello World', version: '1.0.0', - description: 'A sample API', }, - host: 'localhost:3000', - basePath: '/', - swagger: '2.0', - schemes: [], - consumes: [], - produces: [], - paths: {}, - definitions: {}, responses: {}, + definitions: {}, parameters: {}, securityDefinitions: {}, - security: {}, - tags: [], - externalDocs: {}, }; describe('Specification module', () => { describe('organize', () => { - it('should be a function', () => { - expect(typeof organize).toBe('function'); + it('should support merging', () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + responses: { + api: { + foo: { + 200: { + description: 'OK', + }, + }, + }, + }, + }, + { + responses: { + api: { + bar: { + 200: { + description: 'OK', + }, + }, + }, + }, + }, + ]; + + organize(testSpec, annotations); + expect(testSpec.responses).toEqual({ + api: { + bar: { 200: { description: 'OK' } }, + foo: { 200: { description: 'OK' } }, + }, + }); }); it('should handle "definitions"', () => { - const annotation = { - definitions: { - testDefinition: { - required: ['username', 'password'], - properties: { - username: { - type: 'string', - }, - password: { - type: 'string', + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + definitions: { + testDefinition: { + required: ['username', 'password'], + properties: { + username: { + type: 'string', + }, + password: { + type: 'string', + }, }, }, }, }, - }; - organize(swaggerObject, annotation, 'definitions'); - expect(swaggerObject.definitions).toEqual({ + ]; + organize(testSpec, annotations); + expect(testSpec.definitions).toEqual({ testDefinition: { required: ['username', 'password'], properties: { @@ -57,20 +81,23 @@ describe('Specification module', () => { }); it('should handle "parameters"', () => { - const annotation = { - parameters: { - testParameter: { - name: 'limit', - in: 'query', - description: 'max records to return', - required: true, - type: 'integer', - format: 'int32', + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + parameters: { + testParameter: { + name: 'limit', + in: 'query', + description: 'max records to return', + required: true, + type: 'integer', + format: 'int32', + }, }, }, - }; - organize(swaggerObject, annotation, 'parameters'); - expect(swaggerObject.parameters).toEqual({ + ]; + organize(testSpec, annotations); + expect(testSpec.parameters).toEqual({ testParameter: { name: 'limit', in: 'query', @@ -83,17 +110,20 @@ describe('Specification module', () => { }); it('should handle "securityDefinitions"', () => { - const annotation = { - securityDefinitions: { - basicAuth: { - type: 'basic', - description: - 'HTTP Basic Authentication. Works over `HTTP` and `HTTPS`', + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + securityDefinitions: { + basicAuth: { + type: 'basic', + description: + 'HTTP Basic Authentication. Works over `HTTP` and `HTTPS`', + }, }, }, - }; - organize(swaggerObject, annotation, 'securityDefinitions'); - expect(swaggerObject.securityDefinitions).toEqual({ + ]; + organize(testSpec, annotations); + expect(testSpec.securityDefinitions).toEqual({ basicAuth: { type: 'basic', description: @@ -103,15 +133,18 @@ describe('Specification module', () => { }); it('should handle "responses"', () => { - const annotation = { - responses: { - IllegalInput: { - description: 'Illegal input for operation.', + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + responses: { + IllegalInput: { + description: 'Illegal input for operation.', + }, }, }, - }; - organize(swaggerObject, annotation, 'responses'); - expect(swaggerObject.responses).toEqual({ + ]; + organize(testSpec, annotations); + expect(testSpec.responses).toEqual({ IllegalInput: { description: 'Illegal input for operation.' }, }); }); From 57d9b03678521405034aadf204e702b3311a6734 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Tue, 9 Feb 2021 10:06:32 +0100 Subject: [PATCH 28/41] make spec operations transparent to library --- src/lib.js | 12 +++++++++--- src/specification.js | 20 +++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lib.js b/src/lib.js index c03b5744..ed10ccd2 100644 --- a/src/lib.js +++ b/src/lib.js @@ -1,4 +1,4 @@ -import { build } from './specification.js'; +import { prepare, extract, organize, finalize } from './specification.js'; import { validateOptions } from './utils.js'; /** @@ -9,11 +9,17 @@ import { validateOptions } from './utils.js'; * @param {object} options.swaggerDefinition * @param {object} options.definition * @param {array} options.apis - * @returns {object} Output specification + * @returns {object|string} Output specification as json or yaml */ const lib = (options) => { validateOptions(options); - return build(options); + + const spec = prepare(options); + const parts = extract(options); + + organize(spec, parts); + + return finalize(spec, options); }; export default lib; diff --git a/src/specification.js b/src/specification.js index cf30c07a..98cf72d3 100644 --- a/src/specification.js +++ b/src/specification.js @@ -13,12 +13,14 @@ import { /** * Prepare the swagger/openapi specification object. * @see https://github.com/OAI/OpenAPI-Specification/tree/master/versions - * @param {object} definition - The `definition` or `swaggerDefinition` from options. + * @param {object} options The library input options. * @returns {object} swaggerObject */ -export function prepare(definition) { +export function prepare(options) { let version; - const swaggerObject = JSON.parse(JSON.stringify(definition)); + const swaggerObject = JSON.parse( + JSON.stringify(options.swaggerDefinition || options.definition) + ); const specificationTemplate = { v2: [ 'paths', @@ -176,12 +178,9 @@ export function organize(swaggerObject, annotations) { * @param {object} options * @returns {object} swaggerObject */ -export function build(options) { +export function extract(options) { YAML.defaultOptions.keepCstNodes = true; - // Get input definition and prepare the specification's skeleton - const definition = options.swaggerDefinition || options.definition; - const specification = prepare(definition); const yamlDocsAnchors = new Map(); const yamlDocsErrors = []; const yamlDocsReady = []; @@ -275,10 +274,5 @@ export function build(options) { } } - organize( - specification, - yamlDocsReady.map((doc) => doc.toJSON()) - ); - - return finalize(specification, options); + return yamlDocsReady.map((doc) => doc.toJSON()); } From 5419f65aa100f2306c97d0eba438f161b160fd36 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Tue, 9 Feb 2021 17:16:08 +0100 Subject: [PATCH 29/41] refactor spec.prepare() tests --- src/lib.js | 2 +- src/specification.js | 4 +- test/fixtures/v3/README.md | 3 - test/fixtures/v3/callback/api.js | 63 ---------- test/fixtures/v3/callback/openapi.json | 83 ------------- test/fixtures/v3/links/api.js | 47 -------- test/fixtures/v3/links/openapi.json | 65 ----------- test/fixtures/v3/petstore/api.js | 141 ---------------------- test/fixtures/v3/petstore/openapi.json | 156 ------------------------- test/lib.spec.js | 127 ++------------------ test/specification.spec.js | 31 ++++- 11 files changed, 40 insertions(+), 682 deletions(-) delete mode 100644 test/fixtures/v3/README.md delete mode 100644 test/fixtures/v3/callback/api.js delete mode 100644 test/fixtures/v3/callback/openapi.json delete mode 100644 test/fixtures/v3/links/api.js delete mode 100644 test/fixtures/v3/links/openapi.json delete mode 100644 test/fixtures/v3/petstore/api.js delete mode 100644 test/fixtures/v3/petstore/openapi.json diff --git a/src/lib.js b/src/lib.js index ed10ccd2..daa9e154 100644 --- a/src/lib.js +++ b/src/lib.js @@ -2,7 +2,7 @@ import { prepare, extract, organize, finalize } from './specification.js'; import { validateOptions } from './utils.js'; /** - * Generates the specification. + * Main library function * @param {object} options - Configuration options * @param {string} options.encoding Optional, passed to readFileSync options. Defaults to 'utf8'. * @param {string} options.format Optional, defaults to '.json' - target file format '.yml' or '.yaml'. diff --git a/src/specification.js b/src/specification.js index 98cf72d3..3cc075ec 100644 --- a/src/specification.js +++ b/src/specification.js @@ -18,9 +18,7 @@ import { */ export function prepare(options) { let version; - const swaggerObject = JSON.parse( - JSON.stringify(options.swaggerDefinition || options.definition) - ); + const swaggerObject = options.swaggerDefinition || options.definition; const specificationTemplate = { v2: [ 'paths', diff --git a/test/fixtures/v3/README.md b/test/fixtures/v3/README.md deleted file mode 100644 index f94d55f5..00000000 --- a/test/fixtures/v3/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# OpenAPI specification tests - -Taken from https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/api-with-examples.yaml diff --git a/test/fixtures/v3/callback/api.js b/test/fixtures/v3/callback/api.js deleted file mode 100644 index dddb8567..00000000 --- a/test/fixtures/v3/callback/api.js +++ /dev/null @@ -1,63 +0,0 @@ -// Imaginary API helper -module.exports = function (app) { - /** - * @swagger - * - * /streams: - * post: - * description: subscribes a client to receive out-of-band data - * parameters: - * - name: callbackUrl - * in: query - * required: true - * description: | - * the location where data will be sent. Must be network accessible - * by the source server - * schema: - * type: string - * format: uri - * example: https://tonys-server.com - * responses: - * '201': - * description: subscription successfully created - * content: - * application/json: - * schema: - * description: subscription information - * required: - * - subscriptionId - * properties: - * subscriptionId: - * description: this unique identifier allows management of the subscription - * type: string - * example: 2531329f-fb09-4ef7-887e-84e648214436 - * callbacks: - * # the name `onData` is a convenience locator - * onData: - * # when data is sent, it will be sent to the `callbackUrl` provided - * # when making the subscription PLUS the suffix `/data` - * '{$request.query.callbackUrl}/data': - * post: - * requestBody: - * description: subscription payload - * content: - * application/json: - * schema: - * properties: - * timestamp: - * type: string - * format: date-time - * userData: - * type: string - * responses: - * '202': - * description: | - * Your server implementation should return this HTTP status code - * if the data was received successfully - * '204': - * description: | - * Your server should return this HTTP status code if no longer interested - * in further updates - */ - app.post('/streams', () => {}); -}; diff --git a/test/fixtures/v3/callback/openapi.json b/test/fixtures/v3/callback/openapi.json deleted file mode 100644 index 07a6f42d..00000000 --- a/test/fixtures/v3/callback/openapi.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "Sample specification testing callback" - }, - "paths": { - "/streams": { - "post": { - "description": "subscribes a client to receive out-of-band data", - "parameters": [ - { - "name": "callbackUrl", - "in": "query", - "required": true, - "description": "the location where data will be sent. Must be network accessible\nby the source server\n", - "schema": { - "type": "string", - "format": "uri", - "example": "https://tonys-server.com" - } - } - ], - "responses": { - "201": { - "description": "subscription successfully created", - "content": { - "application/json": { - "schema": { - "description": "subscription information", - "required": ["subscriptionId"], - "properties": { - "subscriptionId": { - "description": "this unique identifier allows management of the subscription", - "type": "string", - "example": "2531329f-fb09-4ef7-887e-84e648214436" - } - } - } - } - } - } - }, - "callbacks": { - "onData": { - "{$request.query.callbackUrl}/data": { - "post": { - "requestBody": { - "description": "subscription payload", - "content": { - "application/json": { - "schema": { - "properties": { - "timestamp": { - "type": "string", - "format": "date-time" - }, - "userData": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "202": { - "description": "Your server implementation should return this HTTP status code\nif the data was received successfully\n" - }, - "204": { - "description": "Your server should return this HTTP status code if no longer interested\nin further updates\n" - } - } - } - } - } - } - } - } - }, - "components": {}, - "tags": [] -} diff --git a/test/fixtures/v3/links/api.js b/test/fixtures/v3/links/api.js deleted file mode 100644 index 3504774f..00000000 --- a/test/fixtures/v3/links/api.js +++ /dev/null @@ -1,47 +0,0 @@ -// Imaginary API helper - -/** - * @swagger - * - * components: - * links: - * UserRepositories: - * operationId: getRepositoriesByOwner - * parameters: - * username: '$response.body#/username' - * schemas: - * user: - * type: object - * properties: - * username: - * type: string - * uuid: - * type: string - */ - -/** - * @swagger - * - * /users/{username}: - * get: - * operationId: getUserByName - * parameters: - * - name: username - * in: path - * required: true - * schema: - * type: string - * responses: - * '200': - * description: The User - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/user' - * links: - * userRepositories: - * $ref: '#/components/links/UserRepositories' - */ -module.exports = function (app) { - app.get('/users/:username', () => {}); -}; diff --git a/test/fixtures/v3/links/openapi.json b/test/fixtures/v3/links/openapi.json deleted file mode 100644 index 5a67d42e..00000000 --- a/test/fixtures/v3/links/openapi.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "Sample specification testing links" - }, - "paths": { - "/users/{username}": { - "get": { - "operationId": "getUserByName", - "parameters": [ - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The User", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/user" - } - } - }, - "links": { - "userRepositories": { - "$ref": "#/components/links/UserRepositories" - } - } - } - } - } - } - }, - "components": { - "links": { - "UserRepositories": { - "operationId": "getRepositoriesByOwner", - "parameters": { - "username": "$response.body#/username" - } - } - }, - "schemas": { - "user": { - "type": "object", - "properties": { - "username": { - "type": "string" - }, - "uuid": { - "type": "string" - } - } - } - } - }, - "tags": [] -} diff --git a/test/fixtures/v3/petstore/api.js b/test/fixtures/v3/petstore/api.js deleted file mode 100644 index bb8be0f0..00000000 --- a/test/fixtures/v3/petstore/api.js +++ /dev/null @@ -1,141 +0,0 @@ -// Imaginary API helper - -/** - * @swagger - * - * components: - * schemas: - * Pet: - * required: - * - id - * - name - * properties: - * id: - * type: integer - * format: int64 - * name: - * type: string - * tag: - * type: string - * Pets: - * type: array - * items: - * $ref: "#/components/schemas/Pet" - * Error: - * required: - * - code - * - message - * properties: - * code: - * type: integer - * format: int32 - * message: - * type: string - */ - -module.exports = function (app) { - /** - * @swagger - * - * /pets: - * get: - * summary: List all pets - * operationId: listPets - * tags: - * - pets - * parameters: - * - name: limit - * in: query - * description: How many items to return at one time (max 100) - * required: false - * schema: - * type: integer - * format: int32 - * responses: - * '200': - * description: A paged array of pets - * headers: - * x-next: - * description: A link to the next page of responses - * schema: - * type: string - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Pets" - * default: - * description: unexpected error - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Error" - * post: - * summary: Create a pet - * operationId: createPets - * tags: - * - pets - * responses: - * '201': - * description: Null response - * default: - * description: unexpected error - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Error" - */ - app.get('/pets', () => {}); - - /** - * @swagger - * - * /pets: - * post: - * summary: Create a pet - * operationId: createPets - * tags: - * - pets - * responses: - * '201': - * description: Null response - * default: - * description: unexpected error - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Error" - */ - app.post('/pets', () => {}); - - /** - * @swagger - * - * /pets/{petId}: - * get: - * summary: Info for a specific pet - * operationId: showPetById - * tags: - * - pets - * parameters: - * - name: petId - * in: path - * required: true - * description: The id of the pet to retrieve - * schema: - * type: string - * responses: - * '200': - * description: Expected response to a valid request - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Pets" - * default: - * description: unexpected error - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Error" - */ - app.get('/pets/:petId', () => {}); -}; diff --git a/test/fixtures/v3/petstore/openapi.json b/test/fixtures/v3/petstore/openapi.json deleted file mode 100644 index 6d359d07..00000000 --- a/test/fixtures/v3/petstore/openapi.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "Sample specification testing petstore" - }, - "paths": { - "/pets": { - "get": { - "summary": "List all pets", - "operationId": "listPets", - "tags": ["pets"], - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "How many items to return at one time (max 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "A paged array of pets", - "headers": { - "x-next": { - "description": "A link to the next page of responses", - "schema": { - "type": "string" - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pets" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - }, - "post": { - "summary": "Create a pet", - "operationId": "createPets", - "tags": ["pets"], - "responses": { - "201": { - "description": "Null response" - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/pets/{petId}": { - "get": { - "summary": "Info for a specific pet", - "operationId": "showPetById", - "tags": ["pets"], - "parameters": [ - { - "name": "petId", - "in": "path", - "required": true, - "description": "The id of the pet to retrieve", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Expected response to a valid request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pets" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "Pet": { - "required": ["id", "name"], - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - }, - "tag": { - "type": "string" - } - } - }, - "Pets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - }, - "Error": { - "required": ["code", "message"], - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - } - } - }, - "tags": [] -} diff --git a/test/lib.spec.js b/test/lib.spec.js index c4c35cbe..91219d54 100644 --- a/test/lib.spec.js +++ b/test/lib.spec.js @@ -1,10 +1,4 @@ -import { dirname } from 'path'; -import { createRequire } from 'module'; -import { fileURLToPath } from 'url'; - import swaggerJsdoc from '../src/lib.js'; -const require = createRequire(import.meta.url); -const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Main lib module', () => { describe('General', () => { @@ -20,124 +14,19 @@ describe('Main lib module', () => { encoding: 'ascii', }); - expect(result).toEqual({ - info: { title: 'Example weird characters', version: '1.0.0' }, - swagger: '2.0', - paths: { - '/no-utf8': { - get: { - description: - "p\u001d\u00175D\u0015E\u0000a87p\u001d\u0019$ a:\u0018a;#p\u001d\u0019'a8;D\u000f", - responses: { - 200: { - description: - 'j\u001e\u000eG\u0012I { - const officialExamples = ['callback', 'links', 'petstore']; - - it('should respect default properties', () => { - const definition = { - openapi: '3.0.0', - servers: [ - { - url: '{scheme}://developer.uspto.gov/ds-api', - variables: { - scheme: { + expect(result.paths).toEqual({ + '/no-utf8': { + get: { + description: + "p\u001d\u00175D\u0015E\u0000a87p\u001d\u0019$ a:\u0018a;#p\u001d\u0019'a8;D\u000f", + responses: { + 200: { description: - 'The Data Set API is accessible via https and http', - enum: ['https', 'http'], - default: 'https', + 'j\u001e\u000eG\u0012I { - it(`Example: ${example}`, () => { - const title = `Sample specification testing ${example}`; - const examplePath = `${__dirname}/fixtures/v3/${example}`; - - const referenceSpecification = require(`${examplePath}/openapi.json`, import.meta - .url); - - const definition = { - openapi: '3.0.0', - info: { - version: '1.0.0', - title, - }, - }; - - const options = { - definition, - apis: [`${examplePath}/api.js`], - }; - - const specification = swaggerJsdoc(options); - expect(specification).toEqual(referenceSpecification); }); }); }); diff --git a/test/specification.spec.js b/test/specification.spec.js index 7e28683f..b85a4215 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -1,4 +1,4 @@ -import { organize, format } from '../src/specification.js'; +import { prepare, organize, format } from '../src/specification.js'; const swaggerObject = { info: { @@ -12,6 +12,35 @@ const swaggerObject = { }; describe('Specification module', () => { + describe('prepare', () => { + it('should produce swagger specification by default: backwards compatibility', () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const result = prepare({ swaggerDefinition: testSpec }); + expect(result.swagger).toBe('2.0'); + expect(result.tags).toEqual([]); + expect(result.paths).toEqual({}); + }); + + it(`should produce swagger specification when 'swagger' property`, () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + testSpec.swagger = '2.0'; + let result = prepare({ swaggerDefinition: testSpec }); + expect(result.swagger).toBe('2.0'); + expect(result.tags).toEqual([]); + expect(result.paths).toEqual({}); + }); + + it(`should produce openapi specification when 'openapi' property`, () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + testSpec.openapi = '3.0'; + const result = prepare({ swaggerDefinition: testSpec }); + expect(result.openapi).toBe('3.0'); + expect(result.tags).toEqual([]); + expect(result.paths).toEqual({}); + expect(result.components).toEqual({}); + }); + }); + describe('organize', () => { it('should support merging', () => { const testSpec = JSON.parse(JSON.stringify(swaggerObject)); From 7dd1a7846f799d5d616a6724ee572dc925eedd02 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Tue, 9 Feb 2021 17:44:23 +0100 Subject: [PATCH 30/41] add spec.clean() tests --- src/utils.js | 2 ++ test/specification.spec.js | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 1228d3a7..41d3babd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -21,6 +21,8 @@ export function convertGlobPaths(globs) { * @returns {boolean} */ export function hasEmptyProperty(obj) { + if (!obj) return; + return Object.keys(obj) .map((key) => obj[key]) .every( diff --git a/test/specification.spec.js b/test/specification.spec.js index b85a4215..0aec8e49 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -1,4 +1,4 @@ -import { prepare, organize, format } from '../src/specification.js'; +import { prepare, organize, format, clean } from '../src/specification.js'; const swaggerObject = { info: { @@ -189,4 +189,26 @@ describe('Specification module', () => { expect(format({ foo: 'bar' }, '.yml')).toEqual('foo: bar\n'); }); }); + + describe('clean', () => { + it('should ensure clean property definitions', () => { + expect(clean({ definitions: { foo: {} } })).toEqual({}); + }); + + it('should ensure clean property responses', () => { + expect(clean({ responses: { foo: {} } })).toEqual({}); + }); + + it('should ensure clean property parameters', () => { + expect(clean({ parameters: { foo: {} } })).toEqual({}); + }); + + it('should ensure clean property securityDefinitions', () => { + expect(clean({ securityDefinitions: { foo: {} } })).toEqual({}); + }); + + it('should not clean other cases', () => { + expect(clean({ misc: { foo: {} } })).toEqual({ misc: { foo: {} } }); + }); + }); }); From e7df61d3201fed3c1fbf9cc8cf41432829569e01 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Tue, 9 Feb 2021 18:09:49 +0100 Subject: [PATCH 31/41] move encoding test at its implementation --- examples/app/app.js | 2 +- examples/cli/cli.js | 2 +- examples/extensions/extensions.spec.js | 2 +- .../yaml-anchors-aliases.spec.js | 2 +- index.js | 25 ++++++++++- src/lib.js | 25 ----------- test/lib.spec.js | 33 --------------- test/utils.spec.js | 42 ++++++++++++++++++- 8 files changed, 68 insertions(+), 65 deletions(-) delete mode 100644 src/lib.js delete mode 100644 test/lib.spec.js diff --git a/examples/app/app.js b/examples/app/app.js index dc69ad46..728ee581 100644 --- a/examples/app/app.js +++ b/examples/app/app.js @@ -5,7 +5,7 @@ import express from 'express'; import bodyParser from 'body-parser'; import { setup as setupRoute1 } from './routes.js'; import { setup as setupRoute2 } from './routes2.js'; -import swaggerJsdoc from '../../src/lib.js'; +import swaggerJsdoc from '../../index.js'; const PORT = process.env.PORT || 3000; diff --git a/examples/cli/cli.js b/examples/cli/cli.js index 4667a174..fb8a4d8b 100755 --- a/examples/cli/cli.js +++ b/examples/cli/cli.js @@ -3,7 +3,7 @@ import { promises as fs } from 'fs'; import { pathToFileURL } from 'url'; import { loadDefinition } from '../../src/utils.js'; -import swaggerJsdoc from '../../src/lib.js'; +import swaggerJsdoc from '../../index.js'; /** * Handle CLI arguments in your preferred way. diff --git a/examples/extensions/extensions.spec.js b/examples/extensions/extensions.spec.js index eac8872d..c9ce14d3 100644 --- a/examples/extensions/extensions.spec.js +++ b/examples/extensions/extensions.spec.js @@ -1,5 +1,5 @@ import { createRequire } from 'module'; -import swaggerJsdoc from '../../src/lib.js'; +import swaggerJsdoc from '../../index.js'; const require = createRequire(import.meta.url); const referenceSpecification = require('./reference-specification.json'); diff --git a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js index 5b369b45..dac35ff2 100644 --- a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js +++ b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js @@ -1,5 +1,5 @@ import { createRequire } from 'module'; -import swaggerJsdoc from '../../src/lib.js'; +import swaggerJsdoc from '../../index.js'; const require = createRequire(import.meta.url); const referenceSpecification = require('./reference-specification.json'); diff --git a/index.js b/index.js index e6504456..1f7546ca 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,25 @@ -import lib from './src/lib.js'; +import { prepare, extract, organize, finalize } from './src/specification.js'; +import { validateOptions } from './src/utils.js'; + +/** + * Main library function + * @param {object} options - Configuration options + * @param {string} options.encoding Optional, passed to readFileSync options. Defaults to 'utf8'. + * @param {string} options.format Optional, defaults to '.json' - target file format '.yml' or '.yaml'. + * @param {object} options.swaggerDefinition + * @param {object} options.definition + * @param {array} options.apis + * @returns {object|string} Output specification as json or yaml + */ +const lib = (options) => { + validateOptions(options); + + const spec = prepare(options); + const parts = extract(options); + + organize(spec, parts); + + return finalize(spec, options); +}; + export default lib; diff --git a/src/lib.js b/src/lib.js deleted file mode 100644 index daa9e154..00000000 --- a/src/lib.js +++ /dev/null @@ -1,25 +0,0 @@ -import { prepare, extract, organize, finalize } from './specification.js'; -import { validateOptions } from './utils.js'; - -/** - * Main library function - * @param {object} options - Configuration options - * @param {string} options.encoding Optional, passed to readFileSync options. Defaults to 'utf8'. - * @param {string} options.format Optional, defaults to '.json' - target file format '.yml' or '.yaml'. - * @param {object} options.swaggerDefinition - * @param {object} options.definition - * @param {array} options.apis - * @returns {object|string} Output specification as json or yaml - */ -const lib = (options) => { - validateOptions(options); - - const spec = prepare(options); - const parts = extract(options); - - organize(spec, parts); - - return finalize(spec, options); -}; - -export default lib; diff --git a/test/lib.spec.js b/test/lib.spec.js deleted file mode 100644 index 91219d54..00000000 --- a/test/lib.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import swaggerJsdoc from '../src/lib.js'; - -describe('Main lib module', () => { - describe('General', () => { - it('should support custom encoding', () => { - const result = swaggerJsdoc({ - swaggerDefinition: { - info: { - title: 'Example weird characters', - version: '1.0.0', - }, - }, - apis: ['./test/fixtures/non-utf-file.js'], - encoding: 'ascii', - }); - - expect(result.paths).toEqual({ - '/no-utf8': { - get: { - description: - "p\u001d\u00175D\u0015E\u0000a87p\u001d\u0019$ a:\u0018a;#p\u001d\u0019'a8;D\u000f", - responses: { - 200: { - description: - 'j\u001e\u000eG\u0012I { }); }); - it('should extract jsdoc comments from empty javascript files/syntax', () => { + it('should return empty arrays from empty javascript files/syntax', () => { expect( extractAnnotations(resolve(__dirname, './fixtures/empty/example.js')) ).toEqual({ @@ -91,6 +90,45 @@ describe('Utilities module', () => { jsdoc: [], }); }); + + it('should respect custom encoding', () => { + expect( + extractAnnotations(resolve(__dirname, './fixtures/non-utf-file.js')) + ).toEqual({ + yaml: [], + jsdoc: [ + '/**\n' + + ' * @swagger\n' + + ' * /no-utf8:\n' + + ' * get:\n' + + ' * description: 𝗵ĕŀḷ𝙤 ẘợ𝙧ḻď\n' + + ' * responses:\n' + + ' * 200:\n' + + ' * description: ꞎǒɼ𝙚ᶆ ịⲣŝừɱ\n' + + ' */', + ], + }); + + expect( + extractAnnotations( + resolve(__dirname, './fixtures/non-utf-file.js'), + 'ascii' + ) + ).toEqual({ + yaml: [], + jsdoc: [ + '/**\n' + + ' * @swagger\n' + + ' * /no-utf8:\n' + + ' * get:\n' + + " * description: p\u001d\u00175D\u0015E\u0000a87p\u001d\u0019$ a:\u0018a;#p\u001d\u0019'a8;D\u000f\n" + + ' * responses:\n' + + ' * 200:\n' + + ' * description: j\u001e\u000eG\u0012I { From 61aa10df7431a26e814f02831f9b4e0f3d0c46c8 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Thu, 11 Feb 2021 17:11:44 +0100 Subject: [PATCH 32/41] add some coverage for the finalize method --- src/specification.js | 6 +++++- test/specification.spec.js | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/specification.js b/src/specification.js index 3cc075ec..2e9dce07 100644 --- a/src/specification.js +++ b/src/specification.js @@ -106,7 +106,11 @@ export function finalize(swaggerObject, options) { specification = clean(specification); } - return format(specification, options.format); + if (options && options.format) { + specification = format(specification, options.format); + } + + return specification; } /** diff --git a/test/specification.spec.js b/test/specification.spec.js index 0aec8e49..7560dd56 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -1,4 +1,10 @@ -import { prepare, organize, format, clean } from '../src/specification.js'; +import { + prepare, + organize, + format, + clean, + finalize, +} from '../src/specification.js'; const swaggerObject = { info: { @@ -211,4 +217,17 @@ describe('Specification module', () => { expect(clean({ misc: { foo: {} } })).toEqual({ misc: { foo: {} } }); }); }); + + describe('finalize', () => { + // Node ESM with Jest and mocking is not possible at this moment + it('should clean up when target specification is openapi', () => { + const spec = { openapi: 'yes, please', parameters: { seeabovewhy: {} } }; + expect(finalize(spec)).toEqual({ openapi: 'yes, please' }); + }); + + it('should call the format method when input options ask for it', () => { + const spec = { openapi: 'yes' }; + expect(finalize(spec, { format: '.yaml' })).toBe('openapi: yes\n'); + }); + }); }); From 8982b3540cc123d32d75aa2d4f39481bc4667de5 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Fri, 12 Feb 2021 18:03:16 +0100 Subject: [PATCH 33/41] Making main function async --- examples/app/app.js | 61 +++++++------- examples/extensions/extensions.spec.js | 4 +- .../yaml-anchors-aliases.spec.js | 4 +- index.js | 4 +- src/specification.js | 4 +- src/utils.js | 10 +-- test/utils.spec.js | 79 +++++++++---------- 7 files changed, 79 insertions(+), 87 deletions(-) diff --git a/examples/app/app.js b/examples/app/app.js index 728ee581..9ebf0938 100644 --- a/examples/app/app.js +++ b/examples/app/app.js @@ -7,6 +7,35 @@ import { setup as setupRoute1 } from './routes.js'; import { setup as setupRoute2 } from './routes2.js'; import swaggerJsdoc from '../../index.js'; +async function surveSwaggerSpecification(req, res) { + // Swagger definition + // You can set every attribute except paths and swagger + // https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md + const swaggerDefinition = { + info: { + // API informations (required) + title: 'Hello World', // Title (required) + version: '1.0.0', // Version (required) + description: 'A sample API', // Description (optional) + }, + host: `localhost:${PORT}`, // Host (optional) + basePath: '/', // Base path (optional) + }; + // Options for the swagger docs + const options = { + // Import swaggerDefinitions + swaggerDefinition, + // Path to the API docs + // Note that this path is relative to the current directory from which the Node.js is ran, not the application itself. + apis: ['./examples/app/routes*.js', './examples/app/parameters.yaml'], + }; + const swaggerSpec = await swaggerJsdoc(options); + + // And here we go, we serve it. + res.setHeader('Content-Type', 'application/json'); + res.send(swaggerSpec); +} + const PORT = process.env.PORT || 3000; // Initialize express @@ -19,37 +48,7 @@ app.use( }) ); -// Swagger definition -// You can set every attribute except paths and swagger -// https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md -const swaggerDefinition = { - info: { - // API informations (required) - title: 'Hello World', // Title (required) - version: '1.0.0', // Version (required) - description: 'A sample API', // Description (optional) - }, - host: `localhost:${PORT}`, // Host (optional) - basePath: '/', // Base path (optional) -}; - -// Options for the swagger docs -const options = { - // Import swaggerDefinitions - swaggerDefinition, - // Path to the API docs - // Note that this path is relative to the current directory from which the Node.js is ran, not the application itself. - apis: ['./examples/app/routes*.js', './examples/app/parameters.yaml'], -}; - -// Initialize swagger-jsdoc -> returns validated swagger spec in json format -const swaggerSpec = swaggerJsdoc(options); - -// Serve swagger docs the way you like (Recommendation: swagger-tools) -app.get('/api-docs.json', (req, res) => { - res.setHeader('Content-Type', 'application/json'); - res.send(swaggerSpec); -}); +app.get('/api-docs.json', surveSwaggerSpecification); // Set up the routes setupRoute1(app); diff --git a/examples/extensions/extensions.spec.js b/examples/extensions/extensions.spec.js index c9ce14d3..eeaa6a13 100644 --- a/examples/extensions/extensions.spec.js +++ b/examples/extensions/extensions.spec.js @@ -5,8 +5,8 @@ const require = createRequire(import.meta.url); const referenceSpecification = require('./reference-specification.json'); describe('Example for using extensions', () => { - it('should support x-webhooks', () => { - const result = swaggerJsdoc({ + it('should support x-webhooks', async () => { + const result = await swaggerJsdoc({ swaggerDefinition: { info: { title: 'Example with extensions', diff --git a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js index dac35ff2..a59de064 100644 --- a/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js +++ b/examples/yaml-anchors-aliases/yaml-anchors-aliases.spec.js @@ -5,8 +5,8 @@ const require = createRequire(import.meta.url); const referenceSpecification = require('./reference-specification.json'); describe('Example for using anchors and aliases in YAML documents', () => { - it('should handle references in a separate YAML file', () => { - const result = swaggerJsdoc({ + it('should handle references in a separate YAML file', async () => { + const result = await swaggerJsdoc({ swaggerDefinition: { info: { title: 'Example with anchors and aliases', diff --git a/index.js b/index.js index 1f7546ca..b2b58440 100644 --- a/index.js +++ b/index.js @@ -11,11 +11,11 @@ import { validateOptions } from './src/utils.js'; * @param {array} options.apis * @returns {object|string} Output specification as json or yaml */ -const lib = (options) => { +const lib = async (options) => { validateOptions(options); const spec = prepare(options); - const parts = extract(options); + const parts = await extract(options); organize(spec, parts); diff --git a/src/specification.js b/src/specification.js index 2e9dce07..4ba2fcb4 100644 --- a/src/specification.js +++ b/src/specification.js @@ -180,7 +180,7 @@ export function organize(swaggerObject, annotations) { * @param {object} options * @returns {object} swaggerObject */ -export function extract(options) { +export async function extract(options) { YAML.defaultOptions.keepCstNodes = true; const yamlDocsAnchors = new Map(); @@ -191,7 +191,7 @@ export function extract(options) { const { yaml: yamlAnnotations, jsdoc: jsdocAnnotations, - } = extractAnnotations(filePath, options.encoding); + } = await extractAnnotations(filePath, options.encoding); if (yamlAnnotations.length) { for (const annotation of yamlAnnotations) { diff --git a/src/utils.js b/src/utils.js index 41d3babd..0e9567fa 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,4 @@ -import { promises as fsp, readFileSync } from 'fs'; +import { promises as fs } from 'fs'; import { createRequire } from 'module'; import { extname } from 'path'; import glob from 'glob'; @@ -53,8 +53,8 @@ export function extractYamlFromJsDoc(jsDocComment) { * @param {string} filePath * @returns {{jsdoc: array, yaml: array}} JSDoc comments and Yaml files */ -export function extractAnnotations(filePath, encoding = 'utf8') { - const fileContent = readFileSync(filePath, { encoding }); +export async function extractAnnotations(filePath, encoding = 'utf8') { + const fileContent = await fs.readFile(filePath, { encoding }); const ext = extname(filePath); const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm; const csDocRegex = /###([\s\S]*?)###/gm; @@ -115,11 +115,11 @@ export async function loadDefinition(definitionPath) { return require(definitionPath); }; const loadJson = async () => { - const fileContents = await fsp.readFile(definitionPath); + const fileContents = await fs.readFile(definitionPath); return JSON.parse(fileContents); }; const loadYaml = async () => { - const fileContents = await fsp.readFile(definitionPath); + const fileContents = await fs.readFile(definitionPath); return yaml.parse(String(fileContents)); }; diff --git a/test/utils.spec.js b/test/utils.spec.js index b24c7ba8..11163c5d 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -27,10 +27,12 @@ describe('Utilities module', () => { }); describe('extractAnnotations', () => { - it('should extract jsdoc comments by default', () => { - expect( - extractAnnotations(resolve(__dirname, '../examples/app/routes2.js')) - ).toEqual({ + it('should extract jsdoc comments by default', async () => { + expect.assertions(1); + const result = await extractAnnotations( + resolve(__dirname, '../examples/app/routes2.js') + ); + expect(result).toEqual({ yaml: [], jsdoc: [ '/**\n * @swagger\n * /hello:\n * get:\n * description: Returns the homepage\n * responses:\n * 200:\n * description: hello world\n */', @@ -38,21 +40,11 @@ describe('Utilities module', () => { }); }); - it('should extract data from YAML files', () => { - expect( - extractAnnotations( - resolve(__dirname, '../examples/app/parameters.yaml') - ) - ).toEqual({ - yaml: [ - 'parameters:\n username:\n name: username\n description: Username to use for login.\n in: formData\n required: true\n type: string\n', - ], - jsdoc: [], - }); - - expect( - extractAnnotations(resolve(__dirname, '../examples/app/parameters.yml')) - ).toEqual({ + it('should extract data from YAML files', async () => { + const result = await extractAnnotations( + resolve(__dirname, '../examples/app/parameters.yaml') + ); + expect(result).toEqual({ yaml: [ 'parameters:\n username:\n name: username\n description: Username to use for login.\n in: formData\n required: true\n type: string\n', ], @@ -60,10 +52,11 @@ describe('Utilities module', () => { }); }); - it('should extract jsdoc comments from coffeescript files/syntax', () => { - expect( - extractAnnotations(resolve(__dirname, '../examples/app/route.coffee')) - ).toEqual({ + it('should extract jsdoc comments from coffeescript files/syntax', async () => { + const result = await extractAnnotations( + resolve(__dirname, '../examples/app/route.coffee') + ); + expect(result).toEqual({ yaml: [], jsdoc: [ '/**\n* @swagger\n* /login:\n* post:\n* description: Login to the application\n* produces:\n* - application/json\n*/', @@ -71,30 +64,31 @@ describe('Utilities module', () => { }); }); - it('should return empty arrays from empty coffeescript files/syntax', () => { - expect( - extractAnnotations( - resolve(__dirname, './fixtures/empty/example.coffee') - ) - ).toEqual({ + it('should return empty arrays from empty coffeescript files/syntax', async () => { + const result = await extractAnnotations( + resolve(__dirname, './fixtures/empty/example.coffee') + ); + expect(result).toEqual({ yaml: [], jsdoc: [], }); }); - it('should return empty arrays from empty javascript files/syntax', () => { - expect( - extractAnnotations(resolve(__dirname, './fixtures/empty/example.js')) - ).toEqual({ + it('should return empty arrays from empty javascript files/syntax', async () => { + const result = await extractAnnotations( + resolve(__dirname, './fixtures/empty/example.js') + ); + expect(result).toEqual({ yaml: [], jsdoc: [], }); }); - it('should respect custom encoding', () => { - expect( - extractAnnotations(resolve(__dirname, './fixtures/non-utf-file.js')) - ).toEqual({ + it('should respect custom encoding', async () => { + const regular = await extractAnnotations( + resolve(__dirname, './fixtures/non-utf-file.js') + ); + expect(regular).toEqual({ yaml: [], jsdoc: [ '/**\n' + @@ -109,12 +103,11 @@ describe('Utilities module', () => { ], }); - expect( - extractAnnotations( - resolve(__dirname, './fixtures/non-utf-file.js'), - 'ascii' - ) - ).toEqual({ + const encoded = await extractAnnotations( + resolve(__dirname, './fixtures/non-utf-file.js'), + 'ascii' + ); + expect(encoded).toEqual({ yaml: [], jsdoc: [ '/**\n' + From f29f8a26283f51f2d648171aa3451b5f775c959b Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sat, 13 Feb 2021 16:50:24 +0100 Subject: [PATCH 34/41] Add some coverage --- index.js | 4 +-- src/utils.js | 2 +- test/utils.spec.js | 90 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index b2b58440..a1a2536a 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,9 @@ import { prepare, extract, organize, finalize } from './src/specification.js'; import { validateOptions } from './src/utils.js'; /** - * Main library function + * Main function * @param {object} options - Configuration options - * @param {string} options.encoding Optional, passed to readFileSync options. Defaults to 'utf8'. + * @param {string} options.encoding Optional, passed to read file function options. Defaults to 'utf8'. * @param {string} options.format Optional, defaults to '.json' - target file format '.yml' or '.yaml'. * @param {object} options.swaggerDefinition * @param {object} options.definition diff --git a/src/utils.js b/src/utils.js index 0e9567fa..60b1a4a2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -92,13 +92,13 @@ export async function extractAnnotations(filePath, encoding = 'utf8') { /** * @param {object} tag + * @param {string} tag.name * @param {array} tags * @returns {boolean} */ export function isTagPresentInTags(tag, tags) { const match = tags.find((targetTag) => tag.name === targetTag.name); if (match) return true; - return false; } diff --git a/test/utils.spec.js b/test/utils.spec.js index 11163c5d..a8f64e68 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -1,6 +1,8 @@ import { extractAnnotations, + extractYamlFromJsDoc, hasEmptyProperty, + isTagPresentInTags, loadDefinition, validateOptions, } from '../src/utils.js'; @@ -26,6 +28,52 @@ describe('Utilities module', () => { }); }); + describe('extractYamlFromJsDoc', () => { + it('should handle items annotated by @swagger', () => { + const example = { + description: '', + tags: [ + { + title: 'swagger', + description: + '/:\n get:\n description: Returns the homepage\n responses:\n 200:\n description: hello world', + }, + ], + }; + const result = extractYamlFromJsDoc(example); + expect(result).toEqual([ + '/:\n' + + ' get:\n' + + ' description: Returns the homepage\n' + + ' responses:\n' + + ' 200:\n' + + ' description: hello world', + ]); + }); + + it('should handle items annotated by @openapi', () => { + const example = { + description: '', + tags: [ + { + title: 'openapi', + description: + '/:\n get:\n description: Returns the homepage\n responses:\n 200:\n description: hello world', + }, + ], + }; + const result = extractYamlFromJsDoc(example); + expect(result).toEqual([ + '/:\n' + + ' get:\n' + + ' description: Returns the homepage\n' + + ' responses:\n' + + ' 200:\n' + + ' description: hello world', + ]); + }); + }); + describe('extractAnnotations', () => { it('should extract jsdoc comments by default', async () => { expect.assertions(1); @@ -41,7 +89,7 @@ describe('Utilities module', () => { }); it('should extract data from YAML files', async () => { - const result = await extractAnnotations( + let result = await extractAnnotations( resolve(__dirname, '../examples/app/parameters.yaml') ); expect(result).toEqual({ @@ -50,6 +98,16 @@ describe('Utilities module', () => { ], jsdoc: [], }); + + result = await extractAnnotations( + resolve(__dirname, '../examples/app/parameters.yml') + ); + expect(result).toEqual({ + yaml: [ + 'parameters:\n username:\n name: username\n description: Username to use for login.\n in: formData\n required: true\n type: string\n', + ], + jsdoc: [], + }); }); it('should extract jsdoc comments from coffeescript files/syntax', async () => { @@ -124,6 +182,36 @@ describe('Utilities module', () => { }); }); + describe('isTagPresentInTags', () => { + it(`should be true when it's true`, () => { + expect( + isTagPresentInTags( + { name: 'Users', description: 'User management and login' }, + [ + { + name: 'Users', + description: 'User management and login', + }, + ] + ) + ).toBe(true); + }); + + it(`should be false when it's false`, () => { + expect( + isTagPresentInTags( + { name: 'User', description: 'User management and login' }, + [ + { + name: 'Users', + description: 'User management and login', + }, + ] + ) + ).toBe(false); + }); + }); + describe('loadDefinition', () => { const example = './fixtures/swaggerDefinition/example'; From 0df97c5421fd6801bde9f34fb7c21d334ab4fe59 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sat, 13 Feb 2021 17:09:21 +0100 Subject: [PATCH 35/41] 100% on utilities, yey --- test/utils.spec.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/utils.spec.js b/test/utils.spec.js index a8f64e68..24ab0283 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -29,6 +29,20 @@ describe('Utilities module', () => { }); describe('extractYamlFromJsDoc', () => { + it('should not handle false cases', () => { + expect( + extractYamlFromJsDoc({ + description: '', + tags: [ + { + title: 'coverage', + description: 'for else path', + }, + ], + }) + ).toEqual([]); + }); + it('should handle items annotated by @swagger', () => { const example = { description: '', @@ -335,11 +349,17 @@ describe('Utilities module', () => { }); it('should return original options on valid input', () => { - const options = { + let options = { swaggerDefinition: { info: { version: '', title: '' } }, apis: [], }; expect(validateOptions(options)).toEqual(options); + + options = { + definition: { info: { version: '', title: '' } }, + apis: [], + }; + expect(validateOptions(options)).toEqual(options); }); }); }); From 8b5b5f287b32671668b4fdc41cda111010622052 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 14 Feb 2021 12:03:25 +0100 Subject: [PATCH 36/41] add tests for spec.extract() --- .prettierignore | 3 + package.json | 1 - src/specification.js | 11 +++ test/fixtures/wrong/example.js | 12 +++ test/fixtures/wrong/example.yaml | 5 + test/specification.spec.js | 153 ++++++++++++++++++++++++------- 6 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 .prettierignore create mode 100644 test/fixtures/wrong/example.js create mode 100644 test/fixtures/wrong/example.yaml diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..6bd4403d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +test/fixtures/wrong/example.js +test/fixtures/wrong/example.json +test/fixtures/wrong/example.yaml diff --git a/package.json b/package.json index 63c63397..3d058eea 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "type": "module", "exports": "./index.js", "jest": { - "verbose": true, "transform": {} }, "dependencies": { diff --git a/src/specification.js b/src/specification.js index 4ba2fcb4..fa5e8a86 100644 --- a/src/specification.js +++ b/src/specification.js @@ -181,6 +181,17 @@ export function organize(swaggerObject, annotations) { * @returns {object} swaggerObject */ export async function extract(options) { + if ( + !options || + !options.apis || + options.apis.length === 0 || + Array.isArray(options.apis) === false + ) { + throw new Error( + 'Bad input parameter: options is required, as well as options.apis[]' + ); + } + YAML.defaultOptions.keepCstNodes = true; const yamlDocsAnchors = new Map(); diff --git a/test/fixtures/wrong/example.js b/test/fixtures/wrong/example.js new file mode 100644 index 00000000..e98d9fb1 --- /dev/null +++ b/test/fixtures/wrong/example.js @@ -0,0 +1,12 @@ +/* istanbul ignore file */ + +module.exports = (app) => { + /** + * @swagger + * + * /invalid_yaml: + * - foo + * bar + */ + app.get('/invalid', () => {}); +}; diff --git a/test/fixtures/wrong/example.yaml b/test/fixtures/wrong/example.yaml new file mode 100644 index 00000000..6e6bf19b --- /dev/null +++ b/test/fixtures/wrong/example.yaml @@ -0,0 +1,5 @@ +info: + !!!title: Hello World + version: 1.0.0 + description: A sample API +apis: [./**/*/routes.js] diff --git a/test/specification.spec.js b/test/specification.spec.js index 7560dd56..55ea6ebd 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -1,9 +1,11 @@ +import jest from 'jest-mock'; import { prepare, organize, format, clean, finalize, + extract, } from '../src/specification.js'; const swaggerObject = { @@ -47,6 +49,52 @@ describe('Specification module', () => { }); }); + describe('format', () => { + it('should not modify input object when no format specified', () => { + expect(format({ foo: 'bar' })).toEqual({ foo: 'bar' }); + }); + + it('should support yaml', () => { + expect(format({ foo: 'bar' }, '.yaml')).toEqual('foo: bar\n'); + expect(format({ foo: 'bar' }, '.yml')).toEqual('foo: bar\n'); + }); + }); + + describe('clean', () => { + it('should ensure clean property definitions', () => { + expect(clean({ definitions: { foo: {} } })).toEqual({}); + }); + + it('should ensure clean property responses', () => { + expect(clean({ responses: { foo: {} } })).toEqual({}); + }); + + it('should ensure clean property parameters', () => { + expect(clean({ parameters: { foo: {} } })).toEqual({}); + }); + + it('should ensure clean property securityDefinitions', () => { + expect(clean({ securityDefinitions: { foo: {} } })).toEqual({}); + }); + + it('should not clean other cases', () => { + expect(clean({ misc: { foo: {} } })).toEqual({ misc: { foo: {} } }); + }); + }); + + describe('finalize', () => { + // Node ESM with Jest and mocking is not possible at this moment + it('should clean up when target specification is openapi', () => { + const spec = { openapi: 'yes, please', parameters: { seeabovewhy: {} } }; + expect(finalize(spec)).toEqual({ openapi: 'yes, please' }); + }); + + it('should call the format method when input options ask for it', () => { + const spec = { openapi: 'yes' }; + expect(finalize(spec, { format: '.yaml' })).toBe('openapi: yes\n'); + }); + }); + describe('organize', () => { it('should support merging', () => { const testSpec = JSON.parse(JSON.stringify(swaggerObject)); @@ -185,49 +233,90 @@ describe('Specification module', () => { }); }); - describe('format', () => { - it('should not modify input object when no format specified', () => { - expect(format({ foo: 'bar' })).toEqual({ foo: 'bar' }); - }); + describe('extract', () => { + it('should throw on bad input', async () => { + await expect(extract()).rejects.toThrow( + 'Bad input parameter: options is required, as well as options.apis[]' + ); - it('should support yaml', () => { - expect(format({ foo: 'bar' }, '.yaml')).toEqual('foo: bar\n'); - expect(format({ foo: 'bar' }, '.yml')).toEqual('foo: bar\n'); - }); - }); + await expect(extract({})).rejects.toThrow( + 'Bad input parameter: options is required, as well as options.apis[]' + ); - describe('clean', () => { - it('should ensure clean property definitions', () => { - expect(clean({ definitions: { foo: {} } })).toEqual({}); - }); + await expect(extract({ apis: {} })).rejects.toThrow( + 'Bad input parameter: options is required, as well as options.apis[]' + ); - it('should ensure clean property responses', () => { - expect(clean({ responses: { foo: {} } })).toEqual({}); + await expect(extract({ apis: [] })).rejects.toThrow( + 'Bad input parameter: options is required, as well as options.apis[]' + ); }); - it('should ensure clean property parameters', () => { - expect(clean({ parameters: { foo: {} } })).toEqual({}); + it('should extract annotations', async () => { + const annotations = await extract({ apis: ['./examples/app/routes.js'] }); + expect(annotations.length).toBe(7); }); - it('should ensure clean property securityDefinitions', () => { - expect(clean({ securityDefinitions: { foo: {} } })).toEqual({}); - }); + it('should report issues: js files case', async () => { + const consoleInfo = jest.spyOn(console, 'info'); + const consoleErr = jest.spyOn(console, 'error'); + const annotations = await extract({ + apis: ['./test/fixtures/wrong/example.js'], + }); + expect(annotations.length).toBe(0); + expect(consoleInfo).toHaveBeenCalledWith( + 'Not all input has been taken into account at your final specification.' + ); + expect(consoleErr.mock.calls).toEqual([ + [ + "Here's the report: \n" + + '\n' + + '\n' + + ' YAMLSyntaxError: All collection items must start at the same column at line 1, column 1:\n' + + '\n' + + '/invalid_yaml:\n' + + '^^^^^^^^^^^^^^…\n' + + '\n' + + 'YAMLSemanticError: Implicit map keys need to be followed by map values at line 3, column 3:\n' + + '\n' + + ' bar\n' + + ' ^^^\n', + ], + ]); - it('should not clean other cases', () => { - expect(clean({ misc: { foo: {} } })).toEqual({ misc: { foo: {} } }); + consoleInfo.mockClear(); + consoleErr.mockClear(); }); - }); - describe('finalize', () => { - // Node ESM with Jest and mocking is not possible at this moment - it('should clean up when target specification is openapi', () => { - const spec = { openapi: 'yes, please', parameters: { seeabovewhy: {} } }; - expect(finalize(spec)).toEqual({ openapi: 'yes, please' }); - }); + it('should report issues: yaml files case', async () => { + const consoleInfo = jest.spyOn(console, 'info'); + const consoleErr = jest.spyOn(console, 'error'); + const annotations = await extract({ + apis: ['./test/fixtures/wrong/example.yaml'], + }); + expect(annotations.length).toBe(0); + expect(consoleInfo).toHaveBeenCalledWith( + 'Not all input has been taken into account at your final specification.' + ); + expect(consoleErr.mock.calls).toEqual([ + [ + "Here's the report: \n" + + '\n' + + '\n' + + ' YAMLSemanticError: The !!! tag handle is non-default and was not declared. at line 2, column 3:\n' + + '\n' + + ' !!!title: Hello World\n' + + ' ^^^^^^^^^^^^^^^^^^^^^…\n' + + '\n' + + 'YAMLSemanticError: Implicit map keys need to be on a single line at line 2, column 3:\n' + + '\n' + + ' !!!title: Hello World\n' + + ' ^^^^^^^^^^^^^^^^^^^^^…\n', + ], + ]); - it('should call the format method when input options ask for it', () => { - const spec = { openapi: 'yes' }; - expect(finalize(spec, { format: '.yaml' })).toBe('openapi: yes\n'); + consoleInfo.mockClear(); + consoleErr.mockClear(); }); }); }); From b4bb70d56137820e97fb64e317954edab3445a16 Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Sun, 14 Feb 2021 12:49:13 +0100 Subject: [PATCH 37/41] improve coverage --- examples/yaml-anchors-aliases/example.js | 30 +++++++++++++++++++ .../reference-specification.json | 17 +++++++++++ test/specification.spec.js | 8 +++++ 3 files changed, 55 insertions(+) diff --git a/examples/yaml-anchors-aliases/example.js b/examples/yaml-anchors-aliases/example.js index 0ee64d41..a94cec7a 100644 --- a/examples/yaml-anchors-aliases/example.js +++ b/examples/yaml-anchors-aliases/example.js @@ -14,4 +14,34 @@ module.exports = (app) => { * x-amazon-apigateway-integration: *default-integration */ app.get('/aws', () => {}); + + /** + * @swagger + * /richie-rich: + * get: + * summary: another route + * description: contains a reference in the same file + * security: [] + * responses: + * 200: + * description: OK + * x-amazon-another-integration: *another-integration + */ + app.get('/richie-rich', () => {}); }; + +/** + * The following annotation is an example of a jsdoc containing a yaml cotaining an anchor. + * The place should not be relevant, and that's why it's later than its usage. + * + * @swagger + * x-amazon-another-example: + * another-integration: &another-integration + * type: object + * x-amazon-another-example: + * httpMethod: POST + * passthroughBehavior: when_no_match + * type: aws_proxy + * uri: 'irrelevant' + * + */ diff --git a/examples/yaml-anchors-aliases/reference-specification.json b/examples/yaml-anchors-aliases/reference-specification.json index 479ce5ee..081b4814 100644 --- a/examples/yaml-anchors-aliases/reference-specification.json +++ b/examples/yaml-anchors-aliases/reference-specification.json @@ -18,6 +18,23 @@ } } } + }, + "/richie-rich": { + "get": { + "summary": "another route", + "description": "contains a reference in the same file", + "security": [], + "responses": { "200": { "description": "OK" } }, + "x-amazon-another-integration": { + "type": "object", + "x-amazon-another-example": { + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": "irrelevant" + } + } + } } }, "definitions": {}, diff --git a/test/specification.spec.js b/test/specification.spec.js index 55ea6ebd..cc3e724b 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -29,6 +29,14 @@ describe('Specification module', () => { expect(result.paths).toEqual({}); }); + it('should accept also definition property istead of a swaggerDefinition', () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const result = prepare({ definition: testSpec }); + expect(result.swagger).toBe('2.0'); + expect(result.tags).toEqual([]); + expect(result.paths).toEqual({}); + }); + it(`should produce swagger specification when 'swagger' property`, () => { const testSpec = JSON.parse(JSON.stringify(swaggerObject)); testSpec.swagger = '2.0'; From 7a048d94585b1177fd550fad1997d4b529d278dc Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Tue, 16 Feb 2021 09:46:06 +0100 Subject: [PATCH 38/41] Update landing page readme --- README.md | 35 +++++++++++++++++++++++------------ docs/screenshot.png | Bin 0 -> 107629 bytes 2 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 docs/screenshot.png diff --git a/README.md b/README.md index c2d7d157..ca9e3e31 100644 --- a/README.md +++ b/README.md @@ -7,29 +7,45 @@ This library reads your [JSDoc](https://jsdoc.app/)-annotated source code and ge ## Getting started -`swagger-jsdoc` returns the validated OpenAPI specification as JSON or YAML. +Imagine having API files like these: + +```javascript +/** + * @openapi + * /: + * get: + * description: Welcome to swagger-jsdoc! + * responses: + * 200: + * description: Returns a mysterious string. + */ +app.get('/', (req, res) => { + res.send('Hello World!'); +}); +``` + +The library will take the contents of `@openapi` (or `@swagger`) with the following configuration: ```javascript const swaggerJsdoc = require('swagger-jsdoc'); const options = { - swaggerDefinition: { + definition: { openapi: '3.0.0', info: { title: 'Hello World', version: '1.0.0', }, }, - apis: ['./src/routes*.js'], + apis: ['./src/routes*.js'], // files containing annotations as above }; -const swaggerSpecification = swaggerJsdoc(options); +const openapiSpecification = swaggerJsdoc(options); ``` -- `options.definition` is also acceptable. Pass an [oasObject](https://swagger.io/specification/#oasObject) -- `options.apis` are resolved with [node-glob](https://github.com/isaacs/node-glob). Construct these patterns carefully in order to reduce the number of possible matches speeding up files' discovery. Values are relative to the current working directory. +The resulting `openapiSpecification` will be a [swagger tools](https://swagger.io/tools/)-compatible (and validated) specification. -Use any of the [swagger tools](https://swagger.io/tools/) to get the benefits of your `swaggerSpecification`. +![swagger-jsdoc example screenshot](./docs/screenshot.png) ## Node.js version requirements, CommonJS and ESM @@ -61,8 +77,3 @@ yarn add swagger-jsdoc ## Documentation Detailed documentation is available within [`/docs`](https://github.com/Surnet/swagger-jsdoc/tree/master/docs/README.md) folder. - -### Webpack integrations - -- [swagger-jsdoc-webpack-plugin](https://github.com/patsimm/swagger-jsdoc-webpack-plugin) - Rebuild the swagger definition based on a predefined list of files on each webpack build. -- [swagger-jsdoc-sync-webpack-plugin](https://github.com/gautier-lefebvre/swagger-jsdoc-sync-webpack-plugin) - Rebuild the swagger definition based on the files imported in your app on each webpack build. diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1c491df6ea3e17d922048241bc12d052c783fa52 GIT binary patch literal 107629 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 Date: Tue, 16 Feb 2021 11:19:15 +0100 Subject: [PATCH 39/41] full coverage, on paper --- src/specification.js | 3 ++ test/specification.spec.js | 99 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/specification.js b/src/specification.js index fa5e8a86..3a3ea39a 100644 --- a/src/specification.js +++ b/src/specification.js @@ -152,6 +152,9 @@ export function organize(swaggerObject, annotations) { }; } } else if (property === 'tags') { + if (swaggerObject.tags === undefined) { + swaggerObject.tags = []; + } const { tags } = annotation; if (Array.isArray(tags)) { diff --git a/test/specification.spec.js b/test/specification.spec.js index cc3e724b..c0b01257 100644 --- a/test/specification.spec.js +++ b/test/specification.spec.js @@ -239,6 +239,105 @@ describe('Specification module', () => { IllegalInput: { description: 'Illegal input for operation.' }, }); }); + + it('should handle "tags": case merge', () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + tags: { + name: 'Users', + description: 'User management and login', + }, + }, + { + tags: [ + { + name: 'Login', + description: 'Login', + }, + { + name: 'Accounts', + description: 'Accounts', + }, + ], + }, + ]; + organize(testSpec, annotations); + expect(testSpec.tags).toEqual([ + { + name: 'Users', + description: 'User management and login', + }, + { + name: 'Login', + description: 'Login', + }, + { + name: 'Accounts', + description: 'Accounts', + }, + ]); + }); + + it('should handle "tags": case objects', () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + tags: { + name: 'Users', + description: 'User management and login', + }, + }, + { + tags: { + name: 'Users', + description: 'Should not be taken into account, i.e. no override', + }, + }, + ]; + organize(testSpec, annotations); + expect(testSpec.tags).toEqual([ + { + name: 'Users', + description: 'User management and login', + }, + ]); + }); + + it('should handle "tags": case arrays', () => { + const testSpec = JSON.parse(JSON.stringify(swaggerObject)); + const annotations = [ + { + tags: { + name: 'Users', + description: 'User management and login', + }, + }, + { + tags: [ + { + name: 'Users', + description: 'Should not be taken into account, i.e. no override', + }, + { + name: 'Login', + description: 'Login', + }, + ], + }, + ]; + organize(testSpec, annotations); + expect(testSpec.tags).toEqual([ + { + name: 'Users', + description: 'User management and login', + }, + { + name: 'Login', + description: 'Login', + }, + ]); + }); }); describe('extract', () => { From e6f09d3d29ae3da50e836c12ff130210dccd05ec Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Wed, 17 Feb 2021 09:21:49 +0100 Subject: [PATCH 40/41] move loadDefinition --- examples/cli/cli.js | 5 +- examples/cli/cli.spec.js | 95 ++++++++++++++++--- examples/cli/utils.js | 49 ++++++++++ .../swaggerDefinition/example.cjs | 0 .../swaggerDefinition/example.js | 0 .../swaggerDefinition/example.json | 0 .../swaggerDefinition/example.mjs | 0 .../swaggerDefinition/example.yaml | 0 src/utils.js | 47 --------- test/utils.spec.js | 71 -------------- 10 files changed, 137 insertions(+), 130 deletions(-) create mode 100644 examples/cli/utils.js rename {test/fixtures => examples}/swaggerDefinition/example.cjs (100%) rename {test/fixtures => examples}/swaggerDefinition/example.js (100%) rename {test/fixtures => examples}/swaggerDefinition/example.json (100%) rename {test/fixtures => examples}/swaggerDefinition/example.mjs (100%) rename {test/fixtures => examples}/swaggerDefinition/example.yaml (100%) diff --git a/examples/cli/cli.js b/examples/cli/cli.js index fb8a4d8b..51d3ac3e 100755 --- a/examples/cli/cli.js +++ b/examples/cli/cli.js @@ -1,8 +1,8 @@ #!/usr/bin/env node import { promises as fs } from 'fs'; import { pathToFileURL } from 'url'; +import { loadDefinition } from './utils.js'; -import { loadDefinition } from '../../src/utils.js'; import swaggerJsdoc from '../../index.js'; /** @@ -26,6 +26,9 @@ const definitionUrl = pathToFileURL( // Because "Parsing error: Cannot use keyword 'await' outside an async function" (async () => { + /** + * We're using an example module loader which you can swap with your own implemenentation. + */ const swaggerDefinition = await loadDefinition(definitionUrl.href); // Extract apis diff --git a/examples/cli/cli.spec.js b/examples/cli/cli.spec.js index c60d52c3..742323bd 100644 --- a/examples/cli/cli.spec.js +++ b/examples/cli/cli.spec.js @@ -1,22 +1,25 @@ import { promises as fs } from 'fs'; import { createRequire } from 'module'; -import { dirname } from 'path'; +import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; -import { promisify } from 'util'; -import { exec } from 'child_process'; +import { spawnSync } from 'child_process'; +import { loadDefinition } from './utils.js'; -const sh = promisify(exec); const require = createRequire(import.meta.url); const __dirname = dirname(fileURLToPath(import.meta.url)); -const bin = `node ${__dirname}/cli.js`; +const cli = `${__dirname}/cli.js`; describe('Example command line application', () => { - it('should produce results matching reference specification', async () => { - const result = await sh( - `${bin} --definition test/fixtures/swaggerDefinition/example.js --apis examples/app/parameters.* examples/app/route*` - ); - expect(result.stderr).toBe(''); - expect(result.stdout).toBe( + it('should produce results matching reference specification', () => { + const { stderr, stdout } = spawnSync(cli, [ + '--definition', + 'examples/swaggerDefinition/example.js', + '--apis', + 'examples/app/parameters.*', + 'examples/app/route*', + ]); + expect(stderr.toString()).toBe(''); + expect(stdout.toString()).toBe( 'Specification has been created successfully!\n' ); const refSpec = require('./reference-specification.json'); @@ -28,3 +31,73 @@ describe('Example command line application', () => { await fs.unlink(`${process.cwd()}/swagger.json`); }); }); + +describe('loadDefinition', () => { + const example = '../swaggerDefinition/example'; + + it('should throw on bad input', async () => { + await expect(loadDefinition('bad/path/to/nowhere')).rejects.toThrow( + 'Definition file should be any of the following: .js, .mjs, .cjs, .json, .yml, .yaml' + ); + }); + + it('should support .json', async () => { + const def = resolve(__dirname, `${example}.json`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .yaml', async () => { + const def = resolve(__dirname, `${example}.yaml`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .js', async () => { + const def = resolve(__dirname, `${example}.js`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .cjs', async () => { + const def = resolve(__dirname, `${example}.cjs`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); + + it('should support .mjs', async () => { + const def = resolve(__dirname, `${example}.mjs`); + const result = await loadDefinition(def); + expect(result).toEqual({ + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + }); + }); +}); diff --git a/examples/cli/utils.js b/examples/cli/utils.js new file mode 100644 index 00000000..88ea6000 --- /dev/null +++ b/examples/cli/utils.js @@ -0,0 +1,49 @@ +import { createRequire } from 'module'; +import { extname } from 'path'; +import { promises as fs } from 'fs'; +import yaml from 'yaml'; + +/** + * @param {string} definitionPath path to the swaggerDefinition + */ +export async function loadDefinition(definitionPath) { + const loadModule = async () => { + const esmodule = await import(definitionPath); + return esmodule.default; + }; + const loadCJS = () => { + const require = createRequire(import.meta.url); + return require(definitionPath); + }; + const loadJson = async () => { + const fileContents = await fs.readFile(definitionPath); + return JSON.parse(fileContents); + }; + const loadYaml = async () => { + const fileContents = await fs.readFile(definitionPath); + return yaml.parse(String(fileContents)); + }; + + const LOADERS = { + '.js': loadModule, + '.mjs': loadModule, + '.cjs': loadCJS, + '.json': loadJson, + '.yml': loadYaml, + '.yaml': loadYaml, + }; + + const loader = LOADERS[extname(definitionPath)]; + + if (loader === undefined) { + throw new Error( + `Definition file should be any of the following: ${Object.keys( + LOADERS + ).join(', ')}` + ); + } + + const result = await loader(); + + return result; +} diff --git a/test/fixtures/swaggerDefinition/example.cjs b/examples/swaggerDefinition/example.cjs similarity index 100% rename from test/fixtures/swaggerDefinition/example.cjs rename to examples/swaggerDefinition/example.cjs diff --git a/test/fixtures/swaggerDefinition/example.js b/examples/swaggerDefinition/example.js similarity index 100% rename from test/fixtures/swaggerDefinition/example.js rename to examples/swaggerDefinition/example.js diff --git a/test/fixtures/swaggerDefinition/example.json b/examples/swaggerDefinition/example.json similarity index 100% rename from test/fixtures/swaggerDefinition/example.json rename to examples/swaggerDefinition/example.json diff --git a/test/fixtures/swaggerDefinition/example.mjs b/examples/swaggerDefinition/example.mjs similarity index 100% rename from test/fixtures/swaggerDefinition/example.mjs rename to examples/swaggerDefinition/example.mjs diff --git a/test/fixtures/swaggerDefinition/example.yaml b/examples/swaggerDefinition/example.yaml similarity index 100% rename from test/fixtures/swaggerDefinition/example.yaml rename to examples/swaggerDefinition/example.yaml diff --git a/src/utils.js b/src/utils.js index 60b1a4a2..90f70e05 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,6 @@ import { promises as fs } from 'fs'; -import { createRequire } from 'module'; import { extname } from 'path'; import glob from 'glob'; -import yaml from 'yaml'; /** * Converts an array of globs to full paths @@ -102,51 +100,6 @@ export function isTagPresentInTags(tag, tags) { return false; } -/** - * @param {string} definitionPath path to the swaggerDefinition - */ -export async function loadDefinition(definitionPath) { - const loadModule = async () => { - const esmodule = await import(definitionPath); - return esmodule.default; - }; - const loadCJS = () => { - const require = createRequire(import.meta.url); - return require(definitionPath); - }; - const loadJson = async () => { - const fileContents = await fs.readFile(definitionPath); - return JSON.parse(fileContents); - }; - const loadYaml = async () => { - const fileContents = await fs.readFile(definitionPath); - return yaml.parse(String(fileContents)); - }; - - const LOADERS = { - '.js': loadModule, - '.mjs': loadModule, - '.cjs': loadCJS, - '.json': loadJson, - '.yml': loadYaml, - '.yaml': loadYaml, - }; - - const loader = LOADERS[extname(definitionPath)]; - - if (loader === undefined) { - throw new Error( - `Definition file should be any of the following: ${Object.keys( - LOADERS - ).join(', ')}` - ); - } - - const result = await loader(); - - return result; -} - /** * @param {object} options * @returns {object} the original input if valid, throws otherwise diff --git a/test/utils.spec.js b/test/utils.spec.js index 24ab0283..d13fb8e5 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -3,7 +3,6 @@ import { extractYamlFromJsDoc, hasEmptyProperty, isTagPresentInTags, - loadDefinition, validateOptions, } from '../src/utils.js'; @@ -226,76 +225,6 @@ describe('Utilities module', () => { }); }); - describe('loadDefinition', () => { - const example = './fixtures/swaggerDefinition/example'; - - it('should throw on bad input', async () => { - await expect(loadDefinition('bad/path/to/nowhere')).rejects.toThrow( - 'Definition file should be any of the following: .js, .mjs, .cjs, .json, .yml, .yaml' - ); - }); - - it('should support .json', async () => { - const def = resolve(__dirname, `${example}.json`); - const result = await loadDefinition(def); - expect(result).toEqual({ - info: { - title: 'Hello World', - version: '1.0.0', - description: 'A sample API', - }, - }); - }); - - it('should support .yaml', async () => { - const def = resolve(__dirname, `${example}.yaml`); - const result = await loadDefinition(def); - expect(result).toEqual({ - info: { - title: 'Hello World', - version: '1.0.0', - description: 'A sample API', - }, - }); - }); - - it('should support .js', async () => { - const def = resolve(__dirname, `${example}.js`); - const result = await loadDefinition(def); - expect(result).toEqual({ - info: { - title: 'Hello World', - version: '1.0.0', - description: 'A sample API', - }, - }); - }); - - it('should support .cjs', async () => { - const def = resolve(__dirname, `${example}.cjs`); - const result = await loadDefinition(def); - expect(result).toEqual({ - info: { - title: 'Hello World', - version: '1.0.0', - description: 'A sample API', - }, - }); - }); - - it('should support .mjs', async () => { - const def = resolve(__dirname, `${example}.mjs`); - const result = await loadDefinition(def); - expect(result).toEqual({ - info: { - title: 'Hello World', - version: '1.0.0', - description: 'A sample API', - }, - }); - }); - }); - describe('validateOptions', () => { it('should throw on empty input', () => { expect(() => { From 1da74cdaf88049bdf1782d04c54b67e3c5aa0c4d Mon Sep 17 00:00:00 2001 From: Kalin Chernev Date: Wed, 17 Feb 2021 09:56:42 +0100 Subject: [PATCH 41/41] Update docs --- .github/workflows/ci.yml | 2 +- README.md | 12 +++--------- docs/CONCEPTS.md | 14 ++++++++------ docs/FIRST-STEPS.md | 6 +++--- docs/README.md | 1 - package.json | 2 +- 6 files changed, 16 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a429a9e..07456f5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [14.x, 15.x] + node-version: [12.x, 14.x, 15.x] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index ca9e3e31..de81f5f8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ app.get('/', (req, res) => { The library will take the contents of `@openapi` (or `@swagger`) with the following configuration: ```javascript -const swaggerJsdoc = require('swagger-jsdoc'); +import swaggerJsdoc from 'swagger-jsdoc'; const options = { definition: { @@ -47,15 +47,9 @@ The resulting `openapiSpecification` will be a [swagger tools](https://swagger.i ![swagger-jsdoc example screenshot](./docs/screenshot.png) -## Node.js version requirements, CommonJS and ESM +## System requirements -`swagger-jsdoc` 6.x requires Node.js 12.x and above. When using the CLI, the library will attempt to load the definition file in several formats: `.js`, `.cjs`, `.yaml` (or `.yml`) and `.json`. - -The example above follows the CommonJS format, which will work when you do not have `"type": "module"` in your `package.json`. - -However, if you're using ESM and have `"type": "module"`, then please change the extension to `.cjs`. - -Definition files in `.js` and ESM will be supported in `swagger-jsdoc` 7.x. +Notes on CJS and ESM. ## Installation diff --git a/docs/CONCEPTS.md b/docs/CONCEPTS.md index 3e1bd76b..eed78fe5 100644 --- a/docs/CONCEPTS.md +++ b/docs/CONCEPTS.md @@ -12,10 +12,10 @@ Parts of the specification can be placed in annotated JSDoc comments in non-comp Other parts of the specification can be directly written in YAML files. These are usually parts containing static definitions which are referenced from jsDoc comments parameters, components, anchors, etc. which are not so relevant to the API implementation. -Given the following definition `swaggerDefinition.cjs`: +Given the following definition `definition.js`: ```javascript -module.exports = { +export default { info: { title: 'Hello World', version: '1.0.0', @@ -24,20 +24,22 @@ module.exports = { }; ``` -The end `swaggerSpecification` will be a result of following: +The end `openapiSpecification` will be a result of following: ```javascript -const swaggerJsdoc = require('swagger-jsdoc'); -const swaggerDefinition = require('./swaggerDefinition'); +import swaggerJsdoc from 'swagger-jsdoc'; +import definition from './definition.js'; const options = { - swaggerDefinition, + definition, apis: ['./src/routes*.js'], }; const swaggerSpecification = swaggerJsdoc(options); ``` +Please note that it's also possible to use CommonJS syntax with `require` and `module.exports` for the example above. + ## File selection patterns `swagger-jsdoc` uses [node glob](https://github.com/isaacs/node-glob) for discovering your input files. You can use patterns such as `*.js` to select all javascript files or `**/*.js` to select all javascript files in sub-folders recursively. diff --git a/docs/FIRST-STEPS.md b/docs/FIRST-STEPS.md index 91e66462..abfefa0a 100644 --- a/docs/FIRST-STEPS.md +++ b/docs/FIRST-STEPS.md @@ -7,10 +7,10 @@ The default target specification is 2.0. This provides backwards compatibility f In order to create a specification compatibile with 3.0 or higher, i.e. the so called OpenAPI, set this information in the `swaggerDefinition`: ```diff -const swaggerJsdoc = require('swagger-jsdoc'); +import swaggerJsdoc from 'swagger-jsdoc'; const options = { - swaggerDefinition: { + definition: { + openapi: '3.0.0', info: { title: 'Hello World', @@ -20,7 +20,7 @@ const options = { apis: ['./src/routes*.js'], }; -const swaggerSpecification = swaggerJsdoc(options); +const openapiSpecification = swaggerJsdoc(options); ``` ## Annotating source code diff --git a/docs/README.md b/docs/README.md index ab163b33..2792d526 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,6 @@ Quick-start: - [First steps](./FIRST-STEPS.md) -- [CLI](./CLI.md) - [Examples](../examples) Before you submit an issue: diff --git a/package.json b/package.json index 3d058eea..0cfbf369 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "swagger-jsdoc", "description": "Generates swagger doc based on JSDoc", - "version": "7.0.0", + "version": "7.0.0-rc.1", "engines": { "node": ">=12.0.0" },