diff --git a/.eslintignore b/.eslintignore index 21e1ae991..22b0d1dad 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ **/__fixtures__/** /test-harness/**/*.yaml /packages/*/dist +/packages/*/CHANGELOG.md diff --git a/.eslintrc b/.eslintrc index d0c1760d6..95efccda1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -84,10 +84,12 @@ { "files": [ - "src/**/__tests__/**/*.jest.test.{ts,js}" + "src/**/__tests__/**/*.jest.test.{ts,js}", + "__mocks__/**/*.{ts,js}" ], "env": { - "jest": true + "jest": true, + "node": true } }, diff --git a/README.md b/README.md index 231aa2f94..6077c0589 100644 --- a/README.md +++ b/README.md @@ -34,20 +34,28 @@ Find more [installation methods](https://meta.stoplight.io/docs/spectral/docs/ge **Create a Ruleset** -Spectral, being a generic YAML/JSON linter, needs a ruleset in order to be able to lint files. -You can learn more about the rulesets [here](./docs/getting-started/3-rulesets.md). +Spectral, being a generic YAML/JSON linter, needs a ruleset to lint files. There are two ways to get a ruleset: -If you intend to lint an OpenAPI or AsyncAPI document, we have a few predefined rulesets you can extend to get Spectral up and running. -To reference them, you can run the following command: +1. Run this command to get our predefined rulesets based on OpenAPI or AsyncAPI: ```bash printf '{\n "extends": ["spectral:oas", "spectral:asyncapi"]\n}\n' > .spectral.json ``` +2. Create your [own ruleset](./docs/getting-started/3-rulesets.md). + **Lint** +Use this command to lint with the predefined ruleset or a ruleset stored in the same directory as your API document: + +```bash +spectral lint myapifile.yaml +``` + +Use this command to lint with a custom ruleset or one that is located in a different directory than your API document: + ```bash -spectral lint petstore.yaml +spectral lint myapifile.yaml --ruleset myruleset.json ``` ## 📖 Documentation diff --git a/__karma__/perf_hooks.js b/__karma__/perf_hooks.js new file mode 100644 index 000000000..e69de29bb diff --git a/__karma__/process.js b/__karma__/process.js deleted file mode 100644 index f237ddf58..000000000 --- a/__karma__/process.js +++ /dev/null @@ -1 +0,0 @@ -export default undefined; diff --git a/__mocks__/nanoid/non-secure.ts b/__mocks__/nanoid/non-secure.ts deleted file mode 100644 index 3c280b83b..000000000 --- a/__mocks__/nanoid/non-secure.ts +++ /dev/null @@ -1,7 +0,0 @@ -let seed = 0; - -beforeEach(() => { - seed = 0; -}); - -module.exports = jest.fn(() => `random-id-${seed++}`); diff --git a/__mocks__/process.js b/__mocks__/process.js new file mode 100644 index 000000000..85548b6bc --- /dev/null +++ b/__mocks__/process.js @@ -0,0 +1,15 @@ +module.exports.exit = jest.fn(); +module.exports.cwd = jest.fn(); +module.exports.on = jest.fn(); + +module.exports.stdin = { + fd: 0, + isTTY: true, +}; +module.exports.stdout = { + write: jest.fn(), +}; + +module.exports.stderr = { + write: jest.fn(), +}; diff --git a/docs/getting-started/3-rulesets.md b/docs/getting-started/3-rulesets.md index b4bf91d1a..8361550ed 100644 --- a/docs/getting-started/3-rulesets.md +++ b/docs/getting-started/3-rulesets.md @@ -66,7 +66,7 @@ Extends can reference any [distributed ruleset](../guides/7-sharing-rulesets.md) extends: - ./config/spectral.json - https://example.org/api/style.yaml - - some-npm-module + - some-npm-module # note that this would be treated as any other npm package, therefore it has to be placed under node_modules and have a valid package.json. ``` The `extends` keyword can be combined with extra rules in order to extend and override rulesets. Learn more about that in [custom rulesets](../guides/4-custom-rulesets.md). diff --git a/docs/guides/2-cli.md b/docs/guides/2-cli.md index ad5f39398..154acbf8c 100644 --- a/docs/guides/2-cli.md +++ b/docs/guides/2-cli.md @@ -1,11 +1,17 @@ # Spectral CLI -[Once installed](../getting-started/2-installation.md), Spectral can be run via the command-line: +[Once Spectral is installed](../getting-started/2-installation.md) and [you have a ruleset](../../README.md#installation-and-usage), run Spectral via the command-line: ```bash spectral lint petstore.yaml ``` +Use this command to lint with a custom ruleset or one that is located in a different directory than your API document: + +```bash +spectral lint petstore.yaml --ruleset myruleset.json +``` + You can lint multiple files at the same time by passing on multiple arguments: ```bash diff --git a/docs/guides/3-javascript.md b/docs/guides/3-javascript.md index d60ac9d13..d96ed03f6 100644 --- a/docs/guides/3-javascript.md +++ b/docs/guides/3-javascript.md @@ -53,22 +53,62 @@ Find out how to add formats, rules and functions below. ## Loading Rulesets -Spectral comes with some rulesets that are very specific to OpenAPI v2/v3, and they can be loaded using `Spectral.loadRuleset()`. +Spectral comes with some rulesets that are very specific to OpenAPI v2/v3, and they can be set using `Spectral.setRuleset()`. ```js const { Spectral } = require("@stoplight/spectral-core"); -const ruleset = require("./my-ruleset"); // if you use a YAML/JSON ruleset, make sure to use @stoplight/spectral-ruleset-migrator first. - -const myOpenApiDocument = ` -openapi: 3.0.0 -# here goes the rest of document -`; +const ruleset = require("./my-ruleset"); // this works only for JS ruleset, look at the section below to learn how to load a YAML/JSON ruleset const spectral = new Spectral(); spectral.setRuleset(ruleset); -spectral.run(myOpenApiDocument).then(results => { - console.log("here are the results", results); -}); +// lint +``` + +### Loading YAML/JSON rulesets + +#### Node.js + +```js +const path = require("path"); +const fs = require("fs"); + +const { Spectral } = require("@stoplight/spectral-core"); +const { fetch } = require("@stoplight/spectral-runtime"); // can also use isomorphic-fetch, etc.. If you ruleset does not reference any external assets, you can provide some stub instead. +const { bundleAndLoadRuleset } = require("@stoplight/spectral-ruleset-bundler/with-loader"); +// const { commonjs } = require("@stoplight/spectral-ruleset-bundler/plugins/commonjs"); needed if you want to use CommonJS + +const rulesetFilepath = path.join(__dirname, ".spectral.yaml"); + +const spectral = new Spectral(); +s.setRuleset(await bundleAndLoadRuleset(rulesetFilepath, { fs, fetch })); +// or, if you use module.exports (CommonJS) s.setRuleset(await bundleAndLoadRuleset(rulesetFilepath, { fs, fetch }), [commonjs()]); +``` + +#### Browser + +```js +const { Spectral } = require("@stoplight/spectral-core"); +const { bundleAndLoadRuleset } = require("@stoplight/spectral-ruleset-bundler/with-loader"); +// const { commonjs } = require("@stoplight/spectral-ruleset-bundler/plugins/commonjs"); needed if you want to use CommonJS + +const myRuleset = `extends: spectral:oas +rules: {}`; + +const fs = { + promises: { + async readFile(filepath) { + if (filepath === "/.spectral.yaml") { + return myRuleset; + } + + throw new Error(`Could not read ${filepath}`); + }, + }, +}; + +const spectral = new Spectral(); +s.setRuleset(await bundleAndLoadRuleset("/.spectral.yaml", { fs, fetch })); +// or, if you use module.exports (CommonJS) s.setRuleset(await bundleAndLoadRuleset(rulesetFilepath, { fs, fetch }), [commonjs()]); ``` ## Advanced diff --git a/docs/guides/4-custom-rulesets.md b/docs/guides/4-custom-rulesets.md index 2e03a2402..527a850a7 100644 --- a/docs/guides/4-custom-rulesets.md +++ b/docs/guides/4-custom-rulesets.md @@ -402,38 +402,53 @@ Rulesets can then reference aliases in the [given](#given) keyword, either in fu Previously Spectral supported exceptions, which were limited in their ability to target particular rules on specific files or parts of files, or changing parts of a rule. Overrides is the much more powerful version of exceptions, with the ability to customize ruleset usage for different files and projects without having to duplicate any rules. -Overrides can be used to: +Overrides can be used to apply rulesets on: -- Override rulesets to apply on particular files/folders `files: ['schemas/**/*.draft7.json']` -- Override rulesets to apply on particular JSONPath's `files: ['**#/components/schemas/Item']` -- Override rulesets to apply on particular formats `formats: [jsonSchemaDraft7]` +- Particular formats `formats: [jsonSchemaDraft7]` +- Particular files/folders `files: ['schemas/**/*.draft7.json']` +- Particular elements of files `files: ['**#/components/schemas/Item']` - Override particular rules **Example** +```yaml +overrides: + formats: + - json-schema-draft7 + files: + - schemas/**/*.draft7.json + rules: + valid-number-validation: + given: + - $..exclusiveMinimum + - $..exclusiveMaximum + then: + function: schema + functionOptions: + type: number +``` + +To apply an override to particular elements of files, combine a glob for a filepath +with a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) after the anchor, i.e.: + ```yaml overrides: - files: - - schemas/**/*.draft7.json - formats: - - json-schema-draft7 + - "legacy/**/*.oas.json#/paths" rules: - valid-number-validation: - given: - - $..exclusiveMinimum - - $..exclusiveMaximum - then: - function: schema - functionOptions: - type: number + some-inherited-rule: "off" ``` -One can also combine a glob for a filepath with a JSONPath after the anchor, i.e.: +JSON Pointers have a different syntax than JSON Paths used in the `given` component of a rule. +In JSON Pointers, path components are prefixed with a "/" and then concatenated to form the pointer. +Since "/" has a special meaning in JSON pointer, it must be encoded as "~1" when it appears in a component, and "~" must be encoded as "~0". + +You can test JSON Pointer expressions in the [JSON Query online evaluator](https://www.jsonquerytool.com/) by choosing "JSONPointer" as the Transform. ```yaml overrides: - files: - - "legacy/**/*.oas.json#/paths" + - "legacy/**/*.oas.json#/paths/~1Pets~1{petId}/get/parameters/0" rules: some-inherited-rule: "off" ``` diff --git a/docs/guides/7-sharing-rulesets.md b/docs/guides/7-sharing-rulesets.md index b7659c63a..f749700ec 100644 --- a/docs/guides/7-sharing-rulesets.md +++ b/docs/guides/7-sharing-rulesets.md @@ -14,7 +14,7 @@ Or mix and match! extends: - ./config/spectral.json - https://example.org/api/style.yaml - - some-npm-module + - some-npm-module # note that this would be treated as any other npm package, therefore it has to live under node_modules, and have a valid package.json. ``` There are various pros and cons to each approach, so see what is right for you. @@ -109,7 +109,7 @@ module.exports = function (targetVal, { min }) { }; ``` -Developers wanting to pull in your ruleset can just reference the module name in `extends`: +Developers wanting to pull in your ruleset can just install the package using yarn or npm and reference the module name in `extends`: ```yaml extends: @@ -126,7 +126,7 @@ Pegging a ruleset on given version is possible through a `package.json`: } ``` -Or through the use of CDNs for NPM repository, such as [unpkg.com](https://unpkg.com/): +If you Spectral in a browser or don't want to install the package, you can also reference that package through the use of CDNs for NPM repository, such as [unpkg.com](https://unpkg.com/): ```yaml extends: diff --git a/karma.conf.ts b/karma.conf.ts index 45917c494..bc18c36ec 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -17,7 +17,7 @@ module.exports = (config: Config): void => { files: ['./__karma__/jest.ts', 'packages/*/src/**/*.ts'], // list of files / patterns to exclude - exclude: ['packages/cli/**', '**/*.jest.test.ts'], + exclude: ['packages/cli/**', 'packages/ruleset-bundler/src/plugins/commonjs.ts', '**/*.jest.test.ts'], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor @@ -40,7 +40,8 @@ module.exports = (config: Config): void => { 'nimma/legacy': require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'), 'node-fetch': require.resolve('./__karma__/fetch'), fs: require.resolve('./__karma__/fs'), - process: require.resolve('./__karma__/process'), + process: require.resolve('./__mocks__/process'), + perf_hooks: require.resolve('./__karma__/perf_hooks'), fsevents: require.resolve('./__karma__/fsevents'), }, }, diff --git a/package.json b/package.json index c555c05f4..4f92b8ada 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "lint": "yarn lint.prettier && yarn lint.eslint", "lint.fix": "yarn lint.prettier --write && yarn lint.eslint --fix", "lint.eslint": "eslint --cache --cache-location .cache/.eslintcache --ext=.js,.mjs,.ts packages test-harness", - "lint.prettier": "prettier --ignore-path .eslintignore --ignore-unknown --check packages/core/src/ruleset/meta/*.json packages/rulesets/src/{asyncapi,oas}/schemas/*.json packages/*/CHANGELOG.md docs/**/*.md README.md", + "lint.prettier": "prettier --ignore-path .eslintignore --ignore-unknown --check packages/core/src/ruleset/meta/*.json packages/rulesets/src/{asyncapi,oas}/schemas/*.json docs/**/*.md README.md", "pretest": "yarn workspace @stoplight/spectral-ruleset-migrator pretest", "test": "yarn pretest && yarn test.karma && yarn test.jest", "test.harness": "jest -c ./test-harness/jest.config.js", @@ -38,7 +38,7 @@ "test.karma": "karma start", "prepare": "husky install", "prerelease": "patch-package", - "release": "yarn prerelease && HUSKY=0 yarn workspaces foreach run release" + "release": "yarn prerelease && yarn workspaces foreach run release" }, "workspaces": { "packages": [ @@ -100,7 +100,7 @@ "node-powershell": "^4.0.0", "patch-package": "^6.4.7", "prettier": "^2.4.1", - "semantic-release": "^19.0.2", + "semantic-release": "^18.0.1", "semantic-release-monorepo": "^7.0.5", "ts-jest": "^27.0.7", "ts-node": "^10.4.0", @@ -110,9 +110,6 @@ "*.{ts,js}": [ "eslint --fix --cache --cache-location .cache/.eslintcache" ], - "packages/*/CHANGELOG.md": [ - "prettier --write" - ], "docs/**/*.md": [ "prettier --ignore-path .eslintignore --write" ], diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 82be2093f..2f06311b1 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-cli-v6.3.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-cli-v6.2.1...@stoplight/spectral-cli-v6.3.0) (2022-03-03) + + +### Features + +* **cli:** improve error logging ([#2071](https://github.com/stoplightio/spectral/issues/2071)) ([b194368](https://github.com/stoplightio/spectral/commit/b194368164d92dce31b7ceba84ccc94fbe51f979)) + # [@stoplight/spectral-cli-v6.2.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-cli-v6.2.0...@stoplight/spectral-cli-v6.2.1) (2022-02-08) ### Bug Fixes diff --git a/packages/cli/package.json b/packages/cli/package.json index 5ea1046ed..c3b86ebb8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-cli", - "version": "6.2.1", + "version": "6.3.0", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", @@ -51,15 +51,18 @@ "lodash": "~4.17.21", "pony-cause": "^1.0.0", "proxy-agent": "5.0.0", + "stacktracey": "^2.1.7", "strip-ansi": "6.0", "text-table": "0.2", "tslib": "^2.3.0", "yargs": "17.3.1" }, "devDependencies": { + "@types/es-aggregate-error": "^1.0.2", "@types/xml2js": "^0.4.9", "@types/yargs": "^17.0.8", "copyfiles": "^2.4.1", + "es-aggregate-error": "^1.0.7", "jest-when": "^3.4.2", "nock": "^13.1.3", "node-html-parser": "^4.1.5", @@ -73,7 +76,9 @@ ], "assets": [ "./dist/**/*.json", - "./dist/**/*.html" + "./dist/**/*.html", + "../*/dist/**/*.js.map", + "../*/src/**/*.ts" ] } } diff --git a/packages/cli/src/commands/__tests__/lint.test.ts b/packages/cli/src/commands/__tests__/lint.test.ts index 40f1aac6d..eb98e7050 100644 --- a/packages/cli/src/commands/__tests__/lint.test.ts +++ b/packages/cli/src/commands/__tests__/lint.test.ts @@ -1,12 +1,16 @@ import * as yargs from 'yargs'; -import { noop } from 'lodash'; import { DiagnosticSeverity } from '@stoplight/types'; import { IRuleResult } from '@stoplight/spectral-core'; +import * as process from 'process'; +import { ErrorWithCause } from 'pony-cause'; +import AggregateError from 'es-aggregate-error'; import { lint } from '../../services/linter'; import { formatOutput, writeOutput } from '../../services/output'; import lintCommand from '../lint'; +import chalk from 'chalk'; +jest.mock('process'); jest.mock('../../services/output'); jest.mock('../../services/linter'); @@ -24,9 +28,6 @@ function run(command: string) { } describe('lint', () => { - let errorSpy: jest.SpyInstance; - const { isTTY } = process.stdin; - const results: IRuleResult[] = [ { code: 'parser', @@ -47,29 +48,26 @@ describe('lint', () => { ]; beforeEach(() => { - (lint as jest.Mock).mockClear(); (lint as jest.Mock).mockResolvedValueOnce(results); - - (formatOutput as jest.Mock).mockClear(); (formatOutput as jest.Mock).mockReturnValueOnce(''); - - (writeOutput as jest.Mock).mockClear(); (writeOutput as jest.Mock).mockResolvedValueOnce(undefined); - - errorSpy = jest.spyOn(console, 'error').mockImplementation(noop); }); afterEach(() => { - errorSpy.mockRestore(); - process.stdin.isTTY = isTTY; + process.stdin.isTTY = true; + jest.clearAllMocks(); + jest.resetAllMocks(); }); it('shows help when no document and no STDIN are present', () => { - process.stdin.isTTY = true; return expect(run('lint')).rejects.toContain('documents Location of JSON/YAML documents'); }); describe('when STDIN is present', () => { + beforeEach(() => { + process.stdin.isTTY = false; + }); + it('does not show help when documents are missing', async () => { const output = await run('lint'); expect(output).not.toContain('documents Location of JSON/YAML documents'); @@ -150,35 +148,29 @@ describe('lint', () => { it.each(['json', 'stylish'])('calls formatOutput with %s format', async format => { await run(`lint -f ${format} ./__fixtures__/empty-oas2-document.json`); - await new Promise(resolve => void process.nextTick(resolve)); expect(formatOutput).toBeCalledWith(results, format, { failSeverity: DiagnosticSeverity.Error }); }); it('writes formatted output to a file', async () => { await run(`lint -o foo.json ./__fixtures__/empty-oas2-document.json`); - await new Promise(resolve => void process.nextTick(resolve)); expect(writeOutput).toBeCalledWith('', 'foo.json'); }); it('writes formatted output to multiple files when using format and output flags', async () => { - (formatOutput as jest.Mock).mockClear(); (formatOutput as jest.Mock).mockReturnValue(''); await run( `lint --format html --format json --output.json foo.json --output.html foo.html ./__fixtures__/empty-oas2-document.json`, ); - await new Promise(resolve => void process.nextTick(resolve)); expect(writeOutput).toBeCalledTimes(2); expect(writeOutput).nthCalledWith(1, '', 'foo.html'); expect(writeOutput).nthCalledWith(2, '', 'foo.json'); }); it('writes formatted output to multiple files and stdout when using format and output flags', async () => { - (formatOutput as jest.Mock).mockClear(); (formatOutput as jest.Mock).mockReturnValue(''); await run(`lint --format html --format json --output.json foo.json ./__fixtures__/empty-oas2-document.json`); - await new Promise(resolve => void process.nextTick(resolve)); expect(writeOutput).toBeCalledTimes(2); expect(writeOutput).nthCalledWith(1, '', ''); expect(writeOutput).nthCalledWith(2, '', 'foo.json'); @@ -216,8 +208,51 @@ describe('lint', () => { const error = new Error('Failure'); (lint as jest.Mock).mockReset(); (lint as jest.Mock).mockRejectedValueOnce(error); - await run(`lint -o foo.json ./__fixtures__/empty-oas2-document.json`); - await new Promise(resolve => void process.nextTick(resolve)); - expect(errorSpy).toBeCalledWith('Failure'); + await run(`lint ./__fixtures__/empty-oas2-document.json`); + expect(process.stderr.write).nthCalledWith(1, chalk.red('Error running Spectral!\n')); + expect(process.stderr.write).nthCalledWith(2, chalk.red('Use --verbose flag to print the error stack.\n')); + expect(process.stderr.write).nthCalledWith(3, `Error #1: ${chalk.red('Failure')}\n`); + }); + + it('prints each error separately', async () => { + (lint as jest.Mock).mockReset(); + (lint as jest.Mock).mockRejectedValueOnce( + new AggregateError([ + new Error('some unhandled exception'), + new TypeError('another one'), + new ErrorWithCause('some error with cause', { cause: 'original exception' }), + ]), + ); + await run(`lint ./__fixtures__/empty-oas2-document.json`); + expect(process.stderr.write).nthCalledWith(3, `Error #1: ${chalk.red('some unhandled exception')}\n`); + expect(process.stderr.write).nthCalledWith(4, `Error #2: ${chalk.red('another one')}\n`); + expect(process.stderr.write).nthCalledWith(5, `Error #3: ${chalk.red('original exception')}\n`); + }); + + it('given verbose flag, prints each error together with their stacks', async () => { + (lint as jest.Mock).mockReset(); + (lint as jest.Mock).mockRejectedValueOnce( + new AggregateError([ + new Error('some unhandled exception'), + new TypeError('another one'), + new ErrorWithCause('some error with cause', { cause: 'original exception' }), + ]), + ); + + await run(`lint --verbose ./__fixtures__/empty-oas2-document.json`); + + expect(process.stderr.write).nthCalledWith(2, `Error #1: ${chalk.red('some unhandled exception')}\n`); + expect(process.stderr.write).nthCalledWith( + 3, + expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:236`), + ); + + expect(process.stderr.write).nthCalledWith(4, `Error #2: ${chalk.red('another one')}\n`); + expect(process.stderr.write).nthCalledWith( + 5, + expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:237`), + ); + + expect(process.stderr.write).nthCalledWith(6, `Error #3: ${chalk.red('original exception')}\n`); }); }); diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 33f2cb532..094991c17 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -1,14 +1,20 @@ import { Dictionary } from '@stoplight/types'; import { isPlainObject } from '@stoplight/json'; -import { difference, noop, pick } from 'lodash'; -import { ReadStream } from 'tty'; -import type { CommandModule } from 'yargs'; import { getDiagnosticSeverity, IRuleResult } from '@stoplight/spectral-core'; +import { isError, difference, pick } from 'lodash'; +import type { ReadStream } from 'tty'; +import type { CommandModule } from 'yargs'; +import * as process from 'process'; +import chalk from 'chalk'; +import type { ErrorWithCause } from 'pony-cause'; +import StackTracey from 'stacktracey'; import { lint } from '../services/linter'; import { formatOutput, writeOutput } from '../services/output'; import { FailSeverity, ILintConfig, OutputFormat } from '../services/config'; +import { CLIError } from '../errors'; + const formatOptions = Object.values(OutputFormat); const lintCommand: CommandModule = { @@ -55,7 +61,7 @@ const lintCommand: CommandModule = { }) .check((argv: Dictionary) => { if (!Array.isArray(argv.documents) || argv.documents.length === 0) { - throw new TypeError('No documents provided.'); + throw new CLIError('No documents provided.'); } const format = argv.format as string[] & { 0: string }; @@ -66,21 +72,21 @@ const lintCommand: CommandModule = { return true; } - throw new TypeError('Output must be either string or unspecified when a single format is specified'); + throw new CLIError('Output must be either string or unspecified when a single format is specified'); } if (!isPlainObject(output)) { - throw new TypeError('Multiple outputs have to be provided when more than a single format is specified'); + throw new CLIError('Multiple outputs have to be provided when more than a single format is specified'); } const keys = Object.keys(output); if (format.length !== keys.length) { - throw new TypeError('The number of outputs must match the number of formats'); + throw new CLIError('The number of outputs must match the number of formats'); } const diff = difference(format, keys); if (diff.length !== 0) { - throw new TypeError(`Missing outputs for the following formats: ${diff.join(', ')}`); + throw new CLIError(`Missing outputs for the following formats: ${diff.join(', ')}`); } return true; @@ -156,7 +162,7 @@ const lintCommand: CommandModule = { }, }), - handler: args => { + async handler(args) { const { documents, failSeverity, @@ -175,45 +181,90 @@ const lintCommand: CommandModule = { displayOnlyFailures: boolean; }; - return lint(documents, { - format, - output, - encoding, - ignoreUnknownFormat, - failOnUnmatchedGlobs, - ruleset, - stdinFilepath, - ...pick, keyof ILintConfig>(config, ['verbose', 'quiet', 'resolver']), - }) - .then(results => { - if (displayOnlyFailures) { - return filterResultsBySeverity(results, failSeverity); - } - return results; - }) - .then(results => { - if (results.length > 0) { - process.exitCode = severeEnoughToFail(results, failSeverity) ? 1 : 0; - } else if (config.quiet !== true) { - console.log(`No results with a severity of '${failSeverity}' or higher found!`); - } + try { + let results = await lint(documents, { + format, + output, + encoding, + ignoreUnknownFormat, + failOnUnmatchedGlobs, + ruleset, + stdinFilepath, + ...pick, keyof ILintConfig>(config, ['verbose', 'quiet', 'resolver']), + }); - return Promise.all( - format.map(f => { - const formattedOutput = formatOutput(results, f, { failSeverity: getDiagnosticSeverity(failSeverity) }); - return writeOutput(formattedOutput, output?.[f] ?? ''); - }), - ).then(noop); - }) - .catch(fail); + if (displayOnlyFailures) { + results = filterResultsBySeverity(results, failSeverity); + } + + await Promise.all( + format.map(f => { + const formattedOutput = formatOutput(results, f, { failSeverity: getDiagnosticSeverity(failSeverity) }); + return writeOutput(formattedOutput, output?.[f] ?? ''); + }), + ); + + if (results.length > 0) { + process.exit(severeEnoughToFail(results, failSeverity) ? 1 : 0); + } else if (config.quiet !== true) { + process.stdout.write(`No results with a severity of '${failSeverity}' or higher found!`); + } + } catch (ex) { + fail(isError(ex) ? ex : new Error(String(ex)), config.verbose === true); + } }, }; -const fail = ({ message }: Error): void => { - console.error(message); - process.exitCode = 2; +const fail = (error: Error | ErrorWithCause | AggregateError, verbose: boolean): void => { + if (error instanceof CLIError) { + process.stderr.write(chalk.red(error.message)); + process.exit(2); + } + + const errors: unknown[] = 'errors' in error ? error.errors : [error]; + + process.stderr.write(chalk.red('Error running Spectral!\n')); + + if (!verbose) { + process.stderr.write(chalk.red('Use --verbose flag to print the error stack.\n')); + } + + for (const [i, error] of errors.entries()) { + const actualError: unknown = isError(error) && 'cause' in error ? (error as ErrorWithCause).cause : error; + const message = isError(actualError) ? actualError.message : String(actualError); + + const info = `Error #${i + 1}: `; + + process.stderr.write(`${info}${chalk.red(message)}\n`); + + if (verbose && isError(actualError)) { + process.stderr.write(`${chalk.red(printErrorStacks(actualError, info.length))}\n`); + } + } + + process.exit(2); }; +function getWidth(ratio: number): number { + return Math.min(20, Math.floor(ratio * process.stderr.columns)); +} + +function printErrorStacks(error: Error, padding: number): string { + return new StackTracey(error) + .slice(0, 5) + .withSources() + .asTable({ + maxColumnWidths: { + callee: getWidth(0.2), + file: getWidth(0.4), + sourceLine: getWidth(0.4), + }, + }) + .split('\n') + .map(error => `${' '.repeat(padding)}${error}`) + .join('\n'); +} + const filterResultsBySeverity = (results: IRuleResult[], failSeverity: FailSeverity): IRuleResult[] => { const diagnosticSeverity = getDiagnosticSeverity(failSeverity); return results.filter(r => r.severity <= diagnosticSeverity); diff --git a/packages/cli/src/errors/index.ts b/packages/cli/src/errors/index.ts new file mode 100644 index 000000000..a993194ef --- /dev/null +++ b/packages/cli/src/errors/index.ts @@ -0,0 +1 @@ +export class CLIError extends Error {} diff --git a/packages/cli/src/services/__tests__/linter.test.ts b/packages/cli/src/services/__tests__/linter.test.ts index 4ab846dea..84ed44522 100644 --- a/packages/cli/src/services/__tests__/linter.test.ts +++ b/packages/cli/src/services/__tests__/linter.test.ts @@ -1,11 +1,14 @@ import { join, resolve } from '@stoplight/path'; import nock from 'nock'; import * as yargs from 'yargs'; -import lintCommand from '../../commands/lint'; -import { lint } from '../linter'; import { DiagnosticSeverity } from '@stoplight/types'; import { RulesetValidationError } from '@stoplight/spectral-core'; +import * as process from 'process'; +import lintCommand from '../../commands/lint'; +import { lint } from '../linter'; + +jest.mock('process'); jest.mock('../output'); const validCustomOas3SpecPath = resolve(__dirname, '__fixtures__/openapi-3.0-valid-custom.yaml'); @@ -29,25 +32,16 @@ async function run(command: string) { } describe('Linter service', () => { - let consoleLogSpy: jest.SpyInstance; - let consoleErrorSpy: jest.SpyInstance; let processCwdSpy: jest.SpyInstance; beforeEach(() => { - const noop = () => { - /* no-op */ - }; - consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(noop); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(noop); - - processCwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(join(__dirname, '__fixtures__')); + (process.cwd as jest.Mock).mockReturnValue(join(__dirname, '__fixtures__')); + processCwdSpy = jest.spyOn(globalThis.process, 'cwd').mockImplementation(process.cwd); }); afterEach(() => { - consoleLogSpy.mockRestore(); - consoleErrorSpy.mockRestore(); processCwdSpy.mockRestore(); - + jest.clearAllMocks(); nock.cleanAll(); }); @@ -93,7 +87,7 @@ describe('Linter service', () => { }); it('demands some ruleset to be present', () => { - processCwdSpy.mockReturnValue(join(__dirname, '__fixtures__/resolver')); + (process.cwd as jest.Mock).mockReturnValue(join(__dirname, '__fixtures__/resolver')); return expect(run(`lint stoplight-info-document.json`)).rejects.toThrow( 'No ruleset has been found. Please provide a ruleset using the --ruleset CLI argument, or make sure your ruleset file matches .?spectral.(js|ya?ml|json)', ); diff --git a/packages/cli/src/services/__tests__/output.test.ts b/packages/cli/src/services/__tests__/output.test.ts index d04e6a5cb..c9f08e305 100644 --- a/packages/cli/src/services/__tests__/output.test.ts +++ b/packages/cli/src/services/__tests__/output.test.ts @@ -12,11 +12,7 @@ jest.mock('fs', () => ({ writeFile: jest.fn().mockResolvedValue(void 0), }, })); -jest.mock('process', () => ({ - stdout: { - write: jest.fn(), - }, -})); +jest.mock('process'); describe('Output service', () => { describe('formatOutput', () => { diff --git a/packages/cli/src/services/linter/linter.ts b/packages/cli/src/services/linter/linter.ts index c02e4ce46..7796b81b9 100644 --- a/packages/cli/src/services/linter/linter.ts +++ b/packages/cli/src/services/linter/linter.ts @@ -5,6 +5,7 @@ import * as Parsers from '@stoplight/spectral-parsers'; import { getRuleset, listFiles, segregateEntriesPerKind, readFileDescriptor } from './utils'; import { getResolver } from './utils/getResolver'; import { ILintConfig } from '../config'; +import { CLIError } from '../../errors'; export async function lint(documents: Array, flags: ILintConfig): Promise { const spectral = new Spectral({ @@ -25,7 +26,7 @@ export async function lint(documents: Array, flags: ILintConfig if (unmatchedPatterns.length > 0) { if (flags.failOnUnmatchedGlobs) { - throw new Error(`Unmatched glob patterns: \`${unmatchedPatterns.join(',')}\``); + throw new CLIError(`Unmatched glob patterns: \`${unmatchedPatterns.join(',')}\``); } for (const unmatchedPattern of unmatchedPatterns) { diff --git a/packages/cli/src/services/linter/utils/getResolver.ts b/packages/cli/src/services/linter/utils/getResolver.ts index dee98fd39..db63d5a9e 100644 --- a/packages/cli/src/services/linter/utils/getResolver.ts +++ b/packages/cli/src/services/linter/utils/getResolver.ts @@ -2,6 +2,7 @@ import { isAbsolute, join } from '@stoplight/path'; import { Optional } from '@stoplight/types'; import { createHttpAndFileResolver, Resolver } from '@stoplight/spectral-ref-resolver'; import { isError } from 'lodash'; +import { CLIError } from '../../../errors'; export const getResolver = (resolver: Optional, proxy: Optional): Resolver => { if (resolver !== void 0) { @@ -9,7 +10,7 @@ export const getResolver = (resolver: Optional, proxy: Optional) // eslint-disable-next-line @typescript-eslint/no-unsafe-return return require(isAbsolute(resolver) ? resolver : join(process.cwd(), resolver)); } catch (ex) { - throw new Error(isError(ex) ? formatMessage(ex.message) : String(ex)); + throw new CLIError(isError(ex) ? formatMessage(ex.message) : String(ex)); } } diff --git a/packages/cli/src/services/linter/utils/getRuleset.ts b/packages/cli/src/services/linter/utils/getRuleset.ts index c16ea733e..f2af0b7c9 100644 --- a/packages/cli/src/services/linter/utils/getRuleset.ts +++ b/packages/cli/src/services/linter/utils/getRuleset.ts @@ -12,7 +12,7 @@ import { stdin } from '@stoplight/spectral-ruleset-bundler/plugins/stdin'; import { builtins } from '@stoplight/spectral-ruleset-bundler/plugins/builtins'; import { isError, isObject } from 'lodash'; import commonjs from '@rollup/plugin-commonjs'; -import { ErrorWithCause } from 'pony-cause'; +import { CLIError } from '../../../errors'; async function getDefaultRulesetFile(): Promise> { const cwd = process.cwd(); @@ -41,7 +41,7 @@ export async function getRuleset(rulesetFile: Optional): Promise): Promise 0 ? [...givenPath, ...target.path] : givenPath; - const targetResults = then.function(target.value, then.functionOptions ?? null, { - ...fnContext, - path, - }); + let targetResults; + try { + targetResults = then.function(target.value, then.functionOptions ?? null, { + ...fnContext, + path, + }); + } catch (e) { + throw new ErrorWithCause( + `Function "${then.function.name}" threw an exception${isError(e) ? `: ${e.message}` : ''}`, + { + cause: e, + }, + ); + } if (targetResults === void 0) continue; diff --git a/packages/formats/CHANGELOG.md b/packages/formats/CHANGELOG.md index d546c4a8e..f7feee55b 100644 --- a/packages/formats/CHANGELOG.md +++ b/packages/formats/CHANGELOG.md @@ -1,3 +1,9 @@ +# [@stoplight/spectral-formats-v1.1.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-formats-v1.0.2...@stoplight/spectral-formats-v1.1.0) (2022-02-24) + +### Features + +- support 2.1.0, 2.2.0, 2.3.0 AsyncAPI versions ([#2067](https://github.com/stoplightio/spectral/issues/2067)) ([b0b008d](https://github.com/stoplightio/spectral/commit/b0b008d65794df177dbfe7d9589c90d541c2794d)) + # [@stoplight/spectral-formats-v1.0.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-formats-v1.0.1...@stoplight/spectral-formats-v1.0.2) (2021-12-30) ### Bug Fixes diff --git a/packages/formats/package.json b/packages/formats/package.json index 01035af03..d5e42d7e8 100644 --- a/packages/formats/package.json +++ b/packages/formats/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-formats", - "version": "1.0.2", + "version": "1.1.0", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", diff --git a/packages/formats/src/__tests__/asyncapi.test.ts b/packages/formats/src/__tests__/asyncapi.test.ts index 9bd0de0cb..01e22c240 100644 --- a/packages/formats/src/__tests__/asyncapi.test.ts +++ b/packages/formats/src/__tests__/asyncapi.test.ts @@ -1,10 +1,13 @@ -import { asyncApi2 } from '../asyncapi'; +import { aas2, aas2_0, aas2_1, aas2_2, aas2_3 } from '../asyncapi'; -describe('AsyncApi format', () => { - describe('AsyncApi 2.{minor}.{patch}', () => { - it.each([['2.0.17'], ['2.9.0'], ['2.9.3']])('recognizes %s version correctly', (version: string) => { - expect(asyncApi2({ asyncapi: version }, null)).toBe(true); - }); +describe('AsyncAPI format', () => { + describe('AsyncAPI 2.x', () => { + it.each(['2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.0.17', '2.1.37', '2.9.0', '2.9.3'])( + 'recognizes %s version correctly', + version => { + expect(aas2({ asyncapi: version }, null)).toBe(true); + }, + ); const testCases = [ { asyncapi: '3.0' }, @@ -15,6 +18,7 @@ describe('AsyncApi format', () => { { asyncapi: '2.0.01' }, { asyncapi: '1.0' }, { asyncapi: 2 }, + { asyncapi: null }, { openapi: '4.0' }, { openapi: '2.0' }, { openapi: null }, @@ -25,7 +29,50 @@ describe('AsyncApi format', () => { ]; it.each(testCases)('does not recognize invalid document %o', document => { - expect(asyncApi2(document, null)).toBe(false); + expect(aas2(document, null)).toBe(false); + }); + }); + + describe('AsyncAPI 2.0', () => { + it.each(['2.0.0', '2.0.3'])('recognizes %s version correctly', version => { + expect(aas2_0({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.0', '2.1.0', '2.1.3'])('does not recognize %s version', version => { + expect(aas2_0({ asyncapi: version }, null)).toBe(false); + }); + }); + + describe('AsyncAPI 2.1', () => { + it.each(['2.1.0', '2.1.37'])('recognizes %s version correctly', version => { + expect(aas2_1({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.1', '2.0.0', '2.2.0', '2.2.3'])('does not recognize %s version', version => { + expect(aas2_1({ asyncapi: version }, null)).toBe(false); + }); + }); + + describe('AsyncAPI 2.2', () => { + it.each(['2.2.0', '2.2.3'])('recognizes %s version correctly', version => { + expect(aas2_2({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.2', '2.0.0', '2.1.0', '2.1.37', '2.3.0', '2.3.3'])('does not recognize %s version', version => { + expect(aas2_2({ asyncapi: version }, null)).toBe(false); }); }); + + describe('AsyncAPI 2.3', () => { + it.each(['2.3.0', '2.3.3'])('recognizes %s version correctly', version => { + expect(aas2_3({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.3', '2.0.0', '2.1.0', '2.1.37', '2.2.0', '2.4.0', '2.4.3'])( + 'does not recognize %s version', + version => { + expect(aas2_3({ asyncapi: version }, null)).toBe(false); + }, + ); + }); }); diff --git a/packages/formats/src/asyncapi.ts b/packages/formats/src/asyncapi.ts index 8a1a6dde3..87312ac19 100644 --- a/packages/formats/src/asyncapi.ts +++ b/packages/formats/src/asyncapi.ts @@ -1,24 +1,36 @@ import type { Format } from '@stoplight/spectral-core'; import { isPlainObject } from '@stoplight/json'; -type MaybeAsyncApi2 = Partial<{ asyncapi: unknown }>; +type MaybeAAS2 = { asyncapi: unknown } & Record; -const bearsAStringPropertyNamed = (document: unknown, propertyName: string): boolean => { - return isPlainObject(document) && propertyName in document && typeof document[propertyName] === 'string'; -}; +const aas2Regex = /^2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/; +const aas2_0Regex = /^2\.0(?:\.[0-9]*)?$/; +const aas2_1Regex = /^2\.1(?:\.[0-9]*)?$/; +const aas2_2Regex = /^2\.2(?:\.[0-9]*)?$/; +const aas2_3Regex = /^2\.3(?:\.[0-9]*)?$/; -const version2Regex = /^2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/; +const isAas2 = (document: unknown): document is { asyncapi: string } & Record => + isPlainObject(document) && 'asyncapi' in document && aas2Regex.test(String((document as MaybeAAS2).asyncapi)); -export const asyncApi2: Format = document => { - if (!bearsAStringPropertyNamed(document, 'asyncapi')) { - return false; - } +export const aas2: Format = isAas2; +aas2.displayName = 'AsyncAPI 2.x'; - const version = String((document as MaybeAsyncApi2).asyncapi); +// for backward compatibility +export const asyncApi2 = aas2; +export const asyncapi2 = aas2; - return version2Regex.test(version); -}; +export const aas2_0: Format = (document: unknown): boolean => + isAas2(document) && aas2_0Regex.test(String((document as MaybeAAS2).asyncapi)); +aas2_0.displayName = 'AsyncAPI 2.0.x'; -asyncApi2.displayName = 'AsyncAPI 2.x'; +export const aas2_1: Format = (document: unknown): boolean => + isAas2(document) && aas2_1Regex.test(String((document as MaybeAAS2).asyncapi)); +aas2_1.displayName = 'AsyncAPI 2.1.x'; -export { asyncApi2 as asyncapi2 }; +export const aas2_2: Format = (document: unknown): boolean => + isAas2(document) && aas2_2Regex.test(String((document as MaybeAAS2).asyncapi)); +aas2_2.displayName = 'AsyncAPI 2.2.x'; + +export const aas2_3: Format = (document: unknown): boolean => + isAas2(document) && aas2_3Regex.test(String((document as MaybeAAS2).asyncapi)); +aas2_3.displayName = 'AsyncAPI 2.3.x'; diff --git a/packages/functions/CHANGELOG.md b/packages/functions/CHANGELOG.md index 2c270784b..1f6a72127 100644 --- a/packages/functions/CHANGELOG.md +++ b/packages/functions/CHANGELOG.md @@ -1,3 +1,17 @@ +# [@stoplight/spectral-functions-v1.6.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-functions-v1.5.2...@stoplight/spectral-functions-v1.6.0) (2022-03-03) + + +### Features + +* **cli:** improve error logging ([#2071](https://github.com/stoplightio/spectral/issues/2071)) ([b194368](https://github.com/stoplightio/spectral/commit/b194368164d92dce31b7ceba84ccc94fbe51f979)) + +# [@stoplight/spectral-functions-v1.5.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-functions-v1.5.1...@stoplight/spectral-functions-v1.5.2) (2022-02-28) + + +### Bug Fixes + +* **functions:** __importDefault undefined ([609ecb1](https://github.com/stoplightio/spectral/commit/609ecb1b23f354459f96687f27f911e915cb6ab3)) + # [@stoplight/spectral-functions-v1.5.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-functions-v1.5.0...@stoplight/spectral-functions-v1.5.1) (2021-12-29) ### Bug Fixes diff --git a/packages/functions/package.json b/packages/functions/package.json index 6205c5071..0f3c25a65 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-functions", - "version": "1.5.1", + "version": "1.6.0", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", diff --git a/packages/functions/src/__tests__/__helpers__/tester.ts b/packages/functions/src/__tests__/__helpers__/tester.ts index 0dc0c0a9b..21a1c03ea 100644 --- a/packages/functions/src/__tests__/__helpers__/tester.ts +++ b/packages/functions/src/__tests__/__helpers__/tester.ts @@ -38,18 +38,27 @@ export default async function ( path: error.path, message: error.message, })); - } catch (e: unknown) { - if ( - e instanceof Error && - Array.isArray((e as Error & { errors?: unknown }).errors) && - (e as Error & { errors: unknown[] }).errors.length === 1 - ) { - const actualError = (e as Error & { errors: [unknown] }).errors[0]; - throw actualError instanceof Error && 'cause' in (actualError as Error & { cause?: unknown }) - ? (actualError as Error & { cause: unknown }).cause - : actualError; + } catch (error: unknown) { + if (!(error instanceof Error)) { + throw error; + } + + const errors = Array.isArray((error as Error & { errors?: unknown }).errors) + ? (error as Error & { errors: unknown[] }).errors + : [error]; + + if (errors.length === 1) { + throw getCause(errors[0]); } else { - throw e; + throw error; } } } + +function getCause(error: unknown): unknown { + if (error instanceof Error && 'cause' in (error as Error & { cause?: unknown })) { + return getCause((error as Error & { cause?: unknown }).cause); + } + + return error; +} diff --git a/packages/functions/src/index.ts b/packages/functions/src/index.ts index 04652dd19..8f5e2e1c8 100644 --- a/packages/functions/src/index.ts +++ b/packages/functions/src/index.ts @@ -1,15 +1,41 @@ -export { default as alphabetical, Options as AlphabeticalOptions } from './alphabetical'; -export { default as casing, Options as CasingOptions } from './casing'; -export { default as defined } from './defined'; -export { default as enumeration, Options as EnumerationOptions } from './enumeration'; -export { default as falsy } from './falsy'; -export { default as length, Options as LengthOptions } from './length'; -export { default as pattern, Options as PatternOptions } from './pattern'; -export { default as schema, Options as SchemaOptions } from './schema'; -export { default as truthy } from './truthy'; -export { default as undefined } from './undefined'; -export { +import { default as alphabetical, Options as AlphabeticalOptions } from './alphabetical'; +import { default as casing, Options as CasingOptions } from './casing'; +import { default as defined } from './defined'; +import { default as enumeration, Options as EnumerationOptions } from './enumeration'; +import { default as falsy } from './falsy'; +import { default as length, Options as LengthOptions } from './length'; +import { default as pattern, Options as PatternOptions } from './pattern'; +import { default as schema, Options as SchemaOptions } from './schema'; +import { default as truthy } from './truthy'; +import { default as undefined } from './undefined'; +import { default as unreferencedReusableObject, Options as UnreferencedReusableObjectOptions, } from './unreferencedReusableObject'; -export { default as xor, Options as XorOptions } from './xor'; +import { default as xor, Options as XorOptions } from './xor'; + +export { + alphabetical, + casing, + defined, + enumeration, + falsy, + length, + pattern, + schema, + truthy, + undefined, + unreferencedReusableObject, + xor, +}; + +export type { + AlphabeticalOptions, + CasingOptions, + EnumerationOptions, + LengthOptions, + PatternOptions, + SchemaOptions, + UnreferencedReusableObjectOptions, + XorOptions, +}; diff --git a/packages/ruleset-bundler/CHANGELOG.md b/packages/ruleset-bundler/CHANGELOG.md index 642c9ec93..08edf9858 100644 --- a/packages/ruleset-bundler/CHANGELOG.md +++ b/packages/ruleset-bundler/CHANGELOG.md @@ -1,3 +1,24 @@ +# [@stoplight/spectral-ruleset-bundler-v1.2.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-ruleset-bundler-v1.2.0...@stoplight/spectral-ruleset-bundler-v1.2.1) (2022-02-28) + + +### Bug Fixes + +* **ruleset-bundler:** __importDefault undefined ([874a80e](https://github.com/stoplightio/spectral/commit/874a80e9d8e36d96bfbb467e340aab337227bfa7)) + +# [@stoplight/spectral-ruleset-bundler-v1.2.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-ruleset-bundler-v1.1.1...@stoplight/spectral-ruleset-bundler-v1.2.0) (2022-02-25) + + +### Bug Fixes + +* **ruleset-bundler:** builtins plugin should create a new instance for each module ([b06903c](https://github.com/stoplightio/spectral/commit/b06903ce71f556809b06a21ce3a299625b3760e0)) +* **ruleset-bundler:** virtualFs plugin incompatible with commonjs plugin ([a48381b](https://github.com/stoplightio/spectral/commit/a48381bdf86c7c9015dd67daa8bda767ea727376)) + + +### Features + +* **ruleset-bundler:** expose commonjs plugin ([91a4b80](https://github.com/stoplightio/spectral/commit/91a4b807dc1e9b7ed700b6645eff711cfa1d5bef)) +* **ruleset-bundler:** plugins should be easy to override ([0263bf0](https://github.com/stoplightio/spectral/commit/0263bf0234b11d6bb17b7b7feef6ba5716cc8f01)) + # [@stoplight/spectral-ruleset-bundler-v1.1.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-ruleset-bundler-v1.1.0...@stoplight/spectral-ruleset-bundler-v1.1.1) (2021-12-30) ### Bug Fixes diff --git a/packages/ruleset-bundler/package.json b/packages/ruleset-bundler/package.json index 0761b7dcf..933d44b5a 100644 --- a/packages/ruleset-bundler/package.json +++ b/packages/ruleset-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-ruleset-bundler", - "version": "1.1.1", + "version": "1.2.1", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", @@ -38,6 +38,7 @@ "release": "semantic-release -e semantic-release-monorepo" }, "dependencies": { + "@rollup/plugin-commonjs": "^21.0.1", "@stoplight/path": "1.3.2", "@stoplight/spectral-core": ">=1", "@stoplight/spectral-formats": ">=1", @@ -50,7 +51,7 @@ "@stoplight/types": "^12.3.0", "@types/node": "*", "pony-cause": "1.1.1", - "rollup": "~2.60.2", + "rollup": "~2.67.0", "tslib": "^2.3.1", "validate-npm-package-name": "3.0.0" }, diff --git a/packages/ruleset-bundler/src/__tests__/index.jest.test.ts b/packages/ruleset-bundler/src/__tests__/index.jest.test.ts new file mode 100644 index 000000000..9afc25fe0 --- /dev/null +++ b/packages/ruleset-bundler/src/__tests__/index.jest.test.ts @@ -0,0 +1,100 @@ +import { serveAssets } from '@stoplight/spectral-test-utils'; +import { fetch } from '@stoplight/spectral-runtime'; +import * as fs from 'fs'; +import { bundleRuleset } from '../index'; +import { IO } from '../types'; +import { node } from '../presets/node'; +import { browser } from '../presets/browser'; +import { commonjs } from '../plugins/commonjs'; +import { virtualFs } from '../plugins/virtualFs'; +import { runtime } from '../presets/runtime'; + +jest.mock('fs'); + +describe('Ruleset Bundler', () => { + let io: IO; + + beforeEach(() => { + io = { + fs, + fetch, + }; + + serveAssets({ + '/p/.spectral/my-fn.js': `module.exports = function f() { return [] };`, + + '/p/spectral.js': `import myFn from './.spectral/my-fn.js'; + +export default { + rules: { + rule: { + given: '$', + then: { function: myFn }, + } + }, +};`, + }); + }); + + it('given runtime target, should support commonjs', async () => { + const code = await bundleRuleset('/p/spectral.js', { + target: 'runtime', + plugins: [...runtime(io), commonjs()], + }); + + expect(code).toContain(`\tvar myFn = function f() { return [] }; + +\tvar spectral = { +\t rules: { +\t rule: { +\t given: '$', +\t then: { function: myFn }, +\t } +\t }, +\t}; + +\treturn spectral; + +})();`); + }); + + it('given browser target, should support commonjs', async () => { + const code = await bundleRuleset('/p/spectral.js', { + target: 'browser', + plugins: [...browser(io), commonjs()], + }); + + expect(code).toContain(`var myFn = function f() { return [] }; + +var spectral = { + rules: { + rule: { + given: '$', + then: { function: myFn }, + } + }, +}; + +export { spectral as default };`); + }); + + it('given node target, should support commonjs', async () => { + const code = await bundleRuleset('/p/spectral.js', { + target: 'node', + plugins: [...node(io), virtualFs(io), commonjs()], + }); + + expect(code).toContain(`var myFn = function f() { return [] }; + +var spectral = { + rules: { + rule: { + given: '$', + then: { function: myFn }, + } + }, +}; + +export { spectral as default };`); + }); +}); diff --git a/packages/ruleset-bundler/src/index.ts b/packages/ruleset-bundler/src/index.ts index 2228defd9..38acec50f 100644 --- a/packages/ruleset-bundler/src/index.ts +++ b/packages/ruleset-bundler/src/index.ts @@ -1,6 +1,7 @@ import { rollup, Plugin } from 'rollup'; import { isURL } from '@stoplight/path'; import { isPackageImport } from './utils/isPackageImport'; +import { dedupeRollupPlugins } from './utils/dedupeRollupPlugins'; export type BundleOptions = { plugins: Plugin[]; @@ -17,7 +18,7 @@ export async function bundleRuleset( ): Promise { const bundle = await rollup({ input, - plugins, + plugins: dedupeRollupPlugins(plugins), treeshake, watch: false, perf: false, diff --git a/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts b/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts index 8a122f182..44f3fc9ba 100644 --- a/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts +++ b/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts @@ -12,12 +12,23 @@ import { builtins } from '../builtins'; describe('Builtins Plugin', () => { let io: IO; + let randomSpy: jest.SpyInstance; beforeEach(() => { io = { fs, fetch: runtime.fetch, }; + + randomSpy = jest + .spyOn(Math, 'random') + .mockReturnValueOnce(0.8229275205939697) + .mockReturnValueOnce(0.7505242801973444) + .mockReturnValueOnce(0.5647855410879519); + }); + + afterEach(() => { + randomSpy.mockRestore(); }); describe.each(['browser', 'runtime'])('given %s target', target => { @@ -51,21 +62,21 @@ export default { }); expect(code) - .toEqual(`const alphabetical = globalThis[Symbol.for('@stoplight/spectral-functions')]['alphabetical']; -const casing = globalThis[Symbol.for('@stoplight/spectral-functions')]['casing']; -const defined = globalThis[Symbol.for('@stoplight/spectral-functions')]['defined']; -const enumeration = globalThis[Symbol.for('@stoplight/spectral-functions')]['enumeration']; -const falsy = globalThis[Symbol.for('@stoplight/spectral-functions')]['falsy']; -const length = globalThis[Symbol.for('@stoplight/spectral-functions')]['length']; -const pattern = globalThis[Symbol.for('@stoplight/spectral-functions')]['pattern']; -const schema = globalThis[Symbol.for('@stoplight/spectral-functions')]['schema']; -const truthy = globalThis[Symbol.for('@stoplight/spectral-functions')]['truthy']; -const undefined$1 = globalThis[Symbol.for('@stoplight/spectral-functions')]['undefined']; -const unreferencedReusableObject = globalThis[Symbol.for('@stoplight/spectral-functions')]['unreferencedReusableObject']; -const xor = globalThis[Symbol.for('@stoplight/spectral-functions')]['xor']; - -const oas = globalThis[Symbol.for('@stoplight/spectral-rulesets')]['oas']; -const asyncapi = globalThis[Symbol.for('@stoplight/spectral-rulesets')]['asyncapi']; + .toEqual(`const alphabetical = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['alphabetical']; +const casing = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['casing']; +const defined = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['defined']; +const enumeration = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['enumeration']; +const falsy = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['falsy']; +const length = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['length']; +const pattern = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['pattern']; +const schema = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['schema']; +const truthy = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['truthy']; +const undefined$1 = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['undefined']; +const unreferencedReusableObject = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['unreferencedReusableObject']; +const xor = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['xor']; + +const oas = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-rulesets']['oas']; +const asyncapi = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-rulesets']['asyncapi']; var input = { extends: [oas], @@ -87,7 +98,9 @@ var input = { export { input as default }; `); - expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions); + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions'], + ).toStrictEqual(functions); }); it('should support overrides', async () => { @@ -113,30 +126,78 @@ readFile();`, ], }); - expect(code).toEqual(`const fetch = globalThis[Symbol.for('@stoplight/spectral-runtime')]['fetch']; -const DEFAULT_REQUEST_OPTIONS = globalThis[Symbol.for('@stoplight/spectral-runtime')]['DEFAULT_REQUEST_OPTIONS']; -const decodeSegmentFragment = globalThis[Symbol.for('@stoplight/spectral-runtime')]['decodeSegmentFragment']; -const printError = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printError']; -const PrintStyle = globalThis[Symbol.for('@stoplight/spectral-runtime')]['PrintStyle']; -const printPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printPath']; -const printValue = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printValue']; -const startsWithProtocol = globalThis[Symbol.for('@stoplight/spectral-runtime')]['startsWithProtocol']; -const isAbsoluteRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['isAbsoluteRef']; -const traverseObjUntilRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['traverseObjUntilRef']; -const getEndRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['getEndRef']; -const safePointerToPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['safePointerToPath']; -const getClosestJsonPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['getClosestJsonPath']; -const readFile = globalThis[Symbol.for('@stoplight/spectral-runtime')]['readFile']; -const readParsable = globalThis[Symbol.for('@stoplight/spectral-runtime')]['readParsable']; + expect(code) + .toEqual(`const fetch = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['fetch']; +const DEFAULT_REQUEST_OPTIONS = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['DEFAULT_REQUEST_OPTIONS']; +const decodeSegmentFragment = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['decodeSegmentFragment']; +const printError = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printError']; +const PrintStyle = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['PrintStyle']; +const printPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printPath']; +const printValue = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printValue']; +const startsWithProtocol = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['startsWithProtocol']; +const isAbsoluteRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['isAbsoluteRef']; +const traverseObjUntilRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['traverseObjUntilRef']; +const getEndRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['getEndRef']; +const safePointerToPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['safePointerToPath']; +const getClosestJsonPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['getClosestJsonPath']; +const readFile = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['readFile']; +const readParsable = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['readParsable']; readFile(); `); - expect(globalThis[Symbol.for('@stoplight/spectral-runtime')]).toStrictEqual({ + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime'], + ).toStrictEqual({ ...runtime, readFile, }); }); + + it('should isolate each instance', async () => { + serveAssets({ + '/tmp/input.js': `import { readFile } from '@stoplight/spectral-runtime'; + +readFile();`, + }); + + // eslint-disable-next-line @typescript-eslint/no-empty-function + function readFile(): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function + function readFile2(): void {} + + await bundleRuleset('/tmp/input.js', { + format: 'esm', + target, + plugins: [ + builtins({ + '@stoplight/spectral-runtime': { + readFile, + }, + }), + builtins({ + '@stoplight/spectral-runtime': { + readFile: readFile2, + }, + }), + virtualFs(io), + ], + }); + + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime'], + ).toStrictEqual({ + ...runtime, + readFile, + }); + + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['750524']['@stoplight/spectral-runtime'], + ).toStrictEqual({ + ...runtime, + readFile: readFile2, + }); + }); }); describe('given node target', () => { @@ -191,7 +252,9 @@ var input = { export { input as default }; `); - expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions); + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions'], + ).toStrictEqual(functions); }); }); }); diff --git a/packages/ruleset-bundler/src/plugins/builtins.ts b/packages/ruleset-bundler/src/plugins/builtins.ts index 66708f18c..937a831cc 100644 --- a/packages/ruleset-bundler/src/plugins/builtins.ts +++ b/packages/ruleset-bundler/src/plugins/builtins.ts @@ -11,15 +11,21 @@ type Module = 'core' | 'formats' | 'functions' | 'parsers' | 'ref-resolver' | 'r type GlobalModules = Record<`@stoplight/spectral-${Module}`, string>; type Overrides = Record>; +const NAME = '@stoplight-spectral/builtins'; + function registerModule( + instanceId: number, id: keyof GlobalModules, members: Record, overrides: Partial, ): [string, string] { const actualOverrides = overrides[id]; - globalThis[Symbol.for(id)] = actualOverrides ? { ...members, ...actualOverrides } : members; + const instances = (globalThis[Symbol.for(NAME)] ??= {}) as Record>; + const root = (instances[instanceId] ??= {}); + + root[id] = actualOverrides ? { ...members, ...actualOverrides } : members; - const m = `globalThis[Symbol.for('${id}')]`; + const m = `globalThis[Symbol.for('${NAME}')]['${instanceId}']['${id}']`; let code = ''; for (const member of Object.keys(members)) { code += `export const ${member} = ${m}['${member}'];\n`; @@ -29,26 +35,28 @@ function registerModule( } export const builtins = (overrides: Partial = {}): Plugin => { + const instanceId = Math.round(Math.random() * 1_000_000); + const modules = Object.fromEntries([ - registerModule('@stoplight/spectral-core', core, overrides), - registerModule('@stoplight/spectral-formats', formats, overrides), - registerModule('@stoplight/spectral-functions', functions, overrides), - registerModule('@stoplight/spectral-parsers', parsers, overrides), - registerModule('@stoplight/spectral-ref-resolver', refResolver, overrides), - registerModule('@stoplight/spectral-rulesets', rulesets, overrides), - registerModule('@stoplight/spectral-runtime', runtime, overrides), + registerModule(instanceId, '@stoplight/spectral-core', core, overrides), + registerModule(instanceId, '@stoplight/spectral-formats', formats, overrides), + registerModule(instanceId, '@stoplight/spectral-functions', functions, overrides), + registerModule(instanceId, '@stoplight/spectral-parsers', parsers, overrides), + registerModule(instanceId, '@stoplight/spectral-ref-resolver', refResolver, overrides), + registerModule(instanceId, '@stoplight/spectral-rulesets', rulesets, overrides), + registerModule(instanceId, '@stoplight/spectral-runtime', runtime, overrides), ]) as GlobalModules; return { - name: '@stoplight-spectral/builtins', - resolveId(id) { + name: NAME, + resolveId(id): string | null { if (id in modules) { return id; } return null; }, - load(id) { + load(id): string | undefined { if (id in modules) { return modules[id] as string; } diff --git a/packages/ruleset-bundler/src/plugins/commonjs.ts b/packages/ruleset-bundler/src/plugins/commonjs.ts new file mode 100644 index 000000000..f7640bbee --- /dev/null +++ b/packages/ruleset-bundler/src/plugins/commonjs.ts @@ -0,0 +1,3 @@ +import { default as commonjs } from '@rollup/plugin-commonjs'; + +export { commonjs }; diff --git a/packages/ruleset-bundler/src/plugins/virtualFs.ts b/packages/ruleset-bundler/src/plugins/virtualFs.ts index 6ba92dfc5..6c64d851d 100644 --- a/packages/ruleset-bundler/src/plugins/virtualFs.ts +++ b/packages/ruleset-bundler/src/plugins/virtualFs.ts @@ -1,35 +1,48 @@ import { dirname, parse, join, normalize, isAbsolute, isURL } from '@stoplight/path'; -import type { Plugin } from 'rollup'; +import type { Plugin, PluginContext } from 'rollup'; import type { IO } from '../types'; -export const virtualFs = ({ fs }: IO): Plugin => ({ - name: '@stoplight-spectral/virtual-fs', - resolveId(source, importer) { - const { protocol } = parse(source); - - if (protocol === 'http' || protocol === 'https') { - return null; - } - - if (protocol !== 'file' && !/^[./]/.test(source)) { - return null; - } - - if (isAbsolute(source)) { - return normalize(source); - } - - if (importer !== void 0) { - return join(dirname(importer), source); - } - - return source; - }, - load(id) { - if (!isURL(id)) { - return fs.promises.readFile(id, 'utf8'); - } - - return; - }, -}); +export const virtualFs = ({ fs }: IO): Plugin => { + const recognized = new WeakMap(); + + return { + name: '@stoplight-spectral/virtual-fs', + + resolveId(source, importer): string | null { + const { protocol } = parse(source); + + if (protocol === 'http' || protocol === 'https') { + return null; + } + + if (protocol !== 'file' && !/^[./]/.test(source)) { + return null; + } + + let resolvedSource = source; + + if (isAbsolute(source)) { + resolvedSource = normalize(source); + } else if (importer !== void 0) { + resolvedSource = join(dirname(importer), source); + } + + let existingEntries = recognized.get(this); + if (existingEntries === void 0) { + existingEntries = []; + recognized.set(this, existingEntries); + } + + existingEntries.push(resolvedSource); + + return resolvedSource; + }, + load(id): Promise | undefined { + if (!isURL(id) && recognized.get(this)?.includes(id) === true) { + return fs.promises.readFile(id, 'utf8'); + } + + return; + }, + }; +}; diff --git a/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts b/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts new file mode 100644 index 000000000..aef577791 --- /dev/null +++ b/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts @@ -0,0 +1,51 @@ +import type { Plugin } from 'rollup'; + +import { dedupeRollupPlugins } from '../dedupeRollupPlugins'; + +describe('dedupeRollupPlugins util', () => { + it('should keep plugins with different names', () => { + const plugins: Plugin[] = [ + { + name: 'plugin 1', + }, + { + name: 'plugin 2', + }, + { + name: 'plugin 3', + }, + ]; + + expect(dedupeRollupPlugins([...plugins])).toStrictEqual(plugins); + }); + + it('given the same plugin, should replace the first declaration', () => { + const plugins: Plugin[] = [ + { + name: 'plugin 1', + cacheKey: 'key 1', + }, + { + name: 'plugin 2', + }, + { + name: 'plugin 1', + cacheKey: 'key 2', + }, + { + name: 'plugin 1', + cacheKey: 'key 3', + }, + ]; + + expect(dedupeRollupPlugins([...plugins])).toStrictEqual([ + { + name: 'plugin 1', + cacheKey: 'key 3', + }, + { + name: 'plugin 2', + }, + ]); + }); +}); diff --git a/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts b/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts new file mode 100644 index 000000000..12ba67f6a --- /dev/null +++ b/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts @@ -0,0 +1,12 @@ +// this function makes sure we can only have one plugin with the same name +// the last plugin definition has a precedence +import type { Plugin } from 'rollup'; + +export function dedupeRollupPlugins(plugins: Plugin[]): Plugin[] { + const map = new Map(); + for (const plugin of plugins) { + map.set(plugin.name, plugin); + } + + return Array.from(map.values()); +} diff --git a/packages/rulesets/CHANGELOG.md b/packages/rulesets/CHANGELOG.md index 9395cb95b..c03c3798e 100644 --- a/packages/rulesets/CHANGELOG.md +++ b/packages/rulesets/CHANGELOG.md @@ -1,3 +1,30 @@ +# [@stoplight/spectral-rulesets-v1.6.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.5.2...@stoplight/spectral-rulesets-v1.6.0) (2022-03-03) + + +### Features + +* **rulesets:** validate API security in oas-operation-security-defined ([#2046](https://github.com/stoplightio/spectral/issues/2046)) ([5540250](https://github.com/stoplightio/spectral/commit/5540250035f0df290eb0cb0106606a2918471ec5)) + +# [@stoplight/spectral-rulesets-v1.5.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.5.1...@stoplight/spectral-rulesets-v1.5.2) (2022-02-28) + + +### Bug Fixes + +* **rulesets:** __importDefault undefined ([fdd647b](https://github.com/stoplightio/spectral/commit/fdd647b36b8d05c264b2320f0c8ea108e587d686)) + +# [@stoplight/spectral-rulesets-v1.5.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.5.0...@stoplight/spectral-rulesets-v1.5.1) (2022-02-28) + + +### Bug Fixes + +* **rulesets:** __importDefault undefined ([c123bdf](https://github.com/stoplightio/spectral/commit/c123bdf1dfe4d303bf477dc5c211e5b09bb37ed6)) + +# [@stoplight/spectral-rulesets-v1.5.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.4.3...@stoplight/spectral-rulesets-v1.5.0) (2022-02-24) + +### Features + +- **rulesets:** support 2.1.0, 2.2.0, 2.3.0 AsyncAPI versions ([#2067](https://github.com/stoplightio/spectral/issues/2067)) ([2f1d7bf](https://github.com/stoplightio/spectral/commit/2f1d7bf31010bc91102d844bf4279a784cad2d67)) + # [@stoplight/spectral-rulesets-v1.4.3](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.4.2...@stoplight/spectral-rulesets-v1.4.3) (2022-02-14) ### Bug Fixes diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index b0225574f..7275e4b2e 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-rulesets", - "version": "1.4.3", + "version": "1.6.0", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", @@ -21,10 +21,11 @@ "release": "semantic-release -e semantic-release-monorepo" }, "dependencies": { + "@asyncapi/specs": "^2.13.0", "@stoplight/better-ajv-errors": "1.0.1", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.8.1", - "@stoplight/spectral-formats": "^1.0.2", + "@stoplight/spectral-formats": "^1.1.0", "@stoplight/spectral-functions": "^1.5.1", "@stoplight/spectral-runtime": "^1.1.1", "@stoplight/types": "^12.3.0", diff --git a/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts b/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts index e23221019..5eaf939aa 100644 --- a/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts +++ b/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts @@ -1,3 +1,3 @@ -import testRule from '../../../__tests__/__helpers__/tester'; +import testRule, { createWithRules } from '../../../__tests__/__helpers__/tester'; -export { testRule as default }; +export { testRule as default, createWithRules }; diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts index 02b194ed1..0385bfb49 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts @@ -14,7 +14,6 @@ testRule('asyncapi-schema', [ }, errors: [], }, - { name: 'channels property is missing', document: { @@ -24,8 +23,6 @@ testRule('asyncapi-schema', [ version: '1.0', }, }, - errors: [ - { message: 'Object must have required property "channels"', path: [], severity: DiagnosticSeverity.Error }, - ], + errors: [{ message: 'Object must have required property "channels"', severity: DiagnosticSeverity.Error }], }, ]); diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts new file mode 100644 index 000000000..2d8a80c47 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts @@ -0,0 +1,349 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import { Spectral } from '@stoplight/spectral-core'; +import { prepareResults } from '../asyncApi2DocumentSchema'; + +import { ErrorObject } from 'ajv'; +import { createWithRules } from '../../__tests__/__helpers__/tester'; + +describe('asyncApi2DocumentSchema', () => { + let s: Spectral; + + beforeEach(async () => { + s = createWithRules(['asyncapi-schema']); + }); + + describe('given AsyncAPI 2.0.0 document', () => { + test('validate invalid info object', async () => { + expect( + await s.run({ + asyncapi: '2.0.0', + info: { + version: '1.0.1', + description: 'This is a sample server.', + termsOfService: 'http://asyncapi.org/terms/', + }, + channels: { + '/user/signedup': { + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + }, + }, + }, + }, + }), + ).toEqual([ + { + code: 'asyncapi-schema', + message: '"info" property must have required property "title"', + path: ['info'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + ]); + }); + }); + + describe('given AsyncAPI 2.1.0 document', () => { + test('validate with message examples', async () => { + expect( + await s.run({ + asyncapi: '2.1.0', + info: { + title: 'Signup service example (internal)', + version: '0.1.0', + }, + channels: { + '/user/signedup': { + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + examples: [ + { + name: 'Example 1', + summary: 'Example summary for example 1', + payload: { + email: 'bye@foo.bar', + }, + }, + ], + }, + }, + }, + }, + }), + ).toEqual([]); + }); + }); + + describe('given AsyncAPI 2.2.0 document', () => { + test('validate channel with connected server', async () => { + expect( + await s.run({ + asyncapi: '2.2.0', + info: { + title: 'Signup service example (internal)', + version: '0.1.0', + }, + servers: { + kafka: { + url: 'development.gigantic-server.com', + description: 'Development server', + protocol: 'kafka', + protocolVersion: '1.0.0', + }, + }, + channels: { + '/user/signedup': { + servers: [1, 'foobar', 3], + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + }, + }, + }, + }, + }), + ).toEqual([ + { + code: 'asyncapi-schema', + message: '"0" property type must be string', + path: ['channels', '/user/signedup', 'servers', '0'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + { + code: 'asyncapi-schema', + message: '"2" property type must be string', + path: ['channels', '/user/signedup', 'servers', '2'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + ]); + }); + }); + + describe('given AsyncAPI 2.3.0 document', () => { + test('validate reusable server', async () => { + expect( + await s.run({ + asyncapi: '2.3.0', + info: { + title: 'Signup service example (internal)', + version: '0.1.0', + }, + channels: { + '/user/signedup': { + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + }, + }, + }, + }, + components: { + servers: { + kafka: { + description: 'Development server', + }, + }, + }, + }), + ).toEqual([ + { + code: 'asyncapi-schema', + message: '"kafka" property must have required property "url"', + path: ['components', 'servers', 'kafka'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + ]); + }); + }); + + describe('prepareResults', () => { + test('given oneOf error one of which is required $ref property missing, picks only one error', () => { + const errors: ErrorObject[] = [ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'required', + instancePath: '/paths/test/post/parameters/0/schema', + schemaPath: '#/definitions/Reference/required', + params: { missingProperty: '$ref' }, + message: "must have required property '$ref'", + }, + { + keyword: 'oneOf', + instancePath: '/paths/test/post/parameters/0/schema', + schemaPath: '#/properties/schema/oneOf', + params: { passingSchemas: null }, + message: 'must match exactly one schema in oneOf', + }, + ]; + + prepareResults(errors); + + expect(errors).toStrictEqual([ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + ]); + }); + + test('given oneOf error one without any $ref property missing, picks all errors', () => { + const errors: ErrorObject[] = [ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/1/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'oneOf', + instancePath: '/paths/test/post/parameters/0/schema', + schemaPath: '#/properties/schema/oneOf', + params: { passingSchemas: null }, + message: 'must match exactly one schema in oneOf', + }, + ]; + + prepareResults(errors); + + expect(errors).toStrictEqual([ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + instancePath: '/paths/test/post/parameters/1/schema/type', + keyword: 'type', + message: 'must be string', + params: { + type: 'string', + }, + schemaPath: '#/properties/type/type', + }, + { + instancePath: '/paths/test/post/parameters/0/schema', + keyword: 'oneOf', + message: 'must match exactly one schema in oneOf', + params: { + passingSchemas: null, + }, + schemaPath: '#/properties/schema/oneOf', + }, + ]); + }); + + test('given errors with different data paths, picks all errors', () => { + const errors: ErrorObject[] = [ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'required', + instancePath: '/paths/foo/post/parameters/0/schema', + schemaPath: '#/definitions/Reference/required', + params: { missingProperty: '$ref' }, + message: "must have required property '$ref'", + }, + { + keyword: 'oneOf', + instancePath: '/paths/baz/post/parameters/0/schema', + schemaPath: '#/properties/schema/oneOf', + params: { passingSchemas: null }, + message: 'must match exactly one schema in oneOf', + }, + ]; + + prepareResults(errors); + + expect(errors).toStrictEqual([ + { + instancePath: '/paths/test/post/parameters/0/schema/type', + keyword: 'type', + message: 'must be string', + params: { + type: 'string', + }, + schemaPath: '#/properties/type/type', + }, + { + instancePath: '/paths/foo/post/parameters/0/schema', + keyword: 'required', + message: "must have required property '$ref'", + params: { + missingProperty: '$ref', + }, + schemaPath: '#/definitions/Reference/required', + }, + { + instancePath: '/paths/baz/post/parameters/0/schema', + keyword: 'oneOf', + message: 'must match exactly one schema in oneOf', + params: { + passingSchemas: null, + }, + schemaPath: '#/properties/schema/oneOf', + }, + ]); + }); + }); +}); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts new file mode 100644 index 000000000..dceae2ed7 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts @@ -0,0 +1,109 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; +import { schema as schemaFn } from '@stoplight/spectral-functions'; +import { aas2_0, aas2_1, aas2_2, aas2_3 } from '@stoplight/spectral-formats'; + +import type { ErrorObject } from 'ajv'; +import type { IFunctionResult, Format } from '@stoplight/spectral-core'; + +// import only 2.X.X AsyncAPI JSON Schemas for better treeshaking +import * as asyncAPI2_0_0Schema from '@asyncapi/specs/schemas/2.0.0.json'; +import * as asyncAPI2_1_0Schema from '@asyncapi/specs/schemas/2.1.0.json'; +import * as asyncAPI2_2_0Schema from '@asyncapi/specs/schemas/2.2.0.json'; +import * as asyncAPI2_3_0Schema from '@asyncapi/specs/schemas/2.3.0.json'; + +function shouldIgnoreError(error: ErrorObject): boolean { + return ( + // oneOf is a fairly error as we have 2 options to choose from for most of the time. + error.keyword === 'oneOf' || + // the required $ref is entirely useless, since aas-schema rules operate on resolved content, so there won't be any $refs in the document + (error.keyword === 'required' && error.params.missingProperty === '$ref') + ); +} + +// this is supposed to cover edge cases we need to cover manually, when it's impossible to detect the most appropriate error, i.e. oneOf consisting of more than 3 members, etc. +// note, more errors can be included if certain messages reported by AJV are not quite meaningful +const ERROR_MAP = [ + { + path: /^components\/securitySchemes\/[^/]+$/, + message: 'Invalid security scheme', + }, +]; + +// The function removes irrelevant (aka misleading, confusing, useless, whatever you call it) errors. +// There are a few exceptions, i.e. security components I covered manually, +// yet apart from them we usually deal with a relatively simple scenario that can be literally expressed as: "either proper value of $ref property". +// The $ref part is never going to be interesting for us, because both aas-schema rules operate on resolved content, so we won't have any $refs left. +// As you can see, what we deal here wit is actually not really oneOf anymore - it's always the first member of oneOf we match against. +// That being said, we always strip both oneOf and $ref, since we are always interested in the first error. +export function prepareResults(errors: ErrorObject[]): void { + // Update additionalProperties errors to make them more precise and prevent them from being treated as duplicates + for (const error of errors) { + if (error.keyword === 'additionalProperties') { + error.instancePath = `${error.instancePath}/${String(error.params['additionalProperty'])}`; + } + } + + for (let i = 0; i < errors.length; i++) { + const error = errors[i]; + + if (i + 1 < errors.length && errors[i + 1].instancePath === error.instancePath) { + errors.splice(i + 1, 1); + i--; + } else if (i > 0 && shouldIgnoreError(error) && errors[i - 1].instancePath.startsWith(error.instancePath)) { + errors.splice(i, 1); + i--; + } + } +} + +function applyManualReplacements(errors: IFunctionResult[]): void { + for (const error of errors) { + if (error.path === void 0) continue; + + const joinedPath = error.path.join('/'); + + for (const mappedError of ERROR_MAP) { + if (mappedError.path.test(joinedPath)) { + error.message = mappedError.message; + break; + } + } + } +} + +function getSchema(formats: Set): Record | void { + switch (true) { + case formats.has(aas2_0): + return asyncAPI2_0_0Schema; + case formats.has(aas2_1): + return asyncAPI2_1_0Schema; + case formats.has(aas2_2): + return asyncAPI2_2_0Schema; + case formats.has(aas2_3): + return asyncAPI2_3_0Schema; + default: + return; + } +} + +export default createRulesetFunction( + { + input: null, + options: null, + }, + function oasDocumentSchema(targetVal, _, context) { + const formats = context.document.formats; + if (formats === null || formats === void 0) return; + + const schema = getSchema(formats); + if (schema === void 0) return; + + const errors = schemaFn(targetVal, { allErrors: true, schema, prepareResults }, context); + + if (Array.isArray(errors)) { + applyManualReplacements(errors); + } + + return errors; + }, +); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts index fec61d070..501ccc79f 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts @@ -2,10 +2,11 @@ import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { createRulesetFunction } from '@stoplight/spectral-core'; import betterAjvErrors from '@stoplight/better-ajv-errors'; -import * as asyncApi2Schema from '../schemas/schema.asyncapi2.json'; -const fakeSchemaObjectId = 'asyncapi2#/definitions/schema'; -const asyncApi2SchemaObject = { $ref: fakeSchemaObjectId }; +// use latest AsyncAPI JSON Schema because there are no differences of Schema Object definitions between the 2.X.X. +import * as asyncApi2Schema from '@asyncapi/specs/schemas/2.3.0.json'; + +const asyncApi2SchemaObject = { $ref: 'asyncapi2#/definitions/schema' }; const ajv = new Ajv({ allErrors: true, @@ -14,7 +15,7 @@ const ajv = new Ajv({ addFormats(ajv); -ajv.addSchema(asyncApi2Schema, asyncApi2Schema.$id); +ajv.addSchema(asyncApi2Schema, 'asyncapi2'); const ajvValidationFn = ajv.compile(asyncApi2SchemaObject); diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 15c751c03..f6e83d7e8 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -1,4 +1,4 @@ -import { asyncApi2 } from '@stoplight/spectral-formats'; +import { aas2_0, aas2_1, aas2_2, aas2_3 } from '@stoplight/spectral-formats'; import { truthy, pattern, @@ -8,13 +8,13 @@ import { alphabetical, } from '@stoplight/spectral-functions'; +import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; -import * as asyncApi2Schema from './schemas/schema.asyncapi2.json'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', - formats: [asyncApi2], + formats: [aas2_0, aas2_1, aas2_2, aas2_3], rules: { 'asyncapi-channel-no-empty-parameter': { description: 'Channel path must not have empty parameter substitution pattern.', @@ -272,18 +272,14 @@ export default { }, }, 'asyncapi-schema': { - description: 'Validate structure of AsyncAPI v2.0.0 Specification.', + description: 'Validate structure of AsyncAPI v2 specification.', message: '{{error}}', severity: 'error', recommended: true, type: 'validation', given: '$', then: { - function: schema, - functionOptions: { - allErrors: true, - schema: asyncApi2Schema, - }, + function: asyncApi2DocumentSchema, }, }, 'asyncapi-server-no-empty-variable': { diff --git a/packages/rulesets/src/asyncapi/schemas/schema.asyncapi2.json b/packages/rulesets/src/asyncapi/schemas/schema.asyncapi2.json deleted file mode 100644 index e07443707..000000000 --- a/packages/rulesets/src/asyncapi/schemas/schema.asyncapi2.json +++ /dev/null @@ -1,1486 +0,0 @@ -{ - "title": "AsyncAPI 2.0.0 schema.", - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "asyncapi2", - "type": "object", - "required": ["asyncapi", "info", "channels"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "asyncapi": { - "type": "string", - "enum": ["2.0.0"], - "description": "The AsyncAPI specification version of this document." - }, - "id": { - "type": "string", - "description": "A unique id representing the application.", - "format": "uri" - }, - "info": { - "$ref": "#/definitions/info" - }, - "servers": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/server" - } - }, - "defaultContentType": { - "type": "string" - }, - "channels": { - "$ref": "#/definitions/channels" - }, - "components": { - "$ref": "#/definitions/components" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - } - }, - "definitions": { - "Reference": { - "type": "object", - "required": ["$ref"], - "properties": { - "$ref": { - "$ref": "#/definitions/ReferenceObject" - } - } - }, - "ReferenceObject": { - "type": "string", - "format": "uri-reference" - }, - "info": { - "type": "object", - "description": "General information about the API.", - "required": ["version", "title"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "title": { - "type": "string", - "description": "A unique and precise title of the API." - }, - "version": { - "type": "string", - "description": "A semantic version number of the API." - }, - "description": { - "type": "string", - "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." - }, - "termsOfService": { - "type": "string", - "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", - "format": "uri" - }, - "contact": { - "$ref": "#/definitions/contact" - }, - "license": { - "$ref": "#/definitions/license" - } - } - }, - "contact": { - "type": "object", - "description": "Contact information for the owners of the API.", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "The identifying name of the contact person/organization." - }, - "url": { - "type": "string", - "description": "The URL pointing to the contact information.", - "format": "uri" - }, - "email": { - "type": "string", - "description": "The email address of the contact person/organization.", - "format": "email" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "license": { - "type": "object", - "required": ["name"], - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "The name of the license type. It's encouraged to use an OSI compatible license." - }, - "url": { - "type": "string", - "description": "The URL pointing to the license.", - "format": "uri" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "server": { - "type": "object", - "description": "An object representing a Server.", - "required": ["url", "protocol"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "protocol": { - "type": "string", - "description": "The transfer protocol." - }, - "protocolVersion": { - "type": "string" - }, - "variables": { - "$ref": "#/definitions/serverVariables" - }, - "security": { - "type": "array", - "items": { - "$ref": "#/definitions/SecurityRequirement" - } - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "serverVariables": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/serverVariable" - } - }, - "serverVariable": { - "type": "object", - "description": "An object representing a Server Variable for server URL template substitution.", - "minProperties": 1, - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "enum": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "default": { - "type": "string" - }, - "description": { - "type": "string" - }, - "examples": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "channels": { - "type": "object", - "propertyNames": { - "type": "string", - "format": "uri-template", - "minLength": 1 - }, - "additionalProperties": { - "$ref": "#/definitions/channelItem" - } - }, - "components": { - "type": "object", - "description": "An object to hold a set of reusable objects for different aspects of the AsyncAPI Specification.", - "additionalProperties": false, - "properties": { - "schemas": { - "$ref": "#/definitions/schemas" - }, - "messages": { - "$ref": "#/definitions/messages" - }, - "securitySchemes": { - "type": "object", - "patternProperties": { - "^[\\w\\d.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/SecurityScheme" - } - ] - } - } - }, - "parameters": { - "$ref": "#/definitions/parameters" - }, - "correlationIds": { - "type": "object", - "patternProperties": { - "^[\\w\\d.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/correlationId" - } - ] - } - } - }, - "operationTraits": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/operationTrait" - } - }, - "messageTraits": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/messageTrait" - } - }, - "serverBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - }, - "channelBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - }, - "operationBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - }, - "messageBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - } - } - }, - "schemas": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema" - }, - "description": "JSON objects describing schemas the API uses." - }, - "messages": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/message" - }, - "description": "JSON objects describing the messages being consumed and produced by the API." - }, - "parameters": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/parameter" - }, - "description": "JSON objects describing re-usable channel parameters." - }, - "schema": { - "allOf": [ - { - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/schema/allOf/0" - } - }, - "nonNegativeInteger": { - "type": "integer", - "minimum": 0 - }, - "nonNegativeIntegerDefault0": { - "allOf": [ - { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - { - "default": 0 - } - ] - }, - "simpleTypes": { - "enum": ["array", "boolean", "integer", "null", "number", "object", "string"] - }, - "stringArray": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true, - "default": [] - } - }, - "type": ["object", "boolean"], - "properties": { - "$id": { - "type": "string", - "format": "uri-reference" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "$ref": { - "type": "string", - "format": "uri-reference" - }, - "$comment": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": true, - "readOnly": { - "type": "boolean", - "default": false - }, - "writeOnly": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": true - }, - "multipleOf": { - "type": "number", - "exclusiveMinimum": 0 - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "number" - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "number" - }, - "maxLength": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - "minLength": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "$ref": "#/definitions/schema/allOf/0" - }, - "items": { - "anyOf": [ - { - "$ref": "#/definitions/schema/allOf/0" - }, - { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - } - ], - "default": true - }, - "maxItems": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - "minItems": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "contains": { - "$ref": "#/definitions/schema/allOf/0" - }, - "maxProperties": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - "minProperties": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" - }, - "required": { - "$ref": "#/definitions/schema/allOf/0/definitions/stringArray" - }, - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "definitions": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "propertyNames": { - "format": "regex" - }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/schema/allOf/0" - }, - { - "$ref": "#/definitions/schema/allOf/0/definitions/stringArray" - } - ] - } - }, - "propertyNames": { - "$ref": "#/definitions/schema/allOf/0" - }, - "const": true, - "enum": { - "type": "array", - "items": true, - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - { - "$ref": "#/definitions/schema/allOf/0/definitions/simpleTypes" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/schema/allOf/0/definitions/simpleTypes" - }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "format": { - "type": "string" - }, - "contentMediaType": { - "type": "string" - }, - "contentEncoding": { - "type": "string" - }, - "if": { - "$ref": "#/definitions/schema/allOf/0" - }, - "then": { - "$ref": "#/definitions/schema/allOf/0" - }, - "else": { - "$ref": "#/definitions/schema/allOf/0" - }, - "allOf": { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - }, - "anyOf": { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - }, - "oneOf": { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - }, - "not": { - "$ref": "#/definitions/schema/allOf/0" - } - }, - "default": true - }, - { - "type": "object", - "patternProperties": { - "c": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/schema" - }, - { - "type": "boolean" - } - ], - "default": {} - }, - "items": { - "anyOf": [ - { - "$ref": "#/definitions/schema" - }, - { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/schema" - } - } - ], - "default": {} - }, - "allOf": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/schema" - } - }, - "oneOf": { - "type": "array", - "minItems": 2, - "items": { - "$ref": "#/definitions/schema" - } - }, - "anyOf": { - "type": "array", - "minItems": 2, - "items": { - "$ref": "#/definitions/schema" - } - }, - "not": { - "$ref": "#/definitions/schema" - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema" - }, - "default": {} - }, - "propertyNames": { - "$ref": "#/definitions/schema" - }, - "contains": { - "$ref": "#/definitions/schema" - }, - "discriminator": { - "type": "string" - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "deprecated": { - "type": "boolean", - "default": false - } - } - } - ] - }, - "externalDocs": { - "type": "object", - "additionalProperties": false, - "description": "information about external documentation", - "required": ["url"], - "properties": { - "description": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "channelItem": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "minProperties": 1, - "properties": { - "$ref": { - "$ref": "#/definitions/ReferenceObject" - }, - "parameters": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/parameter" - } - }, - "description": { - "type": "string", - "description": "A description of the channel." - }, - "publish": { - "$ref": "#/definitions/operation" - }, - "subscribe": { - "$ref": "#/definitions/operation" - }, - "deprecated": { - "type": "boolean", - "default": false - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "parameter": { - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "description": { - "type": "string", - "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." - }, - "schema": { - "$ref": "#/definitions/schema" - }, - "location": { - "type": "string", - "description": "A runtime expression that specifies the location of the parameter value", - "pattern": "^\\$message\\.(header|payload)#(/(([^/~])|(~[01]))*)*" - }, - "$ref": { - "$ref": "#/definitions/ReferenceObject" - } - } - }, - "operation": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "traits": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/operationTrait" - }, - { - "type": "array", - "items": [ - { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/operationTrait" - } - ] - }, - { - "type": "object", - "additionalItems": true - } - ] - } - ] - } - }, - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "operationId": { - "type": "string" - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - }, - "message": { - "$ref": "#/definitions/message" - } - } - }, - "message": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "oneOf": [ - { - "type": "object", - "required": ["oneOf"], - "additionalProperties": false, - "properties": { - "oneOf": { - "type": "array", - "items": { - "$ref": "#/definitions/message" - } - } - } - }, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "schemaFormat": { - "type": "string" - }, - "contentType": { - "type": "string" - }, - "headers": { - "$ref": "#/definitions/schema" - }, - "payload": {}, - "correlationId": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/correlationId" - } - ] - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "summary": { - "type": "string", - "description": "A brief summary of the message." - }, - "name": { - "type": "string", - "description": "Name of the message." - }, - "title": { - "type": "string", - "description": "A human-friendly title for the message." - }, - "description": { - "type": "string", - "description": "A longer description of the message. CommonMark is allowed." - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "deprecated": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": { - "type": "object" - } - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - }, - "traits": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/messageTrait" - }, - { - "type": "array", - "items": [ - { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/messageTrait" - } - ] - }, - { - "type": "object", - "additionalItems": true - } - ] - } - ] - } - } - } - } - ] - } - ] - }, - "bindingsObject": { - "type": "object", - "additionalProperties": true, - "properties": { - "http": {}, - "ws": {}, - "amqp": {}, - "amqp1": {}, - "mqtt": {}, - "mqtt5": {}, - "kafka": {}, - "nats": {}, - "jms": {}, - "sns": {}, - "sqs": {}, - "stomp": {}, - "redis": {} - } - }, - "correlationId": { - "type": "object", - "required": ["location"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "description": { - "type": "string", - "description": "A optional description of the correlation ID. GitHub Flavored Markdown is allowed." - }, - "location": { - "type": "string", - "description": "A runtime expression that specifies the location of the correlation ID", - "pattern": "^\\$message\\.(header|payload)#(/(([^/~])|(~[01]))*)*" - } - } - }, - "specificationExtension": { - "description": "Any property starting with x- is valid.", - "additionalProperties": true, - "additionalItems": true - }, - "tag": { - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "operationTrait": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "operationId": { - "type": "string" - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "messageTrait": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "schemaFormat": { - "type": "string" - }, - "contentType": { - "type": "string" - }, - "headers": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/schema" - } - ] - }, - "correlationId": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/correlationId" - } - ] - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "summary": { - "type": "string", - "description": "A brief summary of the message." - }, - "name": { - "type": "string", - "description": "Name of the message." - }, - "title": { - "type": "string", - "description": "A human-friendly title for the message." - }, - "description": { - "type": "string", - "description": "A longer description of the message. CommonMark is allowed." - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "deprecated": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": { - "type": "object" - } - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "SecurityScheme": { - "oneOf": [ - { - "$ref": "#/definitions/userPassword" - }, - { - "$ref": "#/definitions/apiKey" - }, - { - "$ref": "#/definitions/X509" - }, - { - "$ref": "#/definitions/symmetricEncryption" - }, - { - "$ref": "#/definitions/asymmetricEncryption" - }, - { - "$ref": "#/definitions/HTTPSecurityScheme" - }, - { - "$ref": "#/definitions/oauth2Flows" - }, - { - "$ref": "#/definitions/openIdConnect" - } - ] - }, - "userPassword": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["userPassword"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "apiKey": { - "type": "object", - "required": ["type", "in"], - "properties": { - "type": { - "type": "string", - "enum": ["apiKey"] - }, - "in": { - "type": "string", - "enum": ["user", "password"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "X509": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["X509"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "symmetricEncryption": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["symmetricEncryption"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "asymmetricEncryption": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["asymmetricEncryption"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "HTTPSecurityScheme": { - "oneOf": [ - { - "$ref": "#/definitions/NonBearerHTTPSecurityScheme" - }, - { - "$ref": "#/definitions/BearerHTTPSecurityScheme" - }, - { - "$ref": "#/definitions/APIKeyHTTPSecurityScheme" - } - ] - }, - "NonBearerHTTPSecurityScheme": { - "not": { - "type": "object", - "properties": { - "scheme": { - "type": "string", - "enum": ["bearer"] - } - } - }, - "type": "object", - "required": ["scheme", "type"], - "properties": { - "scheme": { - "type": "string" - }, - "description": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["http"] - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "BearerHTTPSecurityScheme": { - "type": "object", - "required": ["type", "scheme"], - "properties": { - "scheme": { - "type": "string", - "enum": ["bearer"] - }, - "bearerFormat": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["http"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "APIKeyHTTPSecurityScheme": { - "type": "object", - "required": ["type", "name", "in"], - "properties": { - "type": { - "type": "string", - "enum": ["httpApiKey"] - }, - "name": { - "type": "string" - }, - "in": { - "type": "string", - "enum": ["header", "query", "cookie"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "oauth2Flows": { - "type": "object", - "required": ["type", "flows"], - "properties": { - "type": { - "type": "string", - "enum": ["oauth2"] - }, - "description": { - "type": "string" - }, - "flows": { - "type": "object", - "properties": { - "implicit": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["authorizationUrl", "scopes"] - }, - { - "not": { - "required": ["tokenUrl"] - } - } - ] - }, - "password": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["tokenUrl", "scopes"] - }, - { - "not": { - "required": ["authorizationUrl"] - } - } - ] - }, - "clientCredentials": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["tokenUrl", "scopes"] - }, - { - "not": { - "required": ["authorizationUrl"] - } - } - ] - }, - "authorizationCode": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["authorizationUrl", "tokenUrl", "scopes"] - } - ] - } - }, - "additionalProperties": false, - "minProperties": 1 - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "oauth2Flow": { - "type": "object", - "properties": { - "authorizationUrl": { - "type": "string", - "format": "uri" - }, - "tokenUrl": { - "type": "string", - "format": "uri" - }, - "refreshUrl": { - "type": "string", - "format": "uri" - }, - "scopes": { - "$ref": "#/definitions/oauth2Scopes" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "oauth2Scopes": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "openIdConnect": { - "type": "object", - "required": ["type", "openIdConnectUrl"], - "properties": { - "type": { - "type": "string", - "enum": ["openIdConnect"] - }, - "description": { - "type": "string" - }, - "openIdConnectUrl": { - "type": "string", - "format": "uri" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "SecurityRequirement": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - } - } - } -} diff --git a/packages/rulesets/src/index.ts b/packages/rulesets/src/index.ts index f85f7a30b..8f5b53792 100644 --- a/packages/rulesets/src/index.ts +++ b/packages/rulesets/src/index.ts @@ -1,2 +1,4 @@ -export { default as oas } from './oas'; -export { default as asyncapi } from './asyncapi'; +import { default as oas } from './oas'; +import { default as asyncapi } from './asyncapi'; + +export { oas, asyncapi }; diff --git a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts index df1431246..92d714ac5 100644 --- a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts @@ -5,6 +5,7 @@ testRule('oas2-operation-security-defined', [ { name: 'a correct object (just in body)', document: { + swagger: '2.0', securityDefinitions: { apikey: {}, }, @@ -23,6 +24,27 @@ testRule('oas2-operation-security-defined', [ errors: [], }, + { + name: 'a correct object (API-level security)', + document: { + swagger: '2.0', + securityDefinitions: { + apikey: {}, + }, + security: [ + { + apikey: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [], + }, + { name: 'invalid object', document: { @@ -43,7 +65,89 @@ testRule('oas2-operation-security-defined', [ errors: [ { message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', - path: ['paths', '/path', 'get', 'security', '0'], + path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'invalid object (API-level security)', + document: { + swagger: '2.0', + securityDefinitions: {}, + security: [ + { + apikey: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'valid and invalid object', + document: { + swagger: '2.0', + securityDefinitions: { + apikey: {}, + }, + paths: { + '/path': { + get: { + security: [ + { + apikey: [], + basic: [], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['paths', '/path', 'get', 'security', '0', 'basic'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'valid and invalid object (API-level security)', + document: { + swagger: '2.0', + securityDefinitions: { + apikey: {}, + }, + security: [ + { + apikey: [], + basic: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['security', '0', 'basic'], severity: DiagnosticSeverity.Warning, }, ], diff --git a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts index ea518c723..f3d7f5212 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts @@ -25,6 +25,28 @@ testRule('oas3-operation-security-defined', [ }, errors: [], }, + { + name: 'validate a correct object (API-level security)', + document: { + openapi: '3.0.2', + components: { + securitySchemes: { + apikey: {}, + }, + security: [ + { + apikey: [], + }, + ], + }, + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [], + }, { name: 'return errors on invalid object', @@ -46,7 +68,93 @@ testRule('oas3-operation-security-defined', [ errors: [ { message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', - path: ['paths', '/path', 'get', 'security', '0'], + path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'return errors on invalid object (API-level)', + document: { + openapi: '3.0.2', + components: {}, + security: [ + { + apikey: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'return errors on valid and invalid object', + document: { + openapi: '3.0.2', + components: { + securitySchemes: { + apikey: {}, + }, + }, + paths: { + '/path': { + get: { + security: [ + { + apikey: [], + basic: [], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['paths', '/path', 'get', 'security', '0', 'basic'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'valid and invalid object (API-level security)', + document: { + openapi: '3.0.2', + components: { + securitySchemes: { + apikey: {}, + }, + }, + security: [ + { + apikey: [], + basic: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['security', '0', 'basic'], severity: DiagnosticSeverity.Warning, }, ], diff --git a/packages/rulesets/src/oas/functions/index.ts b/packages/rulesets/src/oas/functions/index.ts index 2fe7c0a20..430d8d9a8 100644 --- a/packages/rulesets/src/oas/functions/index.ts +++ b/packages/rulesets/src/oas/functions/index.ts @@ -1,14 +1,31 @@ -export { default as oasOpParams } from './oasOpParams'; -export { default as oasSchema } from './oasSchema'; -export { default as oasDocumentSchema } from './oasDocumentSchema'; -export { default as oasOpFormDataConsumeCheck } from './oasOpFormDataConsumeCheck'; -export { default as oasOpSuccessResponse } from './oasOpSuccessResponse'; -export { default as oasExample } from './oasExample'; -export { default as oasOpSecurityDefined } from './oasOpSecurityDefined'; -export { default as typedEnum } from './typedEnum'; -export { default as refSiblings } from './refSiblings'; -export { default as oasPathParam } from './oasPathParam'; -export { default as oasTagDefined } from './oasTagDefined'; -export { default as oasUnusedComponent } from './oasUnusedComponent'; -export { default as oasOpIdUnique } from './oasOpIdUnique'; -export { default as oasDiscriminator } from './oasDiscriminator'; +import { default as oasOpParams } from './oasOpParams'; +import { default as oasSchema } from './oasSchema'; +import { default as oasDocumentSchema } from './oasDocumentSchema'; +import { default as oasOpFormDataConsumeCheck } from './oasOpFormDataConsumeCheck'; +import { default as oasOpSuccessResponse } from './oasOpSuccessResponse'; +import { default as oasExample } from './oasExample'; +import { default as oasOpSecurityDefined } from './oasOpSecurityDefined'; +import { default as typedEnum } from './typedEnum'; +import { default as refSiblings } from './refSiblings'; +import { default as oasPathParam } from './oasPathParam'; +import { default as oasTagDefined } from './oasTagDefined'; +import { default as oasUnusedComponent } from './oasUnusedComponent'; +import { default as oasOpIdUnique } from './oasOpIdUnique'; +import { default as oasDiscriminator } from './oasDiscriminator'; + +export { + oasOpParams, + oasSchema, + oasDocumentSchema, + oasOpFormDataConsumeCheck, + oasOpSuccessResponse, + oasExample, + oasOpSecurityDefined, + typedEnum, + refSiblings, + oasPathParam, + oasTagDefined, + oasUnusedComponent, + oasOpIdUnique, + oasDiscriminator, +}; diff --git a/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts index 31f42c104..37edd2eca 100644 --- a/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts +++ b/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts @@ -20,7 +20,7 @@ type Options = { schemesPath: JsonPath; }; -export default createRulesetFunction<{ paths: Record }, Options>( +export default createRulesetFunction<{ paths: Record; security: unknown[] }, Options>( { input: { type: 'object', @@ -28,6 +28,9 @@ export default createRulesetFunction<{ paths: Record }, Options paths: { type: 'object', }, + security: { + type: 'array', + }, }, }, options: { @@ -50,6 +53,29 @@ export default createRulesetFunction<{ paths: Record }, Options const schemes = _get(targetVal, schemesPath); const allDefs = isObject(schemes) ? Object.keys(schemes) : []; + // Check global security requirements + + const { security } = targetVal; + + if (Array.isArray(security)) { + for (const [index, value] of security.entries()) { + if (!isObject(value)) { + continue; + } + + const securityKeys = Object.keys(value); + + for (const securityKey of securityKeys) { + if (!allDefs.includes(securityKey)) { + results.push({ + message: `API "security" values must match a scheme defined in the "${schemesPath.join('.')}" object.`, + path: ['security', index, securityKey], + }); + } + } + } + } + for (const { path, operation, value } of getAllOperations(paths)) { if (!isObject(value)) continue; @@ -66,11 +92,15 @@ export default createRulesetFunction<{ paths: Record }, Options const securityKeys = Object.keys(value); - if (securityKeys.length > 0 && !allDefs.includes(securityKeys[0])) { - results.push({ - message: 'Operation must not reference an undefined security scheme.', - path: ['paths', path, operation, 'security', index], - }); + for (const securityKey of securityKeys) { + if (!allDefs.includes(securityKey)) { + results.push({ + message: `Operation "security" values must match a scheme defined in the "${schemesPath.join( + '.', + )}" object.`, + path: ['paths', path, operation, 'security', index, securityKey], + }); + } } } } diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 6e7a4d4fe..8d7e817ff 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -449,6 +449,7 @@ const ruleset = { }, 'oas2-operation-security-defined': { description: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', + message: '{{error}}', recommended: true, formats: [oas2], type: 'validation', @@ -590,6 +591,7 @@ const ruleset = { 'oas3-operation-security-defined': { description: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', + message: '{{error}}', recommended: true, formats: [oas3], type: 'validation', diff --git a/test-harness/scenarios/runtime-errors.scenario b/test-harness/scenarios/runtime-errors.scenario new file mode 100644 index 000000000..c675c986c --- /dev/null +++ b/test-harness/scenarios/runtime-errors.scenario @@ -0,0 +1,68 @@ +====test==== +Logs all runtime errors thrown during the linting process +====document==== +schemas: + user: + type: object + properties: + name: + type: number + age: + type: number + occupation: + type: boolean + addresses: + - +====command==== +{bin} lint {document} -r "{asset:ruleset.js}" +====asset:ruleset.js==== +const { truthy } = require("@stoplight/spectral-functions"); + +function validAddress(input) { + throw new TypeError(`Cannot read properties of null (reading 'test')`); +} + +function upperCase() { + return String(input).toLowerCase() === String(input).toUpperCase(); +} + +module.exports = { + "rules": { + "valid-user-properties": { + "severity": "error", + "given": [ + "$.schemas.user.properties.name", + "$.schemas.user.properties.occupation" + ], + "then": { + "field": "type", + "function": upperCase, + } + }, + "valid-address": { + "given": [ + "$..addresses[*]", + ], + "then": { + "function": validAddress + } + }, + "require-user-and-address": { + "severity": "error", + "given": [ + "$.schemas.user", + ], + "then": { + "function": truthy + } + } + } +} +====status==== +2 +====stderr==== +Error running Spectral! +Use --verbose flag to print the error stack. +Error #1: Function "upperCase" threw an exception: input is not defined +Error #2: Function "upperCase" threw an exception: input is not defined +Error #3: Function "validAddress" threw an exception: Cannot read properties of null (reading 'test') diff --git a/tsconfig.json b/tsconfig.json index f57c5fa8c..b260097c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ "target": "ES2019", "module": "CommonJS", "esModuleInterop": true, - "lib": ["es2020"], + "lib": ["es2021"], "strict": true, "pretty": true, "experimentalDecorators": true, diff --git a/yarn.lock b/yarn.lock index 27ec2d5a0..c809499b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,13 @@ __metadata: languageName: node linkType: hard +"@asyncapi/specs@npm:^2.13.0": + version: 2.13.0 + resolution: "@asyncapi/specs@npm:2.13.0" + checksum: 94355c96ac2562bfd9118a3e33dd36359196d070684da952f6b0f800b588b426d012fbf96f85b7341ec74f401e3487934b92e43f48e086fa956eab29b90ab694 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -2070,6 +2077,23 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-commonjs@npm:^21.0.1": + version: 21.0.1 + resolution: "@rollup/plugin-commonjs@npm:21.0.1" + dependencies: + "@rollup/pluginutils": ^3.1.0 + commondir: ^1.0.1 + estree-walker: ^2.0.1 + glob: ^7.1.6 + is-reference: ^1.2.1 + magic-string: ^0.25.7 + resolve: ^1.17.0 + peerDependencies: + rollup: ^2.38.3 + checksum: 3e56be58c72d655face6f361f85923ddcc3cc07760b5a3a91cfc728115dfed358fc595781148c512d53a03be8c703133379f228e78fd2aed8655fae9d83800b6 + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^3.1.0": version: 3.1.0 resolution: "@rollup/pluginutils@npm:3.1.0" @@ -2172,9 +2196,9 @@ __metadata: languageName: node linkType: hard -"@semantic-release/npm@npm:^9.0.0": - version: 9.0.0 - resolution: "@semantic-release/npm@npm:9.0.0" +"@semantic-release/npm@npm:^8.0.0": + version: 8.0.3 + resolution: "@semantic-release/npm@npm:8.0.3" dependencies: "@semantic-release/error": ^3.0.0 aggregate-error: ^3.0.0 @@ -2183,15 +2207,15 @@ __metadata: lodash: ^4.17.15 nerf-dart: ^1.0.0 normalize-url: ^6.0.0 - npm: ^8.3.0 + npm: ^7.0.0 rc: ^1.2.8 read-pkg: ^5.0.0 registry-auth-token: ^4.0.0 semver: ^7.1.2 tempy: ^1.0.0 peerDependencies: - semantic-release: ">=19.0.0" - checksum: e5cbb66702d9c7030b7e03f0f74764b321fc3ee6d151207180874df933643eb6a4b4f29eec130bbe1521190c169a6c36813afaa853365fb7affd8d6d7642f69a + semantic-release: ">=18.0.0" + checksum: 6c1e178f0fdc1b6ab24d14f02fb012302c7220e64e192293be7d11346d309b00338bd5a42e076c7849af68a91745359e750c5a1d7c85c8f11e9941e4516bb413 languageName: node linkType: hard @@ -2325,12 +2349,14 @@ __metadata: "@stoplight/spectral-rulesets": ">=1" "@stoplight/spectral-runtime": ^1.1.0 "@stoplight/types": 12.3.0 + "@types/es-aggregate-error": ^1.0.2 "@types/xml2js": ^0.4.9 "@types/yargs": ^17.0.8 chalk: 4.1.2 cliui: 7.0.4 copyfiles: ^2.4.1 eol: 0.9.1 + es-aggregate-error: ^1.0.7 fast-glob: 3.2.7 jest-when: ^3.4.2 lodash: ~4.17.21 @@ -2339,6 +2365,7 @@ __metadata: pkg: ^5.4.1 pony-cause: ^1.0.0 proxy-agent: 5.0.0 + stacktracey: ^2.1.7 strip-ansi: 6.0 text-table: 0.2 tslib: ^2.3.0 @@ -2376,15 +2403,16 @@ __metadata: lodash: ~4.17.21 lodash.topath: ^4.5.2 minimatch: 3.0.4 - nimma: 0.1.7 + nimma: 0.1.8 nock: ^13.1.0 + pony-cause: ^1.0.0 simple-eval: 1.0.0 treeify: ^1.1.0 tslib: ^2.3.0 languageName: unknown linkType: soft -"@stoplight/spectral-formats@*, @stoplight/spectral-formats@>=1, @stoplight/spectral-formats@^1.0.0, @stoplight/spectral-formats@^1.0.2, @stoplight/spectral-formats@workspace:packages/formats": +"@stoplight/spectral-formats@*, @stoplight/spectral-formats@>=1, @stoplight/spectral-formats@^1.0.0, @stoplight/spectral-formats@^1.1.0, @stoplight/spectral-formats@workspace:packages/formats": version: 0.0.0-use.local resolution: "@stoplight/spectral-formats@workspace:packages/formats" dependencies: @@ -2442,6 +2470,7 @@ __metadata: version: 0.0.0-use.local resolution: "@stoplight/spectral-ruleset-bundler@workspace:packages/ruleset-bundler" dependencies: + "@rollup/plugin-commonjs": ^21.0.1 "@stoplight/path": 1.3.2 "@stoplight/spectral-core": ">=1" "@stoplight/spectral-formats": ">=1" @@ -2458,7 +2487,7 @@ __metadata: memfs: ^3.3.0 pony-cause: 1.1.1 prettier: ^2.4.1 - rollup: ~2.60.2 + rollup: ~2.67.0 tslib: ^2.3.1 validate-npm-package-name: 3.0.0 languageName: unknown @@ -2495,11 +2524,12 @@ __metadata: version: 0.0.0-use.local resolution: "@stoplight/spectral-rulesets@workspace:packages/rulesets" dependencies: + "@asyncapi/specs": ^2.13.0 "@stoplight/better-ajv-errors": 1.0.1 "@stoplight/json": ^3.17.0 "@stoplight/path": ^1.3.2 "@stoplight/spectral-core": ^1.8.1 - "@stoplight/spectral-formats": ^1.0.2 + "@stoplight/spectral-formats": ^1.1.0 "@stoplight/spectral-functions": ^1.5.1 "@stoplight/spectral-parsers": "*" "@stoplight/spectral-ref-resolver": "*" @@ -2662,6 +2692,15 @@ __metadata: languageName: node linkType: hard +"@types/es-aggregate-error@npm:^1.0.2": + version: 1.0.2 + resolution: "@types/es-aggregate-error@npm:1.0.2" + dependencies: + "@types/node": "*" + checksum: 076fd59b595f33c8c7e7eb68ec55bd43cf8b2cf7bbc6778e25d7ae1a5fa0538a0a56f149015f403d7bbcefe59f1d8182351685b59c1fe719fd46d0dd8a9737fa + languageName: node + linkType: hard + "@types/estree@npm:*": version: 0.0.50 resolution: "@types/estree@npm:0.0.50" @@ -3092,7 +3131,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": +"acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0": version: 8.2.0 resolution: "acorn-walk@npm:8.2.0" checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 @@ -3117,6 +3156,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.7.0": + version: 8.7.0 + resolution: "acorn@npm:8.7.0" + bin: + acorn: bin/acorn + checksum: e0f79409d68923fbf1aa6d4166f3eedc47955320d25c89a20cc822e6ba7c48c5963d5bc657bc242d68f7a4ac9faf96eef033e8f73656da6c640d4219935fdfd0 + languageName: node + linkType: hard + "agent-base@npm:6, agent-base@npm:^6.0.0, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -3213,7 +3261,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.1": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -3222,15 +3270,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^5.0.0": - version: 5.0.0 - resolution: "ansi-escapes@npm:5.0.0" - dependencies: - type-fest: ^1.0.2 - checksum: d4b5eb8207df38367945f5dd2ef41e08c28edc192dc766ef18af6b53736682f49d8bfcfa4e4d6ecbc2e2f97c258fda084fb29a9e43b69170b71090f771afccac - languageName: node - linkType: hard - "ansi-regex@npm:^2.0.0, ansi-regex@npm:^2.1.1": version: 2.1.1 resolution: "ansi-regex@npm:2.1.1" @@ -3417,6 +3456,15 @@ __metadata: languageName: node linkType: hard +"as-table@npm:^1.0.36": + version: 1.0.55 + resolution: "as-table@npm:1.0.55" + dependencies: + printable-characters: ^1.0.42 + checksum: 341c99d9e99a702c315b3f0744d49b4764b26ef7ddd32bafb9e1706626560c0e599100521fc1b17f640e804bd0503ce70b2ba519c023da6edf06bdd9086dccd9 + languageName: node + linkType: hard + "asap@npm:^2.0.0": version: 2.0.6 resolution: "asap@npm:2.0.6" @@ -3986,7 +4034,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:*, chalk@npm:^5.0.0": +"chalk@npm:*": version: 5.0.0 resolution: "chalk@npm:5.0.0" checksum: 6eba7c518b9aa5fe882ae6d14a1ffa58c418d72a3faa7f72af56641f1bbef51b645fca1d6e05d42357b7d3c846cd504c0b7b64d12309cdd07867e3b4411e0d01 @@ -4134,7 +4182,7 @@ __metadata: languageName: node linkType: hard -"cli-table3@npm:*, cli-table3@npm:^0.6.1": +"cli-table3@npm:*, cli-table3@npm:^0.6.0": version: 0.6.1 resolution: "cli-table3@npm:0.6.1" dependencies: @@ -4709,6 +4757,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^2.0.0": + version: 2.0.2 + resolution: "data-uri-to-buffer@npm:2.0.2" + checksum: 152bec5e77513ee253a7c686700a1723246f582dad8b614e8eaaaba7fa45a15c8671ae4b8f4843f4f3a002dae1d3e7a20f852f7d7bdc8b4c15cfe7adfdfb07f8 + languageName: node + linkType: hard + "data-urls@npm:^2.0.0": version: 2.0.0 resolution: "data-urls@npm:2.0.0" @@ -4858,14 +4913,14 @@ __metadata: linkType: hard "degenerator@npm:^3.0.1": - version: 3.0.1 - resolution: "degenerator@npm:3.0.1" + version: 3.0.2 + resolution: "degenerator@npm:3.0.2" dependencies: ast-types: ^0.13.2 escodegen: ^1.8.1 esprima: ^4.0.0 - vm2: ^3.9.3 - checksum: 110d5fa772933d21484995e518feeb2ea54e5804421edf8546900973a227dcdf621a0cbac0a5d0a13273424ea3763aba815246dfffa386483f5480d60f50bed1 + vm2: ^3.9.8 + checksum: 6a8fffe1ddde692931a1d74c0636d9e6963f2aa16748d4b95f4833cdcbe8df571e5c127e4f1d625a4c340cc60f5a969ac9e5aa14baecfb6f69b85638e180cd97 languageName: node linkType: hard @@ -5290,6 +5345,20 @@ __metadata: languageName: node linkType: hard +"es-aggregate-error@npm:^1.0.7": + version: 1.0.7 + resolution: "es-aggregate-error@npm:1.0.7" + dependencies: + define-properties: ^1.1.3 + es-abstract: ^1.19.0 + function-bind: ^1.1.1 + functions-have-names: ^1.2.2 + get-intrinsic: ^1.1.1 + globalthis: ^1.0.2 + checksum: 16b89fefdf56c0478cd21577249156cf83e44c2220c057cbfddd99c01e15e03d6d90a85ce73dece4728a5bfcb022dc160e04a66b1f83a620f140842c6f8325f9 + languageName: node + linkType: hard + "es-to-primitive@npm:^1.2.1": version: 1.2.1 resolution: "es-to-primitive@npm:1.2.1" @@ -6194,6 +6263,13 @@ __metadata: languageName: node linkType: hard +"functions-have-names@npm:^1.2.2": + version: 1.2.2 + resolution: "functions-have-names@npm:1.2.2" + checksum: 25f44b6d1c41ac86ffdf41f25d1de81c0a5b4a3fcf4307a33cdfb23b9d4bd5d0d8bf312eaef5ad368c6500c8a9e19f692b8ce9f96aaab99db9dd936554165558 + languageName: node + linkType: hard + "gauge@npm:^4.0.0": version: 4.0.0 resolution: "gauge@npm:4.0.0" @@ -6259,6 +6335,16 @@ __metadata: languageName: node linkType: hard +"get-source@npm:^2.0.12": + version: 2.0.12 + resolution: "get-source@npm:2.0.12" + dependencies: + data-uri-to-buffer: ^2.0.0 + source-map: ^0.6.1 + checksum: c73368fee709594ba38682ec1a96872aac6f7d766396019611d3d2358b68186a7847765a773ea0db088c42781126cc6bc09e4b87f263951c74dae5dcea50ad42 + languageName: node + linkType: hard + "get-stdin@npm:^8.0.0": version: 8.0.0 resolution: "get-stdin@npm:8.0.0" @@ -6406,6 +6492,15 @@ __metadata: languageName: node linkType: hard +"globalthis@npm:^1.0.2": + version: 1.0.2 + resolution: "globalthis@npm:1.0.2" + dependencies: + define-properties: ^1.1.3 + checksum: 5a5f3c7ab94708260a98106b35946b74bb57f6b2013e39668dc9e8770b80a3418103b63a2b4aa01c31af15fdf6a2940398ffc0a408573c34c2304f928895adff + languageName: node + linkType: hard + "globby@npm:^11.0.0, globby@npm:^11.0.1, globby@npm:^11.0.3": version: 11.1.0 resolution: "globby@npm:11.1.0" @@ -8783,28 +8878,28 @@ __metadata: languageName: node linkType: hard -"marked-terminal@npm:^5.0.0": - version: 5.1.1 - resolution: "marked-terminal@npm:5.1.1" +"marked-terminal@npm:^4.1.1": + version: 4.2.0 + resolution: "marked-terminal@npm:4.2.0" dependencies: - ansi-escapes: ^5.0.0 + ansi-escapes: ^4.3.1 cardinal: ^2.1.1 - chalk: ^5.0.0 - cli-table3: ^0.6.1 - node-emoji: ^1.11.0 - supports-hyperlinks: ^2.2.0 + chalk: ^4.1.0 + cli-table3: ^0.6.0 + node-emoji: ^1.10.0 + supports-hyperlinks: ^2.1.0 peerDependencies: - marked: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - checksum: 24ceb02ebd10e9c6c2fac2240a2cc019093c95029732779ea41ba7a81c45867e956d1f6f1ae7426d5247ab5185b9cdaea31a9663e4d624c17335660fa9474c3d + marked: ^1.0.0 || ^2.0.0 + checksum: a68a4cfd22b42f990a82e3234c68006ab4d1285a4a9bdd162f597740d9a55275c10c78ca21fa3927a76b2197589fe382e33af9baa2ccb2153812986c15aa73b8 languageName: node linkType: hard -"marked@npm:^4.0.10": - version: 4.0.12 - resolution: "marked@npm:4.0.12" +"marked@npm:^2.0.0": + version: 2.1.3 + resolution: "marked@npm:2.1.3" bin: - marked: bin/marked.js - checksum: 7575117f85a8986652f3ac8b8a7b95056c4c5fce01a1fc76dc4c7960412cb4c9bd9da8133487159b6b3ff84f52b543dfe9a36f826a5f358892b5ec4b6824f192 + marked: bin/marked + checksum: 21a5ecd4941bc760aba21dfd97185853ec3b464cf707ad971e3ddb3aeb2f44d0deeb36b0889932afdb6f734975a994d92f18815dd0fabadbd902bdaff997cc5b languageName: node linkType: hard @@ -9242,9 +9337,9 @@ __metadata: languageName: node linkType: hard -"nimma@npm:0.1.7": - version: 0.1.7 - resolution: "nimma@npm:0.1.7" +"nimma@npm:0.1.8": + version: 0.1.8 + resolution: "nimma@npm:0.1.8" dependencies: "@jsep-plugin/regex": ^1.0.1 "@jsep-plugin/ternary": ^1.0.2 @@ -9257,7 +9352,7 @@ __metadata: optional: true lodash.topath: optional: true - checksum: 6dcf4aa3dd35c714142f04cfde76727202e1aea802cde0584ef35eabf45d217a9352c55e65045ff91956bb348b3b07ede4ca13af5a6bc8e7ce2f3b2ca353f9bd + checksum: 0e2f832b20f7c4cc4497c82fef1bd1fee6512db5251eb59223445f9ea7749c3baa981389ef5008b46efd0aa4e11472b62e81118b5933aeafd6f234d87dd49df7 languageName: node linkType: hard @@ -9282,7 +9377,7 @@ __metadata: languageName: node linkType: hard -"node-emoji@npm:^1.11.0": +"node-emoji@npm:^1.10.0": version: 1.11.0 resolution: "node-emoji@npm:1.11.0" dependencies: @@ -9571,9 +9666,9 @@ __metadata: languageName: node linkType: hard -"npm@npm:^8.3.0": - version: 8.4.0 - resolution: "npm@npm:8.4.0" +"npm@npm:^7.0.0": + version: 7.24.2 + resolution: "npm@npm:7.24.2" dependencies: "@isaacs/string-locale-compare": "*" "@npmcli/arborist": "*" @@ -9630,7 +9725,6 @@ __metadata: opener: "*" pacote: "*" parse-conflict-json: "*" - proc-log: "*" qrcode-terminal: "*" read: "*" read-package-json: "*" @@ -9649,7 +9743,7 @@ __metadata: bin: npm: bin/npm-cli.js npx: bin/npx-cli.js - checksum: eb2b78dec31016441adbbf2c708569b99f24bbc004449de0d95c43b0b664a28cf95b368716ac7841c45b3454e2b3772b81c620eb742a42a73851080fab3a4101 + checksum: 8d818fd4f8394a24147d1b5ec8395f96c443fea18c54238ab2e842b8d86d977da98d0ab124744161d2bc7a5b8edbc21b6c0c1117e76e68d2c5ee24c8a4f39381 languageName: node linkType: hard @@ -10404,7 +10498,14 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:*, proc-log@npm:^1.0.0": +"printable-characters@npm:^1.0.42": + version: 1.0.42 + resolution: "printable-characters@npm:1.0.42" + checksum: 2724aa02919d7085933af0f8f904bd0de67a6b53834f2e5b98fc7aa3650e20755c805e8c85bcf96c09f678cb16a58b55640dd3a2da843195fce06b1ccb0c8ce4 + languageName: node + linkType: hard + +"proc-log@npm:^1.0.0": version: 1.0.0 resolution: "proc-log@npm:1.0.0" checksum: 249605d5b28bfa0499d70da24ab056ad1e082a301f0a46d0ace6e8049cf16aaa0e71d9ea5cab29b620ffb327c18af97f0e012d1db090673447e7c1d33239dd96 @@ -11069,9 +11170,9 @@ __metadata: languageName: node linkType: hard -"rollup@npm:~2.60.2": - version: 2.60.2 - resolution: "rollup@npm:2.60.2" +"rollup@npm:~2.67.0": + version: 2.67.2 + resolution: "rollup@npm:2.67.2" dependencies: fsevents: ~2.3.2 dependenciesMeta: @@ -11079,7 +11180,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: bcd41dfe8afb7e0d97ce2237752165bdda689bcce6321d96821d565de3e0c865a49b544923f315985be2bfde086f72b54aae4ae7c87f798b3cb9558a5bec4e65 + checksum: 9aca5251ba4b441064183cde2394b91567259002d68086bdd3906db66d55dd148ab27e57c51eb53830d7b9b813c2d4e834b7735d65e2a869780bc639d4a20c38 languageName: node linkType: hard @@ -11125,7 +11226,7 @@ __metadata: node-powershell: ^4.0.0 patch-package: ^6.4.7 prettier: ^2.4.1 - semantic-release: ^19.0.2 + semantic-release: ^18.0.1 semantic-release-monorepo: ^7.0.5 ts-jest: ^27.0.7 ts-node: ^10.4.0 @@ -11219,14 +11320,14 @@ __metadata: languageName: node linkType: hard -"semantic-release@npm:^19.0.2": - version: 19.0.2 - resolution: "semantic-release@npm:19.0.2" +"semantic-release@npm:^18.0.1": + version: 18.0.1 + resolution: "semantic-release@npm:18.0.1" dependencies: "@semantic-release/commit-analyzer": ^9.0.2 "@semantic-release/error": ^3.0.0 "@semantic-release/github": ^8.0.0 - "@semantic-release/npm": ^9.0.0 + "@semantic-release/npm": ^8.0.0 "@semantic-release/release-notes-generator": ^10.0.0 aggregate-error: ^3.0.0 cosmiconfig: ^7.0.0 @@ -11240,8 +11341,8 @@ __metadata: hook-std: ^2.0.0 hosted-git-info: ^4.0.0 lodash: ^4.17.21 - marked: ^4.0.10 - marked-terminal: ^5.0.0 + marked: ^2.0.0 + marked-terminal: ^4.1.1 micromatch: ^4.0.2 p-each-series: ^2.1.0 p-reduce: ^2.0.0 @@ -11253,7 +11354,7 @@ __metadata: yargs: ^16.2.0 bin: semantic-release: bin/semantic-release.js - checksum: 0807cae8c57445793d3181a15cd587950aaf6b9c6ea9f4b7876b85a4ac78d1cd8d53f309512fe53eca2a8ed48600dd4d5483ac403bb42bfcf1c88a2c2340cf65 + checksum: e99634d2fd392d007cd83cc28318cd4b0781825b550e75486676941b8f67a32c1b907c53de2761440b38ead220629cc3778c22373aacce4ee291dba43971b0d6 languageName: node linkType: hard @@ -11696,6 +11797,16 @@ __metadata: languageName: node linkType: hard +"stacktracey@npm:^2.1.7": + version: 2.1.8 + resolution: "stacktracey@npm:2.1.8" + dependencies: + as-table: ^1.0.36 + get-source: ^2.0.12 + checksum: abd8316b4e120884108f5a47b2f61abdcaeaa118afd95f3c48317cb057fff43d697450ba00de3f9fe7fee61ee72644ccda4db990a8e4553706644f7c17522eab + languageName: node + linkType: hard + "statuses@npm:>= 1.5.0 < 2, statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" @@ -11953,7 +12064,7 @@ __metadata: languageName: node linkType: hard -"supports-hyperlinks@npm:^2.0.0, supports-hyperlinks@npm:^2.2.0": +"supports-hyperlinks@npm:^2.0.0, supports-hyperlinks@npm:^2.1.0": version: 2.2.0 resolution: "supports-hyperlinks@npm:2.2.0" dependencies: @@ -12482,13 +12593,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^1.0.2": - version: 1.4.0 - resolution: "type-fest@npm:1.4.0" - checksum: b011c3388665b097ae6a109a437a04d6f61d81b7357f74cbcb02246f2f5bd72b888ae33631b99871388122ba0a87f4ff1c94078e7119ff22c70e52c0ff828201 - languageName: node - linkType: hard - "type-is@npm:~1.6.17": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -12787,12 +12891,15 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.3": - version: 3.9.5 - resolution: "vm2@npm:3.9.5" +"vm2@npm:^3.9.8": + version: 3.9.8 + resolution: "vm2@npm:3.9.8" + dependencies: + acorn: ^8.7.0 + acorn-walk: ^8.2.0 bin: vm2: bin/vm2 - checksum: d83dbe929ca4d1c9fca71cda34a5aee9a6b4bdc1de1ddb11777c4f6e1e48a471764195258dbf608f962df1a1c3d6ae917c9755f11a8f37b9e0bbf691313a725c + checksum: 1e665a45ce76612922368462a8b98876698e866c1f201393b0b646f07a00449dc4170e987152cf1443af664ca8f2b82bb52a0760456a71912fa63f22980de7b4 languageName: node linkType: hard