mirror of
https://github.com/LukeHagar/redocly-cli.git
synced 2025-12-06 04:21:09 +00:00
feat: add support for a plugin interface common with Realm (#1661)
This commit is contained in:
committed by
GitHub
parent
9ce88a33a5
commit
7a0e52f57e
6
.changeset/famous-socks-rule.md
Normal file
6
.changeset/famous-socks-rule.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@redocly/openapi-core": minor
|
||||||
|
"@redocly/cli": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added support for ESM plugins and importing of plugins directly from npm package: `@vendor/package/plugin.js` instead of `./node_modules/@vendor/package/plugin.js`.
|
||||||
6
.changeset/tough-gifts-tell.md
Normal file
6
.changeset/tough-gifts-tell.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@redocly/openapi-core": minor
|
||||||
|
"@redocly/cli": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Changed plugins format to export a function instead of an object for compatibility with other Redocly products. The backwards compatibility with an old format of plugins is maintained but will be removed in the future.
|
||||||
@@ -18,6 +18,8 @@ info:
|
|||||||
name: test 2
|
name: test 2
|
||||||
components: {}
|
components: {}
|
||||||
|
|
||||||
|
Deprecated plugin format detected: plugin
|
||||||
|
Deprecated plugin format detected: plugin
|
||||||
bundling ./openapi.yaml...
|
bundling ./openapi.yaml...
|
||||||
📦 Created a bundle for ./openapi.yaml at stdout <test>ms.
|
📦 Created a bundle for ./openapi.yaml at stdout <test>ms.
|
||||||
|
|
||||||
|
|||||||
@@ -6,22 +6,24 @@ const XMetaData = {
|
|||||||
required: ['lifecycle'],
|
required: ['lifecycle'],
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function typeExtensionPlugin() {
|
||||||
id: 'type-extension',
|
return {
|
||||||
typeExtension: {
|
id: 'type-extension',
|
||||||
oas3(types) {
|
typeExtension: {
|
||||||
newTypes = {
|
oas3(types) {
|
||||||
...types,
|
newTypes = {
|
||||||
XMetaData: XMetaData,
|
...types,
|
||||||
Info: {
|
XMetaData: XMetaData,
|
||||||
...types.Info,
|
Info: {
|
||||||
properties: {
|
...types.Info,
|
||||||
...types.Info.properties,
|
properties: {
|
||||||
'x-metadata': 'XMetaData',
|
...types.Info.properties,
|
||||||
|
'x-metadata': 'XMetaData',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
return newTypes;
|
||||||
return newTypes;
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
exports[`E2E check-config wrong config type extension in assertions 1`] = `
|
exports[`E2E check-config wrong config type extension in assertions 1`] = `
|
||||||
|
|
||||||
|
Deprecated plugin format detected: type-extension
|
||||||
[1] redocly.yaml:10:13 at #/rules/rule~1metadata-lifecycle/subject/type
|
[1] redocly.yaml:10:13 at #/rules/rule~1metadata-lifecycle/subject/type
|
||||||
|
|
||||||
\`type\` can be one of the following only: "any", "Root", "Tag", "TagList", "TagGroups", "TagGroup", "ExternalDocs", "Example", "ExamplesMap", "EnumDescriptions", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Logo", "Paths", "PathItem", "Parameter", "ParameterItems", "ParameterList", "Operation", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "XCodeSample", "XCodeSampleList", "XServerList", "XServer", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "DiscriminatorMapping", "Discriminator", "Components", "LinksMap", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "XMetaData", "PatternProperties", "NamedPathItems", "DependentRequired", "HttpServerBinding", "HttpChannelBinding", "HttpMessageBinding", "HttpOperationBinding", "WsServerBinding", "WsChannelBinding", "WsMessageBinding", "WsOperationBinding", "KafkaServerBinding", "KafkaTopicConfiguration", "KafkaChannelBinding", "KafkaMessageBinding", "KafkaOperationBinding", "AnypointmqServerBinding", "AnypointmqChannelBinding", "AnypointmqMessageBinding", "AnypointmqOperationBinding", "AmqpServerBinding", "AmqpChannelBinding", "AmqpMessageBinding", "AmqpOperationBinding", "Amqp1ServerBinding", "Amqp1ChannelBinding", "Amqp1MessageBinding", "Amqp1OperationBinding", "MqttServerBindingLastWill", "MqttServerBinding", "MqttChannelBinding", "MqttMessageBinding", "MqttOperationBinding", "Mqtt5ServerBinding", "Mqtt5ChannelBinding", "Mqtt5MessageBinding", "Mqtt5OperationBinding", "NatsServerBinding", "NatsChannelBinding", "NatsMessageBinding", "NatsOperationBinding", "JmsServerBinding", "JmsChannelBinding", "JmsMessageBinding", "JmsOperationBinding", "SolaceServerBinding", "SolaceChannelBinding", "SolaceMessageBinding", "SolaceDestination", "SolaceOperationBinding", "StompServerBinding", "StompChannelBinding", "StompMessageBinding", "StompOperationBinding", "RedisServerBinding", "RedisChannelBinding", "RedisMessageBinding", "RedisOperationBinding", "MercureServerBinding", "MercureChannelBinding", "MercureMessageBinding", "MercureOperationBinding", "ServerBindings", "ChannelBindings", "MessageBindings", "OperationBindings", "ServerMap", "ChannelMap", "Channel", "ParametersMap", "MessageExample", "NamedMessages", "NamedMessageTraits", "NamedOperationTraits", "NamedCorrelationIds", "SecuritySchemeFlows", "Message", "OperationTrait", "OperationTraitList", "MessageTrait", "MessageTraitList", "MessageExampleList", "CorrelationId", "Dependencies", "OperationReply", "OperationReplyAddress", "NamedTags", "NamedExternalDocs", "NamedChannels", "NamedOperations", "NamedOperationReplies", "NamedOperationRelyAddresses", "SecuritySchemeList", "MessageList", "SourceDescriptions", "OpenAPISourceDescription", "NoneSourceDescription", "ArazzoSourceDescription", "Parameters", "ReusableObject", "Workflows", "Workflow", "Steps", "Step", "Replacement", "ExtendedOperation", "ExpectSchema", "Outputs", "CriterionObject", "XPathCriterion", "JSONPathCriterion", "SuccessActionObject", "OnSuccessActionList", "FailureActionObject", "OnFailureActionList", "NamedInputs", "NamedSuccessActions", "NamedFailureActions", "SpecExtension".
|
\`type\` can be one of the following only: "any", "Root", "Tag", "TagList", "TagGroups", "TagGroup", "ExternalDocs", "Example", "ExamplesMap", "EnumDescriptions", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Logo", "Paths", "PathItem", "Parameter", "ParameterItems", "ParameterList", "Operation", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "XCodeSample", "XCodeSampleList", "XServerList", "XServer", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "DiscriminatorMapping", "Discriminator", "Components", "LinksMap", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "XMetaData", "PatternProperties", "NamedPathItems", "DependentRequired", "HttpServerBinding", "HttpChannelBinding", "HttpMessageBinding", "HttpOperationBinding", "WsServerBinding", "WsChannelBinding", "WsMessageBinding", "WsOperationBinding", "KafkaServerBinding", "KafkaTopicConfiguration", "KafkaChannelBinding", "KafkaMessageBinding", "KafkaOperationBinding", "AnypointmqServerBinding", "AnypointmqChannelBinding", "AnypointmqMessageBinding", "AnypointmqOperationBinding", "AmqpServerBinding", "AmqpChannelBinding", "AmqpMessageBinding", "AmqpOperationBinding", "Amqp1ServerBinding", "Amqp1ChannelBinding", "Amqp1MessageBinding", "Amqp1OperationBinding", "MqttServerBindingLastWill", "MqttServerBinding", "MqttChannelBinding", "MqttMessageBinding", "MqttOperationBinding", "Mqtt5ServerBinding", "Mqtt5ChannelBinding", "Mqtt5MessageBinding", "Mqtt5OperationBinding", "NatsServerBinding", "NatsChannelBinding", "NatsMessageBinding", "NatsOperationBinding", "JmsServerBinding", "JmsChannelBinding", "JmsMessageBinding", "JmsOperationBinding", "SolaceServerBinding", "SolaceChannelBinding", "SolaceMessageBinding", "SolaceDestination", "SolaceOperationBinding", "StompServerBinding", "StompChannelBinding", "StompMessageBinding", "StompOperationBinding", "RedisServerBinding", "RedisChannelBinding", "RedisMessageBinding", "RedisOperationBinding", "MercureServerBinding", "MercureChannelBinding", "MercureMessageBinding", "MercureOperationBinding", "ServerBindings", "ChannelBindings", "MessageBindings", "OperationBindings", "ServerMap", "ChannelMap", "Channel", "ParametersMap", "MessageExample", "NamedMessages", "NamedMessageTraits", "NamedOperationTraits", "NamedCorrelationIds", "SecuritySchemeFlows", "Message", "OperationTrait", "OperationTraitList", "MessageTrait", "MessageTraitList", "MessageExampleList", "CorrelationId", "Dependencies", "OperationReply", "OperationReplyAddress", "NamedTags", "NamedExternalDocs", "NamedChannels", "NamedOperations", "NamedOperationReplies", "NamedOperationRelyAddresses", "SecuritySchemeList", "MessageList", "SourceDescriptions", "OpenAPISourceDescription", "NoneSourceDescription", "ArazzoSourceDescription", "Parameters", "ReusableObject", "Workflows", "Workflow", "Steps", "Step", "Replacement", "ExtendedOperation", "ExpectSchema", "Outputs", "CriterionObject", "XPathCriterion", "JSONPathCriterion", "SuccessActionObject", "OnSuccessActionList", "FailureActionObject", "OnFailureActionList", "NamedInputs", "NamedSuccessActions", "NamedFailureActions", "SpecExtension".
|
||||||
|
|||||||
@@ -558,6 +558,26 @@ describe('E2E', () => {
|
|||||||
const result = getCommandOutput(args, testPath);
|
const result = getCommandOutput(args, testPath);
|
||||||
(<any>expect(cleanupOutput(result))).toMatchSpecificSnapshot(join(testPath, 'snapshot.js'));
|
(<any>expect(cleanupOutput(result))).toMatchSpecificSnapshot(join(testPath, 'snapshot.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('lint with a rule from a plugin', () => {
|
||||||
|
const testPath = join(folderPath, 'resolve-plugins');
|
||||||
|
const args = getParams('../../../packages/cli/src/index.ts', 'lint', [
|
||||||
|
'openapi.yaml',
|
||||||
|
'--config=plugin-config.yaml',
|
||||||
|
]);
|
||||||
|
const result = getCommandOutput(args, testPath);
|
||||||
|
(<any>expect(cleanupOutput(result))).toMatchSpecificSnapshot(join(testPath, 'snapshot.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decorate with a decorator from a plugin', () => {
|
||||||
|
const testPath = join(folderPath, 'resolve-plugins');
|
||||||
|
const args = getParams('../../../packages/cli/src/index.ts', 'bundle', [
|
||||||
|
'openapi.yaml',
|
||||||
|
'--config=plugin-config.yaml',
|
||||||
|
]);
|
||||||
|
const result = getCommandOutput(args, testPath);
|
||||||
|
(<any>expect(cleanupOutput(result))).toMatchSpecificSnapshot(join(testPath, 'snapshot.js'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('build-docs', () => {
|
describe('build-docs', () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
exports[`E2E lint arazzo-type-extensions-with-plugin 1`] = `
|
exports[`E2E lint arazzo-type-extensions-with-plugin 1`] = `
|
||||||
|
|
||||||
|
Deprecated plugin format detected: type-extension
|
||||||
validating museum.yaml...
|
validating museum.yaml...
|
||||||
[1] museum.yaml:8:3 at #/info/wrong-key
|
[1] museum.yaml:8:3 at #/info/wrong-key
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
id: 'local',
|
|
||||||
assertions: {
|
|
||||||
checkSchema: (_, opts, ctx) => {
|
|
||||||
const name = opts.required;
|
|
||||||
|
|
||||||
const rawValue = ctx.rawValue;
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (Array.isArray(rawValue?.required) && rawValue?.properties) {
|
|
||||||
const required = rawValue?.required;
|
|
||||||
const properties = rawValue?.properties;
|
|
||||||
|
|
||||||
for (const item of required) {
|
|
||||||
if (properties[item] && properties[item].type === 'string') {
|
|
||||||
if (!properties[item][name]) {
|
|
||||||
errors.push({
|
|
||||||
message: `Required property ${name} inside ${item} string property`,
|
|
||||||
location: ctx.baseLocation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
export default function plugin() {
|
||||||
|
return {
|
||||||
|
id: 'local',
|
||||||
|
assertions: {
|
||||||
|
checkSchema: (_, opts, ctx) => {
|
||||||
|
const name = opts.required;
|
||||||
|
|
||||||
|
const rawValue = ctx.rawValue;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (Array.isArray(rawValue?.required) && rawValue?.properties) {
|
||||||
|
const required = rawValue?.required;
|
||||||
|
const properties = rawValue?.properties;
|
||||||
|
|
||||||
|
for (const item of required) {
|
||||||
|
if (properties[item] && properties[item].type === 'string') {
|
||||||
|
if (!properties[item][name]) {
|
||||||
|
errors.push({
|
||||||
|
message: `Required property ${name} inside ${item} string property`,
|
||||||
|
location: ctx.baseLocation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ apis:
|
|||||||
root: ./openapi.yaml
|
root: ./openapi.yaml
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- ./plugin.js
|
- ./plugin.mjs
|
||||||
rules:
|
rules:
|
||||||
rule/minLength:
|
rule/minLength:
|
||||||
subject:
|
subject:
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
id: 'local',
|
|
||||||
assertions: {
|
|
||||||
checkWordsStarts: (value, opts, ctx) => {
|
|
||||||
const regexp = new RegExp(`^${opts.words.join('|')}`);
|
|
||||||
if (regexp.test(value)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
message: `Should start with one of ${opts.words.join(', ')}`,
|
|
||||||
location: ctx.baseLocation,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
checkWordsCount: (value, opts, ctx) => {
|
|
||||||
const words = value.split(' ');
|
|
||||||
if (words.length >= opts.min) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
message: `Should have at least ${opts.min} words`,
|
|
||||||
location: ctx.baseLocation,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
31
__tests__/lint/assertions-custom-function-options/plugin.mjs
Normal file
31
__tests__/lint/assertions-custom-function-options/plugin.mjs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export default async function plugin() {
|
||||||
|
return {
|
||||||
|
id: 'local',
|
||||||
|
assertions: {
|
||||||
|
checkWordsStarts: (value, opts, ctx) => {
|
||||||
|
const regexp = new RegExp(`^${opts.words.join('|')}`);
|
||||||
|
if (regexp.test(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
message: `Should start with one of ${opts.words.join(', ')}`,
|
||||||
|
location: ctx.baseLocation,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
checkWordsCount: (value, opts, ctx) => {
|
||||||
|
const words = value.split(' ');
|
||||||
|
if (words.length >= opts.min) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
message: `Should have at least ${opts.min} words`,
|
||||||
|
location: ctx.baseLocation,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ apis:
|
|||||||
main:
|
main:
|
||||||
root: ./openapi.yaml
|
root: ./openapi.yaml
|
||||||
plugins:
|
plugins:
|
||||||
- ./plugin.js
|
- ./plugin.mjs
|
||||||
rules:
|
rules:
|
||||||
rule/operation-summary-length-custom:
|
rule/operation-summary-length-custom:
|
||||||
subject:
|
subject:
|
||||||
|
|||||||
9
__tests__/miscellaneous/resolve-plugins/openapi.yaml
Normal file
9
__tests__/miscellaneous/resolve-plugins/openapi.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Test
|
||||||
|
paths: {}
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
OpenID:
|
||||||
|
type: openIdConnect
|
||||||
|
openIdConnectUrl: https://example.com/missing-well-known-configuration
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
plugins:
|
||||||
|
- '../../../packages/core/src/config/__tests__/fixtures/plugin.js'
|
||||||
|
decorators:
|
||||||
|
test-plugin/inject-x-stats: on
|
||||||
|
rules:
|
||||||
|
test-plugin/openid-connect-url-well-known: error
|
||||||
45
__tests__/miscellaneous/resolve-plugins/snapshot.js
Normal file
45
__tests__/miscellaneous/resolve-plugins/snapshot.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`E2E miscellaneous decorate with a decorator from a plugin 1`] = `
|
||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Test
|
||||||
|
x-stats:
|
||||||
|
test: 1
|
||||||
|
paths: {}
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
OpenID:
|
||||||
|
type: openIdConnect
|
||||||
|
openIdConnectUrl: https://example.com/missing-well-known-configuration
|
||||||
|
|
||||||
|
Deprecated plugin format detected: test-plugin
|
||||||
|
bundling openapi.yaml...
|
||||||
|
📦 Created a bundle for openapi.yaml at stdout <test>ms.
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`E2E miscellaneous lint with a rule from a plugin 1`] = `
|
||||||
|
|
||||||
|
Deprecated plugin format detected: test-plugin
|
||||||
|
validating openapi.yaml...
|
||||||
|
[1] openapi.yaml:9:25 at #/components/securitySchemes/OpenID/openIdConnectUrl
|
||||||
|
|
||||||
|
openIdConnectUrl must be a URL that ends with /.well-known/openid-configuration
|
||||||
|
|
||||||
|
7 | OpenID:
|
||||||
|
8 | type: openIdConnect
|
||||||
|
9 | openIdConnectUrl: https://example.com/missing-well-known-configuration
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
10 |
|
||||||
|
|
||||||
|
Error was generated by the test-plugin/openid-connect-url-well-known rule.
|
||||||
|
|
||||||
|
|
||||||
|
openapi.yaml: validated in <test>ms
|
||||||
|
|
||||||
|
❌ Validation failed with 1 error.
|
||||||
|
run \`redocly lint --generate-ignore-file\` to add all problems to the ignore file.
|
||||||
|
|
||||||
|
|
||||||
|
`;
|
||||||
@@ -17,7 +17,9 @@ const preprocessors = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function plugin() {
|
||||||
id: 'plugin',
|
return {
|
||||||
preprocessors,
|
id: 'plugin',
|
||||||
|
preprocessors,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,22 +9,24 @@ configuration file.
|
|||||||
The following is an example plugin, defining two configuration bundles:
|
The following is an example plugin, defining two configuration bundles:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
module.exports = {
|
module.exports = function myLocalPlugin() {
|
||||||
id: 'my-local-plugin'
|
return {
|
||||||
configs: {
|
id: 'my-local-plugin',
|
||||||
all: {
|
configs: {
|
||||||
rules: {
|
all: {
|
||||||
'operation-id-not-test': 'error',
|
rules: {
|
||||||
'boolean-parameter-prefixes': 'error',
|
'operation-id-not-test': 'error',
|
||||||
|
'boolean-parameter-prefixes': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minimal: {
|
||||||
|
rules: {
|
||||||
|
'operation-id-not-test': 'off',
|
||||||
|
'boolean-parameter-prefixes': 'error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
minimal: {
|
};
|
||||||
rules: {
|
|
||||||
'operation-id-not-test': 'off',
|
|
||||||
'boolean-parameter-prefixes': 'error',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,26 +13,29 @@ Decorators and preprocessors are the same in structure, but preprocessors are ru
|
|||||||
|
|
||||||
## Plugin structure
|
## Plugin structure
|
||||||
|
|
||||||
To create a preprocessor or decorator, the object that is exported from your module has to conform to an interface such as the following example:
|
To create a preprocessor or decorator, the function that is exported from your module has to conform to an interface such as the following example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
module.exports = {
|
module.exports = function myLocalPlugin() {
|
||||||
id: 'my-local-plugin',
|
return {
|
||||||
preprocessors: {
|
id: 'my-local-plugin',
|
||||||
oas3: {
|
preprocessors: {
|
||||||
"processor-id": () => {
|
oas3: {
|
||||||
// ...
|
'processor-id': () => {
|
||||||
}
|
// ...
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
decorators: {
|
},
|
||||||
oas3: {
|
decorators: {
|
||||||
"decorator-id": () => {
|
oas3: {
|
||||||
// ...
|
'decorator-id': () => {
|
||||||
}
|
// ...
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Each decorator or preprocessor is a function that returns an object. The object's keys are the node types in the document, and each of those can contain any or all of the `enter()`, `leave()` and `skip()` functions for that node type. Find more information and examples on the [visitor pattern page](./visitor.md).
|
Each decorator or preprocessor is a function that returns an object. The object's keys are the node types in the document, and each of those can contain any or all of the `enter()`, `leave()` and `skip()` functions for that node type. Find more information and examples on the [visitor pattern page](./visitor.md).
|
||||||
@@ -67,14 +70,16 @@ To use this decorator, add it to a plugin. In this example the main decorator fi
|
|||||||
```js
|
```js
|
||||||
const OperationSparkle = require('./decorators/operation-sparkle.js');
|
const OperationSparkle = require('./decorators/operation-sparkle.js');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function sparklePlugin() {
|
||||||
id: 'sparkle',
|
return {
|
||||||
decorators: {
|
id: "sparkle",
|
||||||
oas3: {
|
decorators: {
|
||||||
'operation-sparkle': OperationSparkle,
|
oas3: {
|
||||||
}
|
"operation-sparkle": OperationSparkle,
|
||||||
}
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The plugin is good to go. For a user to include it in their Redocly configuration, edit the configuration file to look something like this:
|
The plugin is good to go. For a user to include it in their Redocly configuration, edit the configuration file to look something like this:
|
||||||
@@ -118,15 +123,17 @@ Now extend the decorator from the previous example to add this to the existing p
|
|||||||
const OperationSparkle = require('./decorators/operation-sparkle.js');
|
const OperationSparkle = require('./decorators/operation-sparkle.js');
|
||||||
const OpIdSuffix = require('./decorators/add-suffix.js');
|
const OpIdSuffix = require('./decorators/add-suffix.js');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function sparklePlugin() {
|
||||||
id: 'sparkle',
|
return {
|
||||||
decorators: {
|
id: "sparkle",
|
||||||
oas3: {
|
decorators: {
|
||||||
'operation-sparkle': OperationSparkle,
|
oas3: {
|
||||||
'add-opid-suffix': OpIdSuffix,
|
"operation-sparkle": OperationSparkle,
|
||||||
}
|
"add-opid-suffix": OpIdSuffix,
|
||||||
}
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
All that remains is for a user to configure this decorator in their `redocly.yaml` configuration file to take advantage of the new decorator functionality. Here's an example of the configuration file:
|
All that remains is for a user to configure this decorator in their `redocly.yaml` configuration file to take advantage of the new decorator functionality. Here's an example of the configuration file:
|
||||||
|
|||||||
@@ -35,18 +35,21 @@ function OperationIdNotTest() {
|
|||||||
|
|
||||||
The `ctx` object here holds all the context, which can be used to give more situation-aware functionality to the rules you build. This is one of the main use cases for custom rules. The `report()` method is used to give information to return to the user if the node being visited doesn't comply with the rule. You can read the [context](#the-context-object) and [location](#location-object) sections for more information.
|
The `ctx` object here holds all the context, which can be used to give more situation-aware functionality to the rules you build. This is one of the main use cases for custom rules. The `report()` method is used to give information to return to the user if the node being visited doesn't comply with the rule. You can read the [context](#the-context-object) and [location](#location-object) sections for more information.
|
||||||
|
|
||||||
Adding this as part of a plugin requires you to add it to the `rules` part of the plugin object, under the relevant document type. The example rule here is intended to be used with OpenAPI, so the plugin code in `plugins/my-rules.js` is as follows:
|
Adding this as part of a plugin requires you to add it to the `rules` part of the plugin object returned by the exported function, under the relevant document type.
|
||||||
|
The example rule here is intended to be used with OpenAPI, so the plugin code in `plugins/my-rules.js` is as follows:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const OperationIdNotTest = require('./rules/opid-not-test.js');
|
const OperationIdNotTest = require('./rules/opid-not-test.js');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function myRulesPlugin() {
|
||||||
id: 'my-rules',
|
return {
|
||||||
rules: {
|
id: 'my-rules',
|
||||||
oas3: {
|
rules: {
|
||||||
'opid-not-test': OperationIdNotTest,
|
oas3: {
|
||||||
}
|
'opid-not-test': OperationIdNotTest,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -38,23 +38,25 @@ Note the quotes around the `owner-team` key since it contains a hyphen `-`. Thes
|
|||||||
To include the new type in the type tree, the plugin must add the type and modify the parent type, which in this example is `info`. This is done by returning a `typeExtension` structure, as shown in the example below (this example is in `plugins/example-type-extension.js`, this filename is used again in the configuration example later):
|
To include the new type in the type tree, the plugin must add the type and modify the parent type, which in this example is `info`. This is done by returning a `typeExtension` structure, as shown in the example below (this example is in `plugins/example-type-extension.js`, this filename is used again in the configuration example later):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
module.exports = {
|
module.exports = function typeExtensionsPlugin() {
|
||||||
id: 'example-type-extension',
|
return {
|
||||||
typeExtension: {
|
id: 'example-type-extension',
|
||||||
oas3(types) {
|
typeExtension: {
|
||||||
return {
|
oas3(types) {
|
||||||
...types,
|
return {
|
||||||
XMetaData: XMetaData,
|
...types,
|
||||||
Info: {
|
XMetaData: XMetaData,
|
||||||
...types.Info,
|
Info: {
|
||||||
properties: {
|
...types.Info,
|
||||||
...types.Info.properties,
|
properties: {
|
||||||
'x-metadata': 'XMetaData',
|
...types.Info.properties,
|
||||||
}
|
'x-metadata': 'XMetaData',
|
||||||
}
|
},
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,13 @@ The paths are relative to the configuration file location. Where there are multi
|
|||||||
|
|
||||||
### Plugin structure
|
### Plugin structure
|
||||||
|
|
||||||
The minimal plugin should export an `id` string that is used to refer to the contents of the plugin in the `redocly.yaml` configuration file:
|
The minimal plugin should export a function that returns an object with a single property `id` that is used to refer to the contents of the plugin in the `redocly.yaml` configuration file:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
module.exports = {
|
module.exports = function myPlugin() {
|
||||||
id: 'my-local-plugin',
|
return {
|
||||||
|
id: 'my-local-plugin',
|
||||||
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ Estimated time: 15 minutes
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function changeTokenPlugin() {
|
||||||
id,
|
return {
|
||||||
decorators,
|
id,
|
||||||
|
decorators,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ In this step, create a custom plugin and define the decorator dependency.
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function hideExtensionsPlugin() {
|
||||||
id,
|
return {
|
||||||
decorators,
|
id,
|
||||||
|
decorators,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -84,9 +84,11 @@ const decorators = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = function replaceServersUrlPlugin() {
|
||||||
id,
|
return {
|
||||||
decorators,
|
id,
|
||||||
|
decorators,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -242,34 +242,36 @@ rule/operation-summary-check:
|
|||||||
`plugin.js`
|
`plugin.js`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
module.exports = {
|
module.exports = function localPlugin() {
|
||||||
id: 'local',
|
return {
|
||||||
assertions: {
|
id: 'local',
|
||||||
checkWordsStarts: (value, options, ctx) => {
|
assertions: {
|
||||||
const regexp = new RegExp(`^${options.words.join('|')}`);
|
checkWordsStarts: (value, options, ctx) => {
|
||||||
if (regexp.test(value)) {
|
const regexp = new RegExp(`^${options.words.join('|')}`);
|
||||||
return [];
|
if (regexp.test(value)) {
|
||||||
}
|
return [];
|
||||||
return [
|
}
|
||||||
{
|
return [
|
||||||
message: 'Operation summary should start with an active verb',
|
{
|
||||||
location: ctx.baseLocation,
|
message: 'Operation summary should start with an active verb',
|
||||||
},
|
location: ctx.baseLocation,
|
||||||
];
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
checkWordsCount: (value, options, ctx) => {
|
||||||
|
const words = value.split(' ');
|
||||||
|
if (words.length >= options.min) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
message: `Operation summary should contain at least ${options.min} words`,
|
||||||
|
location: ctx.baseLocation,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
checkWordsCount: (value, options, ctx) => {
|
};
|
||||||
const words = value.split(' ');
|
|
||||||
if (words.length >= options.min) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
message: `Operation summary should contain at least ${options.min} words`,
|
|
||||||
location: ctx.baseLocation,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
"engineStrict": true,
|
"engineStrict": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run typecheck && npm run compile && npm run unit && npm run e2e",
|
"test": "npm run typecheck && npm run compile && npm run unit && npm run e2e",
|
||||||
"jest": "REDOCLY_TELEMETRY=off jest ./packages",
|
"jest": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" REDOCLY_TELEMETRY=off jest ./packages",
|
||||||
"unit": "npm run jest -- --coverage --coverageReporters lcov text-summary",
|
"unit": "npm run jest -- --coverage --coverageReporters lcov text-summary",
|
||||||
"unit:watch": "REDOCLY_TELEMETRY=off jest --watch",
|
"unit:watch": "REDOCLY_TELEMETRY=off jest --watch",
|
||||||
"coverage:cli": "npm run jest -- --roots packages/cli/src --coverage",
|
"coverage:cli": "npm run jest -- --roots packages/cli/src --coverage",
|
||||||
"coverage:core": "npm run jest -- --roots packages/core/src --coverage",
|
"coverage:core": "npm run jest -- --roots packages/core/src --coverage",
|
||||||
"typecheck": "tsc --noEmit --skipLibCheck",
|
"typecheck": "tsc --noEmit --skipLibCheck",
|
||||||
"e2e": "npm run webpack-bundle -- --mode=none && REDOCLY_TELEMETRY=off jest --roots=./__tests__/",
|
"e2e": "npm run webpack-bundle -- --mode=none && REDOCLY_TELEMETRY=off NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --roots=./__tests__/",
|
||||||
"prettier": " npx prettier --write \"**/*.{ts,js,yaml,yml,json,md}\"",
|
"prettier": " npx prettier --write \"**/*.{ts,js,yaml,yml,json,md}\"",
|
||||||
"prettier:check": "npx prettier --check \"**/*.{ts,js,yaml,yml,json,md}\"",
|
"prettier:check": "npx prettier --check \"**/*.{ts,js,yaml,yml,json,md}\"",
|
||||||
"eslint": "eslint packages/**",
|
"eslint": "eslint packages/**",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const yamlSerializer = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function makeConfigForRuleset(
|
export async function makeConfigForRuleset(
|
||||||
rules: Oas3RuleSet,
|
rules: Oas3RuleSet,
|
||||||
plugin?: Partial<Plugin>,
|
plugin?: Partial<Plugin>,
|
||||||
version: string = 'oas3'
|
version: string = 'oas3'
|
||||||
@@ -55,7 +55,7 @@ export function makeConfigForRuleset(
|
|||||||
Object.keys(rules).forEach((name) => {
|
Object.keys(rules).forEach((name) => {
|
||||||
rulesConf[`${ruleId}/${name}`] = 'error';
|
rulesConf[`${ruleId}/${name}`] = 'error';
|
||||||
});
|
});
|
||||||
const plugins = resolvePlugins([
|
const plugins = await resolvePlugins([
|
||||||
{
|
{
|
||||||
...plugin,
|
...plugin,
|
||||||
id: ruleId,
|
id: ruleId,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(testRuleSet.test).toBeCalledTimes(1);
|
expect(testRuleSet.test).toBeCalledTimes(1);
|
||||||
@@ -100,12 +100,12 @@ describe('walk order', () => {
|
|||||||
const document = parseYamlToDocument(
|
const document = parseYamlToDocument(
|
||||||
outdent`
|
outdent`
|
||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
servers:
|
servers:
|
||||||
- url: http://{test}.url
|
- url: http://{test}.url
|
||||||
variables:
|
variables:
|
||||||
test: test
|
test: test
|
||||||
paths:
|
paths:
|
||||||
/test-path:
|
/test-path:
|
||||||
get:
|
get:
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
@@ -121,7 +121,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(testRuleSet.test).toBeCalledTimes(1);
|
expect(testRuleSet.test).toBeCalledTimes(1);
|
||||||
@@ -184,7 +184,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -264,7 +264,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet, undefined, 'oas2'),
|
config: await makeConfigForRuleset(testRuleSet, undefined, 'oas2'),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -345,7 +345,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -417,7 +417,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -489,7 +489,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -556,7 +556,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -610,7 +610,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -666,7 +666,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -708,7 +708,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -763,7 +763,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -849,7 +849,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -928,7 +928,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -1000,7 +1000,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -1044,7 +1044,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -1109,7 +1109,7 @@ describe('walk order', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
@@ -1197,7 +1197,7 @@ describe('context.report', () => {
|
|||||||
const results = await lintDocument({
|
const results = await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(results).toHaveLength(3);
|
expect(results).toHaveLength(3);
|
||||||
@@ -1277,7 +1277,7 @@ describe('context.report', () => {
|
|||||||
const results = await lintDocument({
|
const results = await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(results).toHaveLength(4);
|
expect(results).toHaveLength(4);
|
||||||
@@ -1385,7 +1385,7 @@ describe('context.resolve', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1434,7 +1434,7 @@ describe('type extensions', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet, {
|
config: await makeConfigForRuleset(testRuleSet, {
|
||||||
typeExtension: {
|
typeExtension: {
|
||||||
oas3(types, version) {
|
oas3(types, version) {
|
||||||
expect(version).toEqual(oas);
|
expect(version).toEqual(oas);
|
||||||
@@ -1528,7 +1528,7 @@ describe('ignoreNextRules', () => {
|
|||||||
await lintDocument({
|
await lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
document,
|
document,
|
||||||
config: makeConfigForRuleset(testRuleSet),
|
config: await makeConfigForRuleset(testRuleSet),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(calls).toMatchInlineSnapshot(`
|
expect(calls).toMatchInlineSnapshot(`
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { lintDocument } from '../../lint';
|
|||||||
import { BaseResolver } from '../../resolve';
|
import { BaseResolver } from '../../resolve';
|
||||||
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
||||||
|
|
||||||
|
import type { StyleguideConfig } from '../../config';
|
||||||
|
|
||||||
export const name = 'Validate with 50 top-level rules';
|
export const name = 'Validate with 50 top-level rules';
|
||||||
export const count = 10;
|
export const count = 10;
|
||||||
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
||||||
@@ -25,7 +27,11 @@ for (let i = 0; i < 50; i++) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = makeConfigForRuleset(ruleset);
|
let config: StyleguideConfig;
|
||||||
|
export async function setupAsync() {
|
||||||
|
config = await makeConfigForRuleset(ruleset);
|
||||||
|
}
|
||||||
|
|
||||||
export function measureAsync() {
|
export function measureAsync() {
|
||||||
return lintDocument({
|
return lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { lintDocument } from '../../lint';
|
|||||||
import { BaseResolver } from '../../resolve';
|
import { BaseResolver } from '../../resolve';
|
||||||
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
||||||
|
|
||||||
|
import type { StyleguideConfig } from '../../config';
|
||||||
|
|
||||||
export const name = 'Validate with single nested rule';
|
export const name = 'Validate with single nested rule';
|
||||||
export const count = 10;
|
export const count = 10;
|
||||||
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
||||||
@@ -29,7 +31,11 @@ const visitor = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const config = makeConfigForRuleset(visitor);
|
let config: StyleguideConfig;
|
||||||
|
export async function setupAsync() {
|
||||||
|
config = await makeConfigForRuleset(visitor);
|
||||||
|
}
|
||||||
|
|
||||||
export function measureAsync() {
|
export function measureAsync() {
|
||||||
return lintDocument({
|
return lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { join as pathJoin, resolve as pathResolve } from 'path';
|
|||||||
import { lintDocument } from '../../lint';
|
import { lintDocument } from '../../lint';
|
||||||
import { BaseResolver } from '../../resolve';
|
import { BaseResolver } from '../../resolve';
|
||||||
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
||||||
|
|
||||||
|
import type { StyleguideConfig } from '../../config';
|
||||||
|
|
||||||
export const name = 'Validate with no rules';
|
export const name = 'Validate with no rules';
|
||||||
export const count = 10;
|
export const count = 10;
|
||||||
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
||||||
@@ -10,7 +13,11 @@ const rebillyDocument = parseYamlToDocument(
|
|||||||
readFileSync(rebillyDefinitionRef, 'utf-8'),
|
readFileSync(rebillyDefinitionRef, 'utf-8'),
|
||||||
rebillyDefinitionRef
|
rebillyDefinitionRef
|
||||||
);
|
);
|
||||||
const config = makeConfigForRuleset({});
|
|
||||||
|
let config: StyleguideConfig;
|
||||||
|
export async function setupAsync() {
|
||||||
|
config = await makeConfigForRuleset({});
|
||||||
|
}
|
||||||
export function measureAsync() {
|
export function measureAsync() {
|
||||||
return lintDocument({
|
return lintDocument({
|
||||||
externalRefResolver: new BaseResolver(),
|
externalRefResolver: new BaseResolver(),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { lintDocument } from '../../lint';
|
|||||||
import { BaseResolver } from '../../resolve';
|
import { BaseResolver } from '../../resolve';
|
||||||
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
||||||
|
|
||||||
|
import type { StyleguideConfig } from '../../config';
|
||||||
|
|
||||||
export const name = 'Validate with single top-level rule and report';
|
export const name = 'Validate with single top-level rule and report';
|
||||||
export const count = 10;
|
export const count = 10;
|
||||||
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
||||||
@@ -12,19 +14,22 @@ const rebillyDocument = parseYamlToDocument(
|
|||||||
rebillyDefinitionRef
|
rebillyDefinitionRef
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = makeConfigForRuleset({
|
let config: StyleguideConfig;
|
||||||
test: () => {
|
export async function setupAsync() {
|
||||||
return {
|
config = await makeConfigForRuleset({
|
||||||
Schema(schema, ctx) {
|
test: () => {
|
||||||
if (schema.type === 'number') {
|
return {
|
||||||
ctx.report({
|
Schema(schema, ctx) {
|
||||||
message: 'type number is not allowed',
|
if (schema.type === 'number') {
|
||||||
});
|
ctx.report({
|
||||||
}
|
message: 'type number is not allowed',
|
||||||
},
|
});
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function measureAsync() {
|
export function measureAsync() {
|
||||||
return lintDocument({
|
return lintDocument({
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { join as pathJoin, resolve as pathResolve } from 'path';
|
|||||||
import { lintDocument } from '../../lint';
|
import { lintDocument } from '../../lint';
|
||||||
import { BaseResolver } from '../../resolve';
|
import { BaseResolver } from '../../resolve';
|
||||||
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
import { parseYamlToDocument, makeConfigForRuleset } from '../utils';
|
||||||
|
|
||||||
|
import type { StyleguideConfig } from '../../config';
|
||||||
|
|
||||||
export const name = 'Validate with single top-level rule';
|
export const name = 'Validate with single top-level rule';
|
||||||
export const count = 10;
|
export const count = 10;
|
||||||
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
const rebillyDefinitionRef = pathResolve(pathJoin(__dirname, 'rebilly.yaml'));
|
||||||
@@ -11,17 +14,20 @@ const rebillyDocument = parseYamlToDocument(
|
|||||||
rebillyDefinitionRef
|
rebillyDefinitionRef
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = makeConfigForRuleset({
|
let config: StyleguideConfig;
|
||||||
test: () => {
|
export async function setupAsync() {
|
||||||
let count = 0;
|
config = await makeConfigForRuleset({
|
||||||
return {
|
test: () => {
|
||||||
Schema() {
|
let count = 0;
|
||||||
count++;
|
return {
|
||||||
if (count === -1) throw new Error('Disable optimization');
|
Schema() {
|
||||||
},
|
count++;
|
||||||
};
|
if (count === -1) throw new Error('Disable optimization');
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function measureAsync() {
|
export function measureAsync() {
|
||||||
return lintDocument({
|
return lintDocument({
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ if (require.main === module) {
|
|||||||
assert(process.send);
|
assert(process.send);
|
||||||
const module = require(modulePath);
|
const module = require(modulePath);
|
||||||
|
|
||||||
|
if (module.setupAsync) {
|
||||||
|
await module.setupAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (module.measureAsync) {
|
if (module.measureAsync) {
|
||||||
async function run() {
|
async function run() {
|
||||||
await clockAsync(7, module.measureAsync); // warm up
|
await clockAsync(7, module.measureAsync); // warm up
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ export function parseYamlToDocument(body: string, absoluteRef: string = ''): Doc
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeConfigForRuleset(rules: Oas3RuleSet, plugin?: Partial<Plugin>) {
|
export async function makeConfigForRuleset(rules: Oas3RuleSet, plugin?: Partial<Plugin>) {
|
||||||
const rulesConf: Record<string, RuleConfig> = {};
|
const rulesConf: Record<string, RuleConfig> = {};
|
||||||
const ruleId = 'test';
|
const ruleId = 'test';
|
||||||
Object.keys(rules).forEach((name) => {
|
Object.keys(rules).forEach((name) => {
|
||||||
rulesConf[`${ruleId}/${name}`] = 'error';
|
rulesConf[`${ruleId}/${name}`] = 'error';
|
||||||
});
|
});
|
||||||
const extendConfigs = [
|
const extendConfigs = [
|
||||||
resolvePlugins([
|
(await resolvePlugins([
|
||||||
{
|
{
|
||||||
...plugin,
|
...plugin,
|
||||||
id: ruleId,
|
id: ruleId,
|
||||||
rules: { oas3: rules },
|
rules: { oas3: rules },
|
||||||
},
|
},
|
||||||
]) as ResolvedStyleguideConfig,
|
])) as ResolvedStyleguideConfig,
|
||||||
];
|
];
|
||||||
if (rules) {
|
if (rules) {
|
||||||
extendConfigs.push({ rules });
|
extendConfigs.push({ rules });
|
||||||
|
|||||||
@@ -91,6 +91,88 @@ describe('resolveStyleguideConfig', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should resolve local file config with esm plugin', async () => {
|
||||||
|
const config = {
|
||||||
|
...baseStyleguideConfig,
|
||||||
|
extends: ['local-config-with-esm.yaml'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { plugins, ...styleguide } = await resolveStyleguideConfig({
|
||||||
|
styleguideConfig: config,
|
||||||
|
configPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(styleguide?.rules?.['operation-2xx-response']).toEqual('warn');
|
||||||
|
expect(plugins).toBeDefined();
|
||||||
|
expect(plugins?.length).toBe(2);
|
||||||
|
|
||||||
|
const localPlugin = plugins?.find((p) => p.id === 'test-plugin');
|
||||||
|
expect(localPlugin).toBeDefined();
|
||||||
|
|
||||||
|
expect(localPlugin).toMatchObject({
|
||||||
|
id: 'test-plugin',
|
||||||
|
rules: {
|
||||||
|
oas3: {
|
||||||
|
'test-plugin/oas3-rule-name': 'oas3-rule-stub',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(styleguide.extendPaths!.map(removeAbsolutePath)).toEqual([
|
||||||
|
'resolve-config/redocly.yaml',
|
||||||
|
'resolve-config/local-config-with-esm.yaml',
|
||||||
|
'resolve-config/redocly.yaml',
|
||||||
|
]);
|
||||||
|
expect(styleguide.pluginPaths!.map(removeAbsolutePath)).toEqual([
|
||||||
|
'resolve-config/plugin-esm.mjs',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(styleguide.rules).toEqual({
|
||||||
|
'operation-2xx-response': 'warn',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve local file config with commonjs plugin with a default export function', async () => {
|
||||||
|
const config = {
|
||||||
|
...baseStyleguideConfig,
|
||||||
|
extends: ['local-config-with-commonjs-export-function.yaml'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { plugins, ...styleguide } = await resolveStyleguideConfig({
|
||||||
|
styleguideConfig: config,
|
||||||
|
configPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(styleguide?.rules?.['operation-2xx-response']).toEqual('warn');
|
||||||
|
expect(plugins).toBeDefined();
|
||||||
|
expect(plugins?.length).toBe(2);
|
||||||
|
|
||||||
|
const localPlugin = plugins?.find((p) => p.id === 'test-plugin');
|
||||||
|
expect(localPlugin).toBeDefined();
|
||||||
|
|
||||||
|
expect(localPlugin).toMatchObject({
|
||||||
|
id: 'test-plugin',
|
||||||
|
rules: {
|
||||||
|
oas3: {
|
||||||
|
'test-plugin/oas3-rule-name': 'oas3-rule-stub',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(styleguide.extendPaths!.map(removeAbsolutePath)).toEqual([
|
||||||
|
'resolve-config/redocly.yaml',
|
||||||
|
'resolve-config/local-config-with-commonjs-export-function.yaml',
|
||||||
|
'resolve-config/redocly.yaml',
|
||||||
|
]);
|
||||||
|
expect(styleguide.pluginPaths!.map(removeAbsolutePath)).toEqual([
|
||||||
|
'resolve-config/plugin-with-export-function.cjs',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(styleguide.rules).toEqual({
|
||||||
|
'operation-2xx-response': 'warn',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: fix circular test
|
// TODO: fix circular test
|
||||||
it.skip('should throw circular error', () => {
|
it.skip('should throw circular error', () => {
|
||||||
const config = {
|
const config = {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
plugins:
|
||||||
|
- plugin-with-export-function.cjs
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
plugins:
|
||||||
|
- plugin-esm.mjs
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default async function () {
|
||||||
|
return {
|
||||||
|
id: 'test-plugin',
|
||||||
|
rules: {
|
||||||
|
oas3: {
|
||||||
|
'oas3-rule-name': 'oas3-rule-stub',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = async function () {
|
||||||
|
return {
|
||||||
|
id: 'test-plugin',
|
||||||
|
rules: {
|
||||||
|
oas3: {
|
||||||
|
'oas3-rule-name': 'oas3-rule-stub',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { loadConfig } from '../load';
|
import { loadConfig } from '../load';
|
||||||
|
|
||||||
describe('resolving a plugin', () => {
|
// FIXME: skipping due to the 'Test environment has been torn down' Jest error.
|
||||||
|
// Covered the below in the `miscellaneous/resolve-plugins` e2e tests.
|
||||||
|
// Unskip when Jest gets this fixed.
|
||||||
|
describe.skip('resolving a plugin', () => {
|
||||||
const configPath = path.join(__dirname, 'fixtures/plugin-config.yaml');
|
const configPath = path.join(__dirname, 'fixtures/plugin-config.yaml');
|
||||||
|
|
||||||
it('should prefix rule names with the plugin id', async () => {
|
it('should prefix rule names with the plugin id', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
import { isAbsoluteUrl } from '../ref-utils';
|
import { isAbsoluteUrl } from '../ref-utils';
|
||||||
import { pickDefined, isNotString, isString, isDefined, keysOf } from '../utils';
|
import { pickDefined, isNotString, isString, isDefined, keysOf } from '../utils';
|
||||||
import { resolveDocument, BaseResolver } from '../resolve';
|
import { resolveDocument, BaseResolver } from '../resolve';
|
||||||
@@ -6,6 +7,8 @@ import { defaultPlugin } from './builtIn';
|
|||||||
import {
|
import {
|
||||||
getResolveConfig,
|
getResolveConfig,
|
||||||
getUniquePlugins,
|
getUniquePlugins,
|
||||||
|
isCommonJsPlugin,
|
||||||
|
isDeprecatedPluginFormat,
|
||||||
mergeExtends,
|
mergeExtends,
|
||||||
parsePresetName,
|
parsePresetName,
|
||||||
prefixRules,
|
prefixRules,
|
||||||
@@ -27,12 +30,18 @@ import type {
|
|||||||
ResolvedStyleguideConfig,
|
ResolvedStyleguideConfig,
|
||||||
RuleConfig,
|
RuleConfig,
|
||||||
DeprecatedInRawConfig,
|
DeprecatedInRawConfig,
|
||||||
|
ImportedPlugin,
|
||||||
} from './types';
|
} from './types';
|
||||||
import type { Assertion, AssertionDefinition, RawAssertion } from '../rules/common/assertions';
|
import type { Assertion, AssertionDefinition, RawAssertion } from '../rules/common/assertions';
|
||||||
import type { Asserts, AssertionFn } from '../rules/common/assertions/asserts';
|
import type { Asserts, AssertionFn } from '../rules/common/assertions/asserts';
|
||||||
import type { BundleOptions } from '../bundle';
|
import type { BundleOptions } from '../bundle';
|
||||||
import type { Document, ResolvedRefMap } from '../resolve';
|
import type { Document, ResolvedRefMap } from '../resolve';
|
||||||
|
|
||||||
|
const DEFAULT_PROJECT_PLUGIN_PATHS = ['@theme/plugin.js', '@theme/plugin.cjs', '@theme/plugin.mjs'];
|
||||||
|
|
||||||
|
// Workaround for dynamic imports being transpiled to require by Typescript: https://github.com/microsoft/TypeScript/issues/43329#issuecomment-811606238
|
||||||
|
const _importDynamic = new Function('modulePath', 'return import(modulePath)');
|
||||||
|
|
||||||
export async function resolveConfigFileAndRefs({
|
export async function resolveConfigFileAndRefs({
|
||||||
configPath,
|
configPath,
|
||||||
externalRefResolver = new BaseResolver(),
|
externalRefResolver = new BaseResolver(),
|
||||||
@@ -101,14 +110,24 @@ export async function resolveConfig({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePlugins(
|
function getDefaultPluginPath(configPath: string): string | undefined {
|
||||||
|
for (const pluginPath of DEFAULT_PROJECT_PLUGIN_PATHS) {
|
||||||
|
const absolutePluginPath = path.resolve(path.dirname(configPath), pluginPath);
|
||||||
|
if (existsSync(absolutePluginPath)) {
|
||||||
|
return pluginPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolvePlugins(
|
||||||
plugins: (string | Plugin)[] | null,
|
plugins: (string | Plugin)[] | null,
|
||||||
configPath: string = ''
|
configPath: string = ''
|
||||||
): Plugin[] {
|
): Promise<Plugin[]> {
|
||||||
if (!plugins) return [];
|
if (!plugins) return [];
|
||||||
|
|
||||||
// TODO: implement or reuse Resolver approach so it will work in node and browser envs
|
// TODO: implement or reuse Resolver approach so it will work in node and browser envs
|
||||||
const requireFunc = (plugin: string | Plugin): Plugin | undefined => {
|
const requireFunc = async (plugin: string | Plugin): Promise<ImportedPlugin | undefined> => {
|
||||||
if (isBrowser && isString(plugin)) {
|
if (isBrowser && isString(plugin)) {
|
||||||
logger.error(`Cannot load ${plugin}. Plugins aren't supported in browser yet.`);
|
logger.error(`Cannot load ${plugin}. Plugins aren't supported in browser yet.`);
|
||||||
|
|
||||||
@@ -117,14 +136,24 @@ export function resolvePlugins(
|
|||||||
|
|
||||||
if (isString(plugin)) {
|
if (isString(plugin)) {
|
||||||
try {
|
try {
|
||||||
const absoltePluginPath = path.resolve(path.dirname(configPath), plugin);
|
const maybeAbsolutePluginPath = path.resolve(path.dirname(configPath), plugin);
|
||||||
|
|
||||||
|
const absolutePluginPath = existsSync(maybeAbsolutePluginPath)
|
||||||
|
? maybeAbsolutePluginPath
|
||||||
|
: // For plugins imported from packages specifically
|
||||||
|
require.resolve(plugin);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return typeof __webpack_require__ === 'function'
|
if (typeof __webpack_require__ === 'function') {
|
||||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
__non_webpack_require__(absoltePluginPath)
|
return __non_webpack_require__(absolutePluginPath);
|
||||||
: require(absoltePluginPath);
|
} else {
|
||||||
|
// you can import both cjs and mjs
|
||||||
|
const mod = await _importDynamic(absolutePluginPath);
|
||||||
|
return mod.default || mod;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SyntaxError) {
|
if (e instanceof SyntaxError) {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -138,19 +167,47 @@ export function resolvePlugins(
|
|||||||
|
|
||||||
const seenPluginIds = new Map<string, string>();
|
const seenPluginIds = new Map<string, string>();
|
||||||
|
|
||||||
return plugins
|
/**
|
||||||
.map((p) => {
|
* Include the default plugin automatically if it's not in configuration
|
||||||
if (isString(p) && isAbsoluteUrl(p)) {
|
*/
|
||||||
throw new Error(colorize.red(`We don't support remote plugins yet.`));
|
const defaultPluginPath = getDefaultPluginPath(configPath);
|
||||||
|
if (defaultPluginPath) {
|
||||||
|
plugins.push(defaultPluginPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPlugins: Set<string> = new Set();
|
||||||
|
|
||||||
|
const instances = await Promise.all(
|
||||||
|
plugins.map(async (p) => {
|
||||||
|
if (isString(p)) {
|
||||||
|
if (isAbsoluteUrl(p)) {
|
||||||
|
throw new Error(colorize.red(`We don't support remote plugins yet.`));
|
||||||
|
}
|
||||||
|
if (resolvedPlugins.has(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedPlugins.add(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: resolve npm packages similar to eslint
|
const requiredPlugin: ImportedPlugin | undefined = await requireFunc(p);
|
||||||
const pluginModule = requireFunc(p);
|
|
||||||
|
const pluginCreatorOptions = { contentDir: path.dirname(configPath) };
|
||||||
|
|
||||||
|
const pluginModule = isDeprecatedPluginFormat(requiredPlugin)
|
||||||
|
? requiredPlugin
|
||||||
|
: isCommonJsPlugin(requiredPlugin)
|
||||||
|
? await requiredPlugin(pluginCreatorOptions)
|
||||||
|
: await requiredPlugin?.default?.(pluginCreatorOptions);
|
||||||
|
|
||||||
if (!pluginModule) {
|
if (!pluginModule) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isString(p) && pluginModule.id && isDeprecatedPluginFormat(requiredPlugin)) {
|
||||||
|
logger.info(`Deprecated plugin format detected: ${pluginModule.id}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
const id = pluginModule.id;
|
const id = pluginModule.id;
|
||||||
if (typeof id !== 'string') {
|
if (typeof id !== 'string') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -266,7 +323,9 @@ export function resolvePlugins(
|
|||||||
|
|
||||||
return plugin;
|
return plugin;
|
||||||
})
|
})
|
||||||
.filter(isDefined);
|
);
|
||||||
|
|
||||||
|
return instances.filter(isDefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveApis({
|
export async function resolveApis({
|
||||||
@@ -317,7 +376,7 @@ async function resolveAndMergeNestedStyleguideConfig(
|
|||||||
throw new Error(`Circular dependency in config file: "${configPath}"`);
|
throw new Error(`Circular dependency in config file: "${configPath}"`);
|
||||||
}
|
}
|
||||||
const plugins = getUniquePlugins(
|
const plugins = getUniquePlugins(
|
||||||
resolvePlugins([...(styleguideConfig?.plugins || []), defaultPlugin], configPath)
|
await resolvePlugins([...(styleguideConfig?.plugins || []), defaultPlugin], configPath)
|
||||||
);
|
);
|
||||||
const pluginPaths = styleguideConfig?.plugins
|
const pluginPaths = styleguideConfig?.plugins
|
||||||
?.filter(isString)
|
?.filter(isString)
|
||||||
|
|||||||
@@ -144,6 +144,22 @@ export type Plugin = {
|
|||||||
assertions?: AssertionsConfig;
|
assertions?: AssertionsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PluginCreatorOptions = {
|
||||||
|
contentDir: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginCreator = (options: PluginCreatorOptions) => Plugin | Promise<Plugin>;
|
||||||
|
|
||||||
|
export type ImportedPlugin =
|
||||||
|
// ES Modules
|
||||||
|
| {
|
||||||
|
default?: PluginCreator;
|
||||||
|
}
|
||||||
|
// CommonJS
|
||||||
|
| PluginCreator
|
||||||
|
// Deprecated format
|
||||||
|
| Plugin;
|
||||||
|
|
||||||
export type PluginStyleguideConfig<T = undefined> = Omit<
|
export type PluginStyleguideConfig<T = undefined> = Omit<
|
||||||
StyleguideRawConfig<T>,
|
StyleguideRawConfig<T>,
|
||||||
'plugins' | 'extends'
|
'plugins' | 'extends'
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import type {
|
|||||||
Api,
|
Api,
|
||||||
DeprecatedInApi,
|
DeprecatedInApi,
|
||||||
DeprecatedInRawConfig,
|
DeprecatedInRawConfig,
|
||||||
|
ImportedPlugin,
|
||||||
FlatApi,
|
FlatApi,
|
||||||
FlatRawConfig,
|
FlatRawConfig,
|
||||||
Plugin,
|
|
||||||
RawConfig,
|
RawConfig,
|
||||||
RawResolveConfig,
|
RawResolveConfig,
|
||||||
ResolveConfig,
|
ResolveConfig,
|
||||||
@@ -22,6 +22,8 @@ import type {
|
|||||||
RulesFields,
|
RulesFields,
|
||||||
StyleguideRawConfig,
|
StyleguideRawConfig,
|
||||||
ThemeConfig,
|
ThemeConfig,
|
||||||
|
Plugin,
|
||||||
|
PluginCreator,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export function parsePresetName(presetName: string): { pluginId: string; configName: string } {
|
export function parsePresetName(presetName: string): { pluginId: string; configName: string } {
|
||||||
@@ -399,3 +401,11 @@ export class ConfigValidationError extends Error {}
|
|||||||
export function deepCloneMapWithJSON<K, V>(originalMap: Map<K, V>): Map<K, V> {
|
export function deepCloneMapWithJSON<K, V>(originalMap: Map<K, V>): Map<K, V> {
|
||||||
return new Map(JSON.parse(JSON.stringify([...originalMap])));
|
return new Map(JSON.parse(JSON.stringify([...originalMap])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDeprecatedPluginFormat(plugin: ImportedPlugin | undefined): plugin is Plugin {
|
||||||
|
return plugin !== undefined && typeof plugin === 'object' && 'id' in plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCommonJsPlugin(plugin: ImportedPlugin | undefined): plugin is PluginCreator {
|
||||||
|
return typeof plugin === 'function';
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,19 @@ import { StyleguideConfig, defaultPlugin, resolvePlugins, resolvePreset } from '
|
|||||||
|
|
||||||
import { BaseResolver } from '../../../../resolve';
|
import { BaseResolver } from '../../../../resolve';
|
||||||
|
|
||||||
const plugins = resolvePlugins([defaultPlugin]);
|
import type { Plugin, ResolvedStyleguideConfig } from '../../../../config';
|
||||||
const pressets = resolvePreset('all', plugins);
|
|
||||||
const allConfig = new StyleguideConfig({ ...pressets, plugins });
|
|
||||||
describe('Oas3 Structural visitor basic', () => {
|
describe('Oas3 Structural visitor basic', () => {
|
||||||
|
let plugins: Plugin[];
|
||||||
|
let presets: ResolvedStyleguideConfig;
|
||||||
|
let allConfig: StyleguideConfig;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
plugins = await resolvePlugins([defaultPlugin]);
|
||||||
|
presets = resolvePreset('all', plugins);
|
||||||
|
allConfig = new StyleguideConfig({ ...presets, plugins });
|
||||||
|
});
|
||||||
|
|
||||||
it('should report wrong types', async () => {
|
it('should report wrong types', async () => {
|
||||||
const document = parseYamlToDocument(
|
const document = parseYamlToDocument(
|
||||||
outdent`
|
outdent`
|
||||||
|
|||||||
Reference in New Issue
Block a user