Repository: Redocly/redocly-cli
Pull request #811 (2022-08-16 14:19:45)

feat: openapi-core in browser support
## What/Why/How?
Make `openapi-core` browser compatible.

## Reference

## Testing

## Screenshots (optional)

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 

❌ An unexpected error occurred. For more details, [check console](https://github.com/Redocly/redocly-cli/actions/runs/2868683359) 
```
Error: The process '/usr/local/bin/npm' failed with exit code 1
```

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage                                                  | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :---------------------------------------------------------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 67.68%                                                      |    2852/4214    |
|                                                                           🟡                                                                           | Branches   | <div title="Base coverage is: 61.9%">60.8% (-1.1% 🔻)</div> |    1562/2569    |
|                                                                           🟡                                                                           | Functions  | 60.1%                                                       |     464/772     |
|                                                                           🟡                                                                           | Lines      | 67.42%                                                      |    2639/3914    |

<details><summary>Show new covered files 🐣</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                        | Statements | Branches | Functions | Lines |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------- | :--------- | :------- | :-------- | :---- |
|                                                                           🟢                                                                           | <div title="core/src/colorize.ts">`...` / colorize.ts</div> | 90%        | 100%     | 100%      | 87.5% |

</details>


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                                               | Statements                                                      | Branches                                                       | Functions                                                    | Lines                                                           |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :------------------------------------------------------------- | :----------------------------------------------------------- | :-------------------------------------------------------------- |
|                                                                           🟡                                                                           | core/src/utils.ts                                                                  | 73.08%                                                          | <div title="Base coverage is: 58.7%">56.52% (-2.17% 🔻)</div>  | 70.27%                                                       | 73.2%                                                           |
|                                                                           🟢                                                                           | <div title="core/src/config/utils.ts">`...` / utils.ts</div>                       | <div title="Base coverage is: 98.91%">91.3% (-7.61% 🔻)</div>   | <div title="Base coverage is: 90.72%">88.66% (-2.06% 🔻)</div> | <div title="Base coverage is: 100%">92.31% (-7.69% 🔻)</div> | <div title="Base coverage is: 98.82%">91.76% (-7.06% 🔻)</div>  |
|                                                                           🔴                                                                           | <div title="core/src/config/config-resolvers.ts">`...` / config-resolvers.ts</div> | <div title="Base coverage is: 86.89%">59.68% (-27.21% 🔻)</div> | <div title="Base coverage is: 77.04%">67.05% (-9.99% 🔻)</div> | <div title="Base coverage is: 100%">93.33% (-6.67% 🔻)</div> | <div title="Base coverage is: 87.29%">59.17% (-28.12% 🔻)</div> |

</details>

## Test suite run failed

<details><summary>Failed tests: 42/460. Failed suites: 4/77.</summary>
<br/>

```
  ● walk order › should run visitors

    expect(jest.fn()).toBeCalledTimes(expected)

    Expected number of calls: 1
    Received number of calls: 0

      57 |     });
      58 |
    > 59 |     expect(testRuleSet.test).toBeCalledTimes(1);
         |                              ^
      60 |     for (const fns of Object.values(visitors)) {
      61 |       expect(fns.enter).toBeCalled();
      62 |       expect(fns.leave).toBeCalled();

      at packages/core/src/__tests__/walk.test.ts:59:30
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should run nested visitors correctly

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should run nested visitors correctly 1`

    - Snapshot  - 20
    + Received  +  1

    - Array [
    -   "enter param path-param",
    -   "leave param path-param",
    -   "enter operation: get",
    -   "enter operation get > param get_a",
    -   "enter param get_a",
    -   "leave param get_a",
    -   "leave operation get > param get_a",
    -   "enter operation get > param get_b",
    -   "enter param get_b",
    -   "leave param get_b",
    -   "leave operation get > param get_b",
    -   "leave operation: get",
    -   "enter operation: post",
    -   "enter operation post > param post_a",
    -   "enter param post_a",
    -   "leave param post_a",
    -   "leave operation post > param post_a",
    -   "leave operation: post",
    - ]
    + Array []

      120 |     });
      121 |
    > 122 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      123 |       Array [
      124 |         "enter param path-param",
      125 |         "leave param path-param",

      at packages/core/src/__tests__/walk.test.ts:122:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should run nested visitors correctly oas2

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should run nested visitors correctly oas2 1`

    - Snapshot  - 20
    + Received  +  1

    - Array [
    -   "enter param path-param",
    -   "leave param path-param",
    -   "enter operation: get",
    -   "enter operation get > param get_a",
    -   "enter param get_a",
    -   "leave param get_a",
    -   "leave operation get > param get_a",
    -   "enter operation get > param get_b",
    -   "enter param get_b",
    -   "leave param get_b",
    -   "leave operation get > param get_b",
    -   "leave operation: get",
    -   "enter operation: post",
    -   "enter operation post > param post_a",
    -   "enter param post_a",
    -   "leave param post_a",
    -   "leave operation post > param post_a",
    -   "leave operation: post",
    - ]
    + Array []

      200 |     });
      201 |
    > 202 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      203 |       Array [
      204 |         "enter param path-param",
      205 |         "leave param path-param",

      at packages/core/src/__tests__/walk.test.ts:202:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should resolve refs

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should resolve refs 1`

    - Snapshot  - 16
    + Received  +  1

    - Array [
    -   "enter operation: get",
    -   "enter operation get > param shared-a",
    -   "enter param shared-a",
    -   "leave param shared-a",
    -   "leave operation get > param shared-a",
    -   "enter operation get > param get_b",
    -   "enter param get_b",
    -   "leave param get_b",
    -   "leave operation get > param get_b",
    -   "leave operation: get",
    -   "enter operation: post",
    -   "enter operation post > param shared-a",
    -   "leave operation post > param shared-a",
    -   "leave operation: post",
    - ]
    + Array []

      281 |     });
      282 |
    > 283 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      284 |       Array [
      285 |         "enter operation: get",
      286 |         "enter operation get > param shared-a",

      at packages/core/src/__tests__/walk.test.ts:283:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should visit with context same refs with gaps in visitor simple

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should visit with context same refs with gaps in visitor simple 1`

    - Snapshot  - 6
    + Received  + 1

    - Array [
    -   "enter path pet > param path-param",
    -   "enter path pet > param shared-a",
    -   "enter path pet > param get_b",
    -   "enter path dog > param shared-a",
    - ]
    + Array []

      353 |     });
      354 |
    > 355 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      356 |       Array [
      357 |         "enter path pet > param path-param",
      358 |         "enter path pet > param shared-a",

      at packages/core/src/__tests__/walk.test.ts:355:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly visit more specific visitor

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly visit more specific visitor 1`

    - Snapshot  - 7
    + Received  + 1

    - Array [
    -   "enter path pet > param path-param",
    -   "enter operation get > param shared-a",
    -   "enter operation get > param get_b",
    -   "enter operation get > param get_c",
    -   "enter operation post > param shared-b",
    - ]
    + Array []

      425 |     });
      426 |
    > 427 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      428 |       Array [
      429 |         "enter path pet > param path-param",
      430 |         "enter operation get > param shared-a",

      at packages/core/src/__tests__/walk.test.ts:427:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should visit with context same refs with gaps in visitor and nested rule

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should visit with context same refs with gaps in visitor and nested rule 1`

    - Snapshot  - 12
    + Received  +  1

    - Array [
    -   "enter path pet > param path-param",
    -   "leave path pet > param path-param",
    -   "enter path pet > op get",
    -   "enter path pet > param shared-a",
    -   "leave path pet > param shared-a",
    -   "enter path pet > param get_b",
    -   "leave path pet > param get_b",
    -   "enter path dog > op post",
    -   "enter path dog > param shared-a",
    -   "leave path dog > param shared-a",
    - ]
    + Array []

      492 |     });
      493 |
    > 494 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      495 |       Array [
      496 |         "enter path pet > param path-param",
      497 |         "leave path pet > param path-param",

      at packages/core/src/__tests__/walk.test.ts:494:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should visit and do not recurse for circular refs top-level

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should visit and do not recurse for circular refs top-level 1`

    - Snapshot  - 4
    + Received  + 1

    - Array [
    -   "enter schema shared_a",
    -   "enter schema nested",
    - ]
    + Array []

      546 |     });
      547 |
    > 548 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      549 |       Array [
      550 |         "enter schema shared_a",
      551 |         "enter schema nested",

      at packages/core/src/__tests__/walk.test.ts:548:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should visit and do not recurse for circular refs with context

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should visit and do not recurse for circular refs with context 1`

    - Snapshot  - 4
    + Received  + 1

    - Array [
    -   "enter param a > schema shared_a",
    -   "enter param b > schema shared_a",
    - ]
    + Array []

      602 |     });
      603 |
    > 604 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      605 |       Array [
      606 |         "enter param a > schema shared_a",
      607 |         "enter param b > schema shared_a",

      at packages/core/src/__tests__/walk.test.ts:604:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly skip top level

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly skip top level 1`

    - Snapshot  - 4
    + Received  + 1

    - Array [
    -   "enter operation get",
    -   "leave operation get",
    - ]
    + Array []

      644 |     });
      645 |
    > 646 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      647 |       Array [
      648 |         "enter operation get",
      649 |         "leave operation get",

      at packages/core/src/__tests__/walk.test.ts:646:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly skip nested levels

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly skip nested levels 1`

    - Snapshot  - 5
    + Received  + 1

    - Array [
    -   "enter operation get > param shared-a",
    -   "enter operation get > param get_b",
    -   "enter operation get > param get_c",
    - ]
    + Array []

      699 |     });
      700 |
    > 701 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      702 |       Array [
      703 |         "enter operation get > param shared-a",
      704 |         "enter operation get > param get_b",

      at packages/core/src/__tests__/walk.test.ts:701:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly visit more specific visitor with skips

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly visit more specific visitor with skips 1`

    - Snapshot  - 12
    + Received  +  1

    - Array [
    -   "enter path pet > param path-param",
    -   "leave path pet > param path-param",
    -   "enter operation get > param shared-a",
    -   "leave operation get > param shared-a",
    -   "enter operation get > param get_b",
    -   "leave operation get > param get_b",
    -   "enter operation get > param get_c",
    -   "leave operation get > param get_c",
    -   "enter operation post > param shared-b",
    -   "leave operation post > param shared-b",
    - ]
    + Array []

      785 |     });
      786 |
    > 787 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      788 |       Array [
      789 |         "enter path pet > param path-param",
      790 |         "leave path pet > param path-param",

      at packages/core/src/__tests__/walk.test.ts:787:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly visit with nested rules

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly visit with nested rules 1`

    - Snapshot  - 12
    + Received  +  1

    - Array [
    -   "enter nested schema inline-top > inline-top",
    -   "enter nested schema inline-top > inline-nested",
    -   "enter nested schema inline-nested > inline-nested-nested",
    -   "leave nested schema inline-nested > inline-nested-nested",
    -   "leave nested schema inline-top > inline-nested",
    -   "leave nested schema inline-top > inline-top",
    -   "enter nested schema inline-top > inline-nested-2",
    -   "enter nested schema inline-nested-2 > inline-nested-nested-2",
    -   "leave nested schema inline-nested-2 > inline-nested-nested-2",
    -   "leave nested schema inline-top > inline-nested-2",
    - ]
    + Array []

      864 |     });
      865 |
    > 866 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      867 |       Array [
      868 |         "enter nested schema inline-top > inline-top",
      869 |         "enter nested schema inline-top > inline-nested",

      at packages/core/src/__tests__/walk.test.ts:866:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly visit refs

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly visit refs 1`

    - Snapshot  - 6
    + Received  + 1

    - Array [
    -   "enter $ref #/components/parameters/shared_b with target shared-b",
    -   "enter $ref #/components/parameters/shared_b with target shared-b",
    -   "enter $ref #/components/parameters/shared_a with target shared-a",
    -   "enter $ref #/components/parameters/shared_a with target shared-a",
    - ]
    + Array []

      936 |     });
      937 |
    > 938 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      939 |       Array [
      940 |         "enter $ref #/components/parameters/shared_b with target shared-b",
      941 |         "enter $ref #/components/parameters/shared_b with target shared-b",

      at packages/core/src/__tests__/walk.test.ts:938:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly visit refs

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly visit refs 2`

    - Snapshot  - 4
    + Received  + 1

    - Array [
    -   "enter schema a: string",
    -   "enter schema b: number",
    - ]
    + Array []

      980 |     });
      981 |
    > 982 |     expect(calls).toMatchInlineSnapshot(`
          |                   ^
      983 |       Array [
      984 |         "enter schema a: string",
      985 |         "enter schema b: number",

      at packages/core/src/__tests__/walk.test.ts:982:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● walk order › should correctly visit any visitor

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `walk order should correctly visit any visitor 1`

    - Snapshot  - 32
    + Received  +  1

    - Array [
    -   "enter DefinitionRoot",
    -   "enter PathMap",
    -   "enter PathItem",
    -   "enter ParameterList",
    -   "enter Parameter",
    -   "leave Parameter",
    -   "leave ParameterList",
    -   "enter Operation",
    -   "enter ParameterList",
    -   "enter ref #/components/parameters/shared_a",
    -   "enter Parameter",
    -   "leave Parameter",
    -   "leave ref #/components/parameters/shared_a",
    -   "enter Parameter",
    -   "leave Parameter",
    -   "enter Parameter",
    -   "leave Parameter",
    -   "leave ParameterList",
    -   "leave Operation",
    -   "leave PathItem",
    -   "leave PathMap",
    -   "enter Components",
    -   "enter NamedParameters",
    -   "leave NamedParameters",
    -   "enter NamedSchemas",
    -   "enter Schema",
    -   "leave Schema",
    -   "leave NamedSchemas",
    -   "leave Components",
    -   "leave DefinitionRoot",
    - ]
    + Array []

      1045 |     });
      1046 |
    > 1047 |     expect(calls).toMatchInlineSnapshot(`
           |                   ^
      1048 |       Array [
      1049 |         "enter DefinitionRoot",
      1050 |         "enter PathMap",

      at packages/core/src/__tests__/walk.test.ts:1047:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● context.report › should report errors correctly

    expect(received).toHaveLength(expected)

    Expected length: 3
    Received length: 0
    Received array:  []

      1133 |     });
      1134 |
    > 1135 |     expect(results).toHaveLength(3);
           |                     ^
      1136 |     expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
      1137 |       Array [
      1138 |         Object {

      at packages/core/src/__tests__/walk.test.ts:1135:21
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● context.report › should report errors correctly

    expect(received).toHaveLength(expected)

    Expected length: 4
    Received length: 0
    Received array:  []

      1213 |     });
      1214 |
    > 1215 |     expect(results).toHaveLength(4);
           |                     ^
      1216 |     expect(replaceSourceWithRef(results, cwd)).toMatchInlineSnapshot(`
      1217 |       Array [
      1218 |         Object {

      at packages/core/src/__tests__/walk.test.ts:1215:21
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● type extensions › should correctly visit OpenAPI 3.0.0 extended types

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `type extensions should correctly visit OpenAPI 3.0.0 extended types 1`

    - Snapshot  - 12
    + Received  +  1

    - Array [
    -   "enter DefinitionRoot",
    -   "enter XWebHooks",
    -   "enter hook test",
    -   "enter ParameterList",
    -   "enter Parameter",
    -   "leave Parameter",
    -   "leave ParameterList",
    -   "leave hook test",
    -   "leave XWebHooks",
    -   "leave DefinitionRoot",
    - ]
    + Array []

      1392 |     });
      1393 |
    > 1394 |     expect(calls).toMatchInlineSnapshot(`
           |                   ^
      1395 |       Array [
      1396 |         "enter DefinitionRoot",
      1397 |         "enter XWebHooks",

      at packages/core/src/__tests__/walk.test.ts:1394:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● type extensions › should correctly visit OpenAPI 3.1.0 extended types

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `type extensions should correctly visit OpenAPI 3.1.0 extended types 1`

    - Snapshot  - 12
    + Received  +  1

    - Array [
    -   "enter DefinitionRoot",
    -   "enter XWebHooks",
    -   "enter hook test",
    -   "enter ParameterList",
    -   "enter Parameter",
    -   "leave Parameter",
    -   "leave ParameterList",
    -   "leave hook test",
    -   "leave XWebHooks",
    -   "leave DefinitionRoot",
    - ]
    + Array []

      1392 |     });
      1393 |
    > 1394 |     expect(calls).toMatchInlineSnapshot(`
           |                   ^
      1395 |       Array [
      1396 |         "enter DefinitionRoot",
      1397 |         "enter XWebHooks",

      at packages/core/src/__tests__/walk.test.ts:1394:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

  ● ignoreNextRules › should correctly skip top level

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `ignoreNextRules should correctly skip top level 1`

    - Snapshot  - 8
    + Received  + 1

    - Array [
    -   "enter and skip operation get",
    -   "leave skipped operation get",
    -   "enter and not skip operation put",
    -   "enter operation put",
    -   "leave not skipped operation put",
    -   "leave operation put",
    - ]
    + Array []

      1464 |     });
      1465 |
    > 1466 |     expect(calls).toMatchInlineSnapshot(`
           |                   ^
      1467 |       Array [
      1468 |         "enter and skip operation get",
      1469 |         "leave skipped operation get",

      at packages/core/src/__tests__/walk.test.ts:1466:19
      at fulfilled (packages/core/src/__tests__/walk.test.ts:5:58)

```
---
```
  ● resolveStyleguideConfig › should resolve extends with local file config

    Invalid config test-plugin/all: plugin test-plugin is not included.

      292 |   const plugin = plugins.find((p) => p.id === pluginId);
      293 |   if (!plugin) {
    > 294 |     throw new Error(
          |           ^
      295 |       `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`
      296 |     );
      297 |   }

      at resolvePreset (packages/core/src/config/config-resolvers.ts:294:11)
      at packages/core/src/config/config-resolvers.ts:227:16
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at map (packages/core/src/config/config-resolvers.ts:225:57)
          at Array.map (<anonymous>)
      at packages/core/src/config/config-resolvers.ts:225:32
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at resolveAndMergeNestedStyleguideConfig (packages/core/src/config/config-resolvers.ts:8560:10)
      at packages/core/src/config/config-resolvers.ts:235:20
      at fulfilled (packages/core/src/config/config-resolvers.ts:7837:24)

  ● resolveStyleguideConfig › should resolve extends with local file config witch contains path to nested config

    Invalid config test-plugin-nested/all: plugin test-plugin-nested is not included.

      292 |   const plugin = plugins.find((p) => p.id === pluginId);
      293 |   if (!plugin) {
    > 294 |     throw new Error(
          |           ^
      295 |       `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`
      296 |     );
      297 |   }

      at resolvePreset (packages/core/src/config/config-resolvers.ts:294:11)
      at packages/core/src/config/config-resolvers.ts:227:16
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at map (packages/core/src/config/config-resolvers.ts:225:57)
          at Array.map (<anonymous>)
      at packages/core/src/config/config-resolvers.ts:225:32
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at resolveAndMergeNestedStyleguideConfig (packages/core/src/config/config-resolvers.ts:8560:10)
      at packages/core/src/config/config-resolvers.ts:235:20
      at fulfilled (packages/core/src/config/config-resolvers.ts:7837:24)

  ● resolveStyleguideConfig › should correctly merge assertions from nested config

    Invalid config test-plugin-nested/all: plugin test-plugin-nested is not included.

      292 |   const plugin = plugins.find((p) => p.id === pluginId);
      293 |   if (!plugin) {
    > 294 |     throw new Error(
          |           ^
      295 |       `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`
      296 |     );
      297 |   }

      at resolvePreset (packages/core/src/config/config-resolvers.ts:294:11)
      at packages/core/src/config/config-resolvers.ts:227:16
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at map (packages/core/src/config/config-resolvers.ts:225:57)
          at Array.map (<anonymous>)
      at packages/core/src/config/config-resolvers.ts:225:32
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at resolveAndMergeNestedStyleguideConfig (packages/core/src/config/config-resolvers.ts:8560:10)
      at packages/core/src/config/config-resolvers.ts:235:20
      at fulfilled (packages/core/src/config/config-resolvers.ts:7837:24)

  ● resolveApis › should resolve apis styleguideConfig when it contains file

    Invalid config test-plugin/all: plugin test-plugin is not included.

      292 |   const plugin = plugins.find((p) => p.id === pluginId);
      293 |   if (!plugin) {
    > 294 |     throw new Error(
          |           ^
      295 |       `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`
      296 |     );
      297 |   }

      at resolvePreset (packages/core/src/config/config-resolvers.ts:294:11)
      at packages/core/src/config/config-resolvers.ts:227:16
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at map (packages/core/src/config/config-resolvers.ts:225:57)
          at Array.map (<anonymous>)
      at packages/core/src/config/config-resolvers.ts:225:32
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at resolveAndMergeNestedStyleguideConfig (packages/core/src/config/config-resolvers.ts:8560:10)
      at packages/core/src/config/config-resolvers.ts:235:20
      at fulfilled (packages/core/src/config/config-resolvers.ts:7837:24)

  ● resolveConfig › should not add recommended to top level by default when apis have extends file

    Invalid config test-plugin/all: plugin test-plugin is not included.

      292 |   const plugin = plugins.find((p) => p.id === pluginId);
      293 |   if (!plugin) {
    > 294 |     throw new Error(
          |           ^
      295 |       `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`
      296 |     );
      297 |   }

      at resolvePreset (packages/core/src/config/config-resolvers.ts:294:11)
      at packages/core/src/config/config-resolvers.ts:227:16
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at map (packages/core/src/config/config-resolvers.ts:225:57)
          at Array.map (<anonymous>)
      at packages/core/src/config/config-resolvers.ts:225:32
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at resolveAndMergeNestedStyleguideConfig (packages/core/src/config/config-resolvers.ts:8560:10)
      at packages/core/src/config/config-resolvers.ts:235:20
      at fulfilled (packages/core/src/config/config-resolvers.ts:7837:24)

  ● resolveConfig › should ignore minimal from the root and read local file

    Invalid config test-plugin/all: plugin test-plugin is not included.

      292 |   const plugin = plugins.find((p) => p.id === pluginId);
      293 |   if (!plugin) {
    > 294 |     throw new Error(
          |           ^
      295 |       `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`
      296 |     );
      297 |   }

      at resolvePreset (packages/core/src/config/config-resolvers.ts:294:11)
      at packages/core/src/config/config-resolvers.ts:227:16
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at map (packages/core/src/config/config-resolvers.ts:225:57)
          at Array.map (<anonymous>)
      at packages/core/src/config/config-resolvers.ts:225:32
      at packages/core/src/config/config-resolvers.ts:7878:39
      at Object.<anonymous>.__awaiter (packages/core/src/config/config-resolvers.ts:7821:10)
      at resolveAndMergeNestedStyleguideConfig (packages/core/src/config/config-resolvers.ts:8560:10)
      at packages/core/src/config/config-resolvers.ts:235:20
      at fulfilled (packages/core/src/config/config-resolvers.ts:7837:24)

```
---
```
  ● codeframes › should correctly generate simple codeframe

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes should correctly generate simple codeframe 2`

    - Snapshot  - 5
    + Received  + 5

    - <g>1 |</g> <g>openapi: 3.0.2</g>
    - <g>2 |</g> <g>info:</g>
    - <g>3 |</g>   <r>license</r>:
    - <g>4 |</g>     <g>name: MIT</g>
    - <g>5 |</g>     <g>url: https://google.com</g>
    + 1 | openapi: 3.0.2
    + 2 | info:
    + 3 |   license:
    + 4 |     name: MIT
    + 5 |     url: https://google.com

      171 |     `);
      172 |
    > 173 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      174 |       <g>1 |</g> <g>openapi: 3.0.2</g>
      175 |       <g>2 |</g> <g>info:</g>
      176 |       <g>3 |</g>   <r>license</r>:

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:173:36)

  ● codeframes › correctly generate code-frame for single-line file on key

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for single-line file on key 2`

    - Snapshot  - 1
    + Received  + 1

    - <g>1 |</g> <r>openapi</r>: 3.0.2
    + 1 | openapi: 3.0.2

      192 |     `);
      193 |
    > 194 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`<g>1 |</g> <r>openapi</r>: 3.0.2`);
          |                                    ^
      195 |   });
      196 |
      197 |   it('correctly generate code-frame for single-line file on value', () => {

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:194:36)

  ● codeframes › correctly generate code-frame for single-line file on value

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for single-line file on value 2`

    - Snapshot  - 1
    + Received  + 1

    - <g>1 |</g> openapi: <r>3.0.2</r>
    + 1 | openapi: 3.0.2

      207 |     `);
      208 |
    > 209 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`<g>1 |</g> openapi: <r>3.0.2</r>`);
          |                                    ^
      210 |   });
      211 |
      212 |   it('correctly generate code-frame for multiline-value', () => {

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:209:36)

  ● codeframes › correctly generate code-frame for multiline-value

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for multiline-value 2`

    - Snapshot  - 4
    + Received  + 4

    - <g>2 |</g> <g>info:</g>
    - <g>3 |</g>   <g>license:</g>
    - <g>4 |</g>     <r>name: MIT</r>
    - <g>5 |</g>     <r>url: https://google.com</r>
    + 2 | info:
    + 3 |   license:
    + 4 |     name: MIT
    + 5 |     url: https://google.com

      235 |     `);
      236 |
    > 237 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      238 |       <g>2 |</g> <g>info:</g>
      239 |       <g>3 |</g>   <g>license:</g>
      240 |       <g>4 |</g>     <r>name: MIT</r>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:237:36)

  ● codeframes › correctly generate code-frame for multiline-value json-like

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for multiline-value json-like 2`

    - Snapshot  - 6
    + Received  + 6

    - <g>1 |</g> <g>openapi: 3.0.2</g>
    - <g>2 |</g> <g>info:</g>
    - <g>3 |</g>   license: <r>{</r>
    - <g>4 |</g>     <r>name: MIT,</r>
    - <g>5 |</g>     <r>url: https://google.com</r>
    - <g>6 |</g>   <r>}</r>
    + 1 | openapi: 3.0.2
    + 2 | info:
    + 3 |   license: {
    + 4 |     name: MIT,
    + 5 |     url: https://google.com
    + 6 |   }

      273 |     `);
      274 |
    > 275 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      276 |       <g>1 |</g> <g>openapi: 3.0.2</g>
      277 |       <g>2 |</g> <g>info:</g>
      278 |       <g>3 |</g>   license: <r>{</r>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:275:36)

  ● codeframes › correctly generate code-frame for multiline-value and show only 5 lines

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for multiline-value and show only 5 lines 2`

    - Snapshot  - 7
    + Received  + 7

    - <g>1 |</g> <g>info:</g>
    - <g>2 |</g>   <g>license:</g>
    - <g>3 |</g>     <r>name: MIT</r>
    - <g>4 |</g>     <r>field2: MIT</r>
    - <g>… |</g>     <g>< 3 more lines ></g>
    - <g>8 |</g>     <r>url: https://google.com</r>
    - <g>9 |</g> <g>openapi: 3.0.2</g>
    + 1 | info:
    + 2 |   license:
    + 3 |     name: MIT
    + 4 |     field2: MIT
    + … |     < 3 more lines >
    + 8 |     url: https://google.com
    + 9 | openapi: 3.0.2

      316 |     `);
      317 |
    > 318 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      319 |       <g>1 |</g> <g>info:</g>
      320 |       <g>2 |</g>   <g>license:</g>
      321 |       <g>3 |</g>     <r>name: MIT</r>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:318:36)

  ● codeframes › correctly generate code-frame for multiline-value and show only 5 lines with empty lines

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for multiline-value and show only 5 lines with empty lines 2`

    - Snapshot  - 6
    + Received  + 6

    - <g>2 |</g>
    - <g>3 |</g> <g>license:</g>
    - <g>4 |</g>   <r>name: MIT</r>
    - <g>5 |</g>
    - <g>… |</g>   <g>< 3 more lines ></g>
    - <g>9 |</g>   <r>field5: MIT</r>
    + 2 |
    + 3 | license:
    + 4 |   name: MIT
    + 5 |
    + … |   < 3 more lines >
    + 9 |   field5: MIT

      359 |     `);
      360 |
    > 361 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      362 |       <g>2 |</g>
      363 |       <g>3 |</g> <g>license:</g>
      364 |       <g>4 |</g>   <r>name: MIT</r>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:361:36)

  ● codeframes › correctly generate code-frame dedent

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame dedent 2`

    - Snapshot  - 5
    + Received  + 5

    - <g>10 |</g>   <g>parameters:</g>
    - <g>11 |</g>     <g>- name: a</g>
    - <g>12 |</g>       in: <r>wrong</r>
    - <g>13 |</g> <g>post:</g>
    - <g>14 |</g>   <g>operationId: test2</g>
    + 10 |   parameters:
    + 11 |     - name: a
    + 12 |       in: wrong
    + 13 | post:
    + 14 |   operationId: test2

      403 |     `);
      404 |
    > 405 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      406 |       <g>10 |</g>   <g>parameters:</g>
      407 |       <g>11 |</g>     <g>- name: a</g>
      408 |       <g>12 |</g>       in: <r>wrong</r>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:405:36)

  ● codeframes › correctly generate code-frame for too long line

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for too long line 2`

    - Snapshot  - 5
    + Received  + 5

    - <g>1 |</g> <g>openapi: 3.0.2</g>
    - <g>2 |</g> <g>info:</g>
    - <g>3 |</g>   description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolo<g>...<87 chars></g>
    - <g>4 |</g>   <g>license:</g>
    - <g>5 |</g>     <g>name: MIT</g>
    + 1 | openapi: 3.0.2
    + 2 | info:
    + 3 |   description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua...<77 chars>
    + 4 |   license:
    + 5 |     name: MIT

      438 |     `);
      439 |
    > 440 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      441 |       <g>1 |</g> <g>openapi: 3.0.2</g>
      442 |       <g>2 |</g> <g>info:</g>
      443 |       <g>3 |</g>   description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolo<g>...<87 chars></g>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:440:36)

  ● codeframes › correctly generate code-frame for empty file

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for empty file 2`

    - Snapshot  - 3
    + Received  + 3

    - <g>1 |</g>
    - <g>2 |</g>
    - <g>3 |</g>
    + 1 |
    + 2 |
    + 3 |

      461 |     `);
      462 |
    > 463 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      464 |       <g>1 |</g>
      465 |       <g>2 |</g>
      466 |       <g>3 |</g>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:463:36)

  ● codeframes › correctly generate code-frame for whole file and correctly skip newlines

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes correctly generate code-frame for whole file and correctly skip newlines 2`

    - Snapshot  - 4
    + Received  + 4

    - <g>1 |</g> <r>openapi: 3.0.2</r>
    - <g>2 |</g>
    - <g>… |</g> <g>< 5 more lines ></g>
    - <g>8 |</g>     <r>url: https://google.com</r>
    + 1 | openapi: 3.0.2
    + 2 |
    + … | < 5 more lines >
    + 8 |     url: https://google.com

      497 |     `);
      498 |
    > 499 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      500 |       <g>1 |</g> <r>openapi: 3.0.2</r>
      501 |       <g>2 |</g>
      502 |       <g>… |</g> <g>< 5 more lines ></g>

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:499:36)

  ● codeframes › should show line even if there are not so many file in the file

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `codeframes should show line even if there are not so many file in the file 2`

    - Snapshot  - 2
    + Received  + 2

    - <g>1 |</g> <g>openapi: 3.0.2</g>
    - <g>2 |</g>
    + 1 | openapi: 3.0.2
    + 2 |

      523 |     `);
      524 |
    > 525 |     expect(getColorCodeframe(loc)).toMatchInlineSnapshot(`
          |                                    ^
      526 |       <g>1 |</g> <g>openapi: 3.0.2</g>
      527 |       <g>2 |</g>
      528 |     `);

      at Object.<anonymous> (packages/core/src/__tests__/codeframes.test.ts:525:36)

```
---
```
  ● resolving a plugin › should prefix rule names with the plugin id

    expect(received).toHaveProperty(path)

    Matcher error: received value must not be null nor undefined

    Received has value: undefined

       9 |     const plugin = config.styleguide.plugins[0];
      10 |
    > 11 |     expect(plugin.rules?.oas3).toHaveProperty('test-plugin/openid-connect-url-well-known');
         |                                ^
      12 |   });
      13 |
      14 |   it('should prefix preprocessor names with the plugin id', async () => {

      at packages/core/src/config/__tests__/resolve-plugins.test.ts:11:32
      at fulfilled (packages/core/src/config/__tests__/resolve-plugins.test.ts:5:58)

  ● resolving a plugin › should prefix preprocessor names with the plugin id

    expect(received).toHaveProperty(path)

    Matcher error: received value must not be null nor undefined

    Received has value: undefined

      16 |     const plugin = config.styleguide.plugins[0];
      17 |
    > 18 |     expect(plugin.preprocessors?.oas2).toHaveProperty('test-plugin/description-preprocessor');
         |                                        ^
      19 |   });
      20 |
      21 |   it('should prefix decorator names with the plugin id', async () => {

      at packages/core/src/config/__tests__/resolve-plugins.test.ts:18:40
      at fulfilled (packages/core/src/config/__tests__/resolve-plugins.test.ts:5:58)

  ● resolving a plugin › should prefix decorator names with the plugin id

    expect(received).toHaveProperty(path)

    Matcher error: received value must not be null nor undefined

    Received has value: undefined

      23 |     const plugin = config.styleguide.plugins[0];
      24 |
    > 25 |     expect(plugin.decorators?.oas3).toHaveProperty('test-plugin/inject-x-stats');
         |                                     ^
      26 |   });
      27 | });
      28 |

      at packages/core/src/config/__tests__/resolve-plugins.test.ts:25:37
      at fulfilled (packages/core/src/config/__tests__/resolve-plugins.test.ts:5:58)

```

</details>


<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from 285edb9b643e0ea727300449be30625072f5634a</p>


Patch:
diff --git a/.prettierignore b/.prettierignore
index 9b3cf5ee..cbdd7a28 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,4 +1,5 @@
 coverage/
+dist/
 packages/cli/lib/
 packages/core/lib/
 *snapshot.js
diff --git a/jest.config.js b/jest.config.js
index e7ea558d..5f99a47f 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -2,6 +2,7 @@ module.exports = {
   clearMocks: true,
   restoreMocks: true,
   preset: 'ts-jest',
+  testEnvironment: 'node',
   collectCoverageFrom: [
     'packages/*/src/**/*.ts',
     '!packages/**/__tests__/**/*',
diff --git a/packages/core/src/config/config-resolvers.ts b/packages/core/src/config/config-resolvers.ts
index f501d02b..52c0696d 100644
--- a/packages/core/src/config/config-resolvers.ts
+++ b/packages/core/src/config/config-resolvers.ts
@@ -1,5 +1,4 @@
 import * as path from 'path';
-import { blue, red } from 'colorette';
 import { isAbsoluteUrl } from '../ref-utils';
 import { BaseResolver } from '../resolve';
 import { defaultPlugin } from './builtIn';
@@ -21,8 +20,10 @@ import type {
   RuleConfig,
   DeprecatedInRawConfig,
 } from './types';
+import { isBrowser } from '../env';
 import { isNotString, isString, notUndefined, parseYaml } from '../utils';
 import { Config } from './config';
+import { colorize, logger } from '../logger';
 
 export async function resolveConfig(rawConfig: RawConfig, configPath?: string): Promise<Config> {
   if (rawConfig.styleguide?.extends?.some(isNotString)) {
@@ -71,35 +72,57 @@ export function resolvePlugins(
 ): Plugin[] {
   if (!plugins) return [];
 
-  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-  // @ts-ignore
-  const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require;
+  // TODO: implement or reuse Resolver approach so it will work in node and browser envs
+  const requireFunc = (plugin: string | Plugin): Plugin | undefined => {
+    if (isBrowser && isString(plugin)) {
+      logger.error(`Cannot load ${plugin}. Plugins aren't supported in browser yet.`);
+
+      return undefined;
+    }
+
+    if (isString(plugin)) {
+      const absoltePluginPath = path.resolve(path.dirname(configPath), plugin);
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-ignore
+      return typeof __webpack_require__ === 'function'
+        ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+          // @ts-ignore
+          __non_webpack_require__(absoltePluginPath)
+        : require(absoltePluginPath);
+    }
+
+    return plugin;
+  };
 
   const seenPluginIds = new Map<string, string>();
 
   return plugins
     .map((p) => {
       if (isString(p) && isAbsoluteUrl(p)) {
-        throw new Error(red(`We don't support remote plugins yet.`));
+        throw new Error(colorize.red(`We don't support remote plugins yet.`));
       }
 
       // TODO: resolve npm packages similar to eslint
-      const pluginModule = isString(p)
-        ? (requireFunc(path.resolve(path.dirname(configPath), p)) as Plugin)
-        : p;
+      const pluginModule = requireFunc(p);
+
+      if (!pluginModule) {
+        return;
+      }
 
       const id = pluginModule.id;
       if (typeof id !== 'string') {
-        throw new Error(red(`Plugin must define \`id\` property in ${blue(p.toString())}.`));
+        throw new Error(
+          colorize.red(`Plugin must define \`id\` property in ${colorize.blue(p.toString())}.`)
+        );
       }
 
       if (seenPluginIds.has(id)) {
         const pluginPath = seenPluginIds.get(id)!;
         throw new Error(
-          red(
-            `Plugin "id" must be unique. Plugin ${blue(p.toString())} uses id "${blue(
-              id
-            )}" already seen in ${blue(pluginPath)}`
+          colorize.red(
+            `Plugin "id" must be unique. Plugin ${colorize.blue(
+              p.toString()
+            )} uses id "${colorize.blue(id)}" already seen in ${colorize.blue(pluginPath)}`
           )
         );
       }
@@ -285,17 +308,19 @@ export function resolvePreset(presetName: string, plugins: Plugin[]): ResolvedSt
   const { pluginId, configName } = parsePresetName(presetName);
   const plugin = plugins.find((p) => p.id === pluginId);
   if (!plugin) {
-    throw new Error(`Invalid config ${red(presetName)}: plugin ${pluginId} is not included.`);
+    throw new Error(
+      `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`
+    );
   }
 
   const preset = plugin.configs?.[configName];
   if (!preset) {
     throw new Error(
       pluginId
-        ? `Invalid config ${red(
+        ? `Invalid config ${colorize.red(
             presetName
           )}: plugin ${pluginId} doesn't export config with name ${configName}.`
-        : `Invalid config ${red(presetName)}: there is no such built-in config.`
+        : `Invalid config ${colorize.red(presetName)}: there is no such built-in config.`
     );
   }
   return preset;
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 26ab4bee..34589f0e 100755
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -4,6 +4,7 @@ import { parseYaml, stringifyYaml } from '../js-yaml';
 import { slash, doesYamlFileExist } from '../utils';
 import { NormalizedProblem } from '../walk';
 import { OasVersion, OasMajorVersion, Oas2RuleSet, Oas3RuleSet } from '../oas-types';
+import { env } from '../env';
 
 import type { NodeType } from '../types';
 import type {
@@ -19,9 +20,6 @@ import type {
 } from './types';
 import { getResolveConfig } from './utils';
 
-// Alias environment here so this file can work in browser environments too.
-export const env = typeof process !== 'undefined' ? process.env || {} : {};
-
 export const IGNORE_FILE = '.redocly.lint-ignore.yaml';
 const IGNORE_BANNER =
   `# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.\n` +
diff --git a/packages/core/src/config/utils.ts b/packages/core/src/config/utils.ts
index 8d6a0cc0..58590956 100644
--- a/packages/core/src/config/utils.ts
+++ b/packages/core/src/config/utils.ts
@@ -1,4 +1,3 @@
-import { yellow } from 'colorette';
 import {
   assignExisting,
   isTruthy,
@@ -18,6 +17,7 @@ import type {
   ResolvedStyleguideConfig,
   RulesFields,
 } from './types';
+import { logger, colorize } from '../logger';
 
 export function parsePresetName(presetName: string): { pluginId: string; configName: string } {
   if (presetName.indexOf('/') > -1) {
@@ -222,7 +222,7 @@ export function getUniquePlugins(plugins: Plugin[]): Plugin[] {
       results.push(p);
       seen.add(p.id);
     } else if (p.id) {
-      process.stderr.write(`Duplicate plugin id "${yellow(p.id)}".\n`);
+      logger.warn(`Duplicate plugin id "${colorize.red(p.id)}".\n`);
     }
   }
   return results;
diff --git a/packages/core/src/env.ts b/packages/core/src/env.ts
new file mode 100644
index 00000000..62a7587d
--- /dev/null
+++ b/packages/core/src/env.ts
@@ -0,0 +1,5 @@
+export const isBrowser =
+  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+  // @ts-ignore
+  typeof window !== 'undefined' || typeof self !== 'undefined' || typeof process === 'undefined'; // main and worker thread
+export const env = isBrowser ? {} : process.env || {};
diff --git a/packages/core/src/format/codeframes.ts b/packages/core/src/format/codeframes.ts
index 93ee7151..d25c2325 100644
--- a/packages/core/src/format/codeframes.ts
+++ b/packages/core/src/format/codeframes.ts
@@ -1,7 +1,7 @@
-import { gray, red, options as colorOptions } from 'colorette';
 import * as yamlAst from 'yaml-ast-parser';
 import { unescapePointer } from '../ref-utils';
 import { LineColLocationObject, Loc, LocationObject } from '../walk';
+import { colorize, colorOptions } from '../logger';
 
 type YAMLMapping = yamlAst.YAMLMapping & { kind: yamlAst.Kind.MAPPING };
 type YAMLMap = yamlAst.YamlMap & { kind: yamlAst.Kind.MAP };
@@ -39,14 +39,20 @@ export function getCodeframe(location: LineColLocationObject, color: boolean) {
     const startIdx = i === startLineNum ? start.col - 1 : currentPad;
     const endIdx = i === endLineNum ? end.col - 1 : line.length;
 
-    prefixedLines.push([`${i}`, markLine(line, startIdx, endIdx, red)]);
+    prefixedLines.push([`${i}`, markLine(line, startIdx, endIdx, colorize.red)]);
     if (!color) prefixedLines.push(['', underlineLine(line, startIdx, endIdx)]);
   }
 
   if (skipLines > 0) {
-    prefixedLines.push([`…`, `${whitespace(currentPad)}${gray(`< ${skipLines} more lines >`)}`]);
+    prefixedLines.push([
+      `…`,
+      `${whitespace(currentPad)}${colorize.gray(`< ${skipLines} more lines >`)}`,
+    ]);
     // print last line
-    prefixedLines.push([`${endLineNum}`, markLine(lines[endLineNum - 1], -1, end.col - 1, red)]);
+    prefixedLines.push([
+      `${endLineNum}`,
+      markLine(lines[endLineNum - 1], -1, end.col - 1, colorize.red),
+    ]);
 
     if (!color) prefixedLines.push(['', underlineLine(lines[endLineNum - 1], -1, end.col - 1)]);
   }
@@ -63,7 +69,7 @@ export function getCodeframe(location: LineColLocationObject, color: boolean) {
     line: string,
     startIdx: number = -1,
     endIdx: number = +Infinity,
-    variant = gray
+    variant = colorize.gray
   ) {
     if (!color) return line;
     if (!line) return line;
@@ -90,7 +96,7 @@ function printPrefixedLines(lines: [string, string][]): string {
   return existingLines
     .map(
       ([prefix, line]) =>
-        gray(leftPad(padLen, prefix) + ' |') +
+        colorize.gray(leftPad(padLen, prefix) + ' |') +
         (line ? ' ' + limitLineLength(line.substring(dedentLen)) : '')
     )
     .join('\n');
@@ -99,7 +105,7 @@ function printPrefixedLines(lines: [string, string][]): string {
 function limitLineLength(line: string, maxLen: number = MAX_LINE_LENGTH) {
   const overflowLen = line.length - maxLen;
   if (overflowLen > 0) {
-    const charsMoreText = gray(`...<${overflowLen} chars>`);
+    const charsMoreText = colorize.gray(`...<${overflowLen} chars>`);
     return line.substring(0, maxLen - charsMoreText.length) + charsMoreText;
   } else {
     return line;
diff --git a/packages/core/src/format/format.ts b/packages/core/src/format/format.ts
index d9733c32..05e5af87 100644
--- a/packages/core/src/format/format.ts
+++ b/packages/core/src/format/format.ts
@@ -1,20 +1,12 @@
 import * as path from 'path';
-import {
-  options as colorOptions,
-  gray,
-  blue,
-  bgRed,
-  bgYellow,
-  black,
-  yellow,
-  red,
-} from 'colorette';
+import { colorOptions, colorize, logger } from '../logger';
+import { output } from '../output';
 
 const coreVersion = require('../../package.json').version;
 
 import { NormalizedProblem, ProblemSeverity, LineColLocationObject, LocationObject } from '../walk';
 import { getCodeframe, getLineColLocation } from './codeframes';
-import { env } from '../config';
+import { env } from '../env';
 
 export type Totals = {
   errors: number;
@@ -27,13 +19,13 @@ const ERROR_MESSAGE = {
 };
 
 const BG_COLORS = {
-  warn: (str: string) => bgYellow(black(str)),
-  error: bgRed,
+  warn: (str: string) => colorize.bgYellow(colorize.black(str)),
+  error: colorize.bgRed,
 };
 
 const COLORS = {
-  warn: yellow,
-  error: red,
+  warn: colorize.yellow,
+  error: colorize.red,
 };
 
 const SEVERITY_NAMES = {
@@ -114,7 +106,7 @@ export function formatProblems(
     case 'codeframe':
       for (let i = 0; i < problems.length; i++) {
         const problem = problems[i];
-        process.stderr.write(`${formatCodeframe(problem, i)}\n`);
+        logger.info(`${formatCodeframe(problem, i)}\n`);
       }
       break;
     case 'stylish': {
@@ -122,30 +114,30 @@ export function formatProblems(
       for (const [file, { ruleIdPad, locationPad: positionPad, fileProblems }] of Object.entries(
         groupedByFile
       )) {
-        process.stderr.write(`${blue(path.relative(cwd, file))}:\n`);
+        logger.info(`${colorize.blue(path.relative(cwd, file))}:\n`);
 
         for (let i = 0; i < fileProblems.length; i++) {
           const problem = fileProblems[i];
-          process.stderr.write(`${formatStylish(problem, positionPad, ruleIdPad)}\n`);
+          logger.info(`${formatStylish(problem, positionPad, ruleIdPad)}\n`);
         }
 
-        process.stderr.write('\n');
+        logger.info('\n');
       }
       break;
     }
     case 'checkstyle': {
       const groupedByFile = groupByFiles(problems);
 
-      process.stdout.write('<?xml version="1.0" encoding="UTF-8"?>\n');
-      process.stdout.write('<checkstyle version="4.3">\n');
+      output.write('<?xml version="1.0" encoding="UTF-8"?>\n');
+      output.write('<checkstyle version="4.3">\n');
 
       for (const [file, { fileProblems }] of Object.entries(groupedByFile)) {
-        process.stdout.write(`<file name="${xmlEscape(path.relative(cwd, file))}">\n`);
+        output.write(`<file name="${xmlEscape(path.relative(cwd, file))}">\n`);
         fileProblems.forEach(formatCheckstyle);
-        process.stdout.write(`</file>\n`);
+        output.write(`</file>\n`);
       }
 
-      process.stdout.write(`</checkstyle>\n`);
+      output.write(`</checkstyle>\n`);
       break;
     }
     case 'codeclimate':
@@ -154,8 +146,8 @@ export function formatProblems(
   }
 
   if (totalProblems - ignoredProblems > maxProblems) {
-    process.stderr.write(
-      `< ... ${totalProblems - maxProblems} more problems hidden > ${gray(
+    logger.info(
+      `< ... ${totalProblems - maxProblems} more problems hidden > ${colorize.gray(
         'increase with `--max-problems N`'
       )}\n`
     );
@@ -177,7 +169,7 @@ export function formatProblems(
         fingerprint: `${p.ruleId}${p.location.length > 0 ? '-' + p.location[0].pointer : ''}`,
       };
     });
-    process.stdout.write(JSON.stringify(issues, null, 2));
+    output.write(JSON.stringify(issues, null, 2));
   }
 
   function outputJSON() {
@@ -211,7 +203,7 @@ export function formatProblems(
         return problem;
       }),
     };
-    process.stdout.write(JSON.stringify(resultObject, null, 2));
+    output.write(JSON.stringify(resultObject, null, 2));
   }
 
   function getBgColor(problem: NormalizedProblem) {
@@ -227,7 +219,7 @@ export function formatProblems(
     const location = problem.location[0]; // TODO: support multiple locations
     const relativePath = path.relative(cwd, location.source.absoluteRef);
     const loc = getLineColLocation(location);
-    const atPointer = location.pointer ? gray(`at ${location.pointer}`) : '';
+    const atPointer = location.pointer ? colorize.gray(`at ${location.pointer}`) : '';
     const fileWithLoc = `${relativePath}:${loc.start.line}:${loc.start.col}`;
     return (
       `[${idx + 1}] ${bgColor(fileWithLoc)} ${atPointer}\n\n` +
@@ -236,7 +228,9 @@ export function formatProblems(
       getCodeframe(loc, color) +
       '\n\n' +
       formatFrom(cwd, problem.from) +
-      `${SEVERITY_NAMES[problem.severity]} was generated by the ${blue(problem.ruleId)} rule.\n\n`
+      `${SEVERITY_NAMES[problem.severity]} was generated by the ${colorize.blue(
+        problem.ruleId
+      )} rule.\n\n`
     );
   }
 
@@ -257,7 +251,7 @@ export function formatProblems(
     const severity = problem.severity == 'warn' ? 'warning' : 'error';
     const message = xmlEscape(problem.message);
     const source = xmlEscape(problem.ruleId);
-    process.stdout.write(
+    output.write(
       `<error line="${line}" column="${col}" severity="${severity}" message="${message}" source="${source}" />\n`
     );
   }
@@ -269,7 +263,7 @@ function formatFrom(cwd: string, location?: LocationObject) {
   const loc = getLineColLocation(location);
   const fileWithLoc = `${relativePath}:${loc.start.line}:${loc.start.col}`;
 
-  return `referenced from ${blue(fileWithLoc)}\n\n`;
+  return `referenced from ${colorize.blue(fileWithLoc)}\n\n`;
 }
 
 function formatDidYouMean(problem: NormalizedProblem) {
diff --git a/packages/core/src/logger.ts b/packages/core/src/logger.ts
new file mode 100644
index 00000000..08926ce1
--- /dev/null
+++ b/packages/core/src/logger.ts
@@ -0,0 +1,34 @@
+import * as colorette from 'colorette';
+export { options as colorOptions } from 'colorette';
+
+import { isBrowser } from './env';
+import { identity } from './utils';
+
+export const colorize = new Proxy(colorette, {
+  get(target: typeof colorette, prop: string): typeof identity {
+    if (isBrowser) {
+      return identity;
+    }
+
+    return (target as any)[prop];
+  },
+});
+class Logger {
+  protected stderr(str: string) {
+    return process.stderr.write(str);
+  }
+
+  info(str: string) {
+    return isBrowser ? console.log(str) : this.stderr(str);
+  }
+
+  warn(str: string) {
+    return isBrowser ? console.warn(str) : this.stderr(colorize.yellow(str));
+  }
+
+  error(str: string) {
+    return isBrowser ? console.error(str) : this.stderr(colorize.red(str));
+  }
+}
+
+export const logger = new Logger();
diff --git a/packages/core/src/output.ts b/packages/core/src/output.ts
new file mode 100644
index 00000000..1583dd7e
--- /dev/null
+++ b/packages/core/src/output.ts
@@ -0,0 +1,7 @@
+import { isBrowser } from './env';
+
+export const output = {
+  write(str: string) {
+    return isBrowser ? undefined : process.stdout.write(str);
+  },
+};
diff --git a/packages/core/src/redocly/index.ts b/packages/core/src/redocly/index.ts
index c9664e91..b0571ff5 100644
--- a/packages/core/src/redocly/index.ts
+++ b/packages/core/src/redocly/index.ts
@@ -1,11 +1,12 @@
 import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
 import { resolve } from 'path';
 import { homedir } from 'os';
-import { green } from 'colorette';
 import { RegistryApi } from './registry-api';
-import { DEFAULT_REGION, DOMAINS, AVAILABLE_REGIONS, env } from '../config/config';
+import { DEFAULT_REGION, DOMAINS, AVAILABLE_REGIONS } from '../config/config';
+import { env } from '../env';
 import { RegionalToken, RegionalTokenWithValidity } from './redocly-client-types';
 import { isNotEmptyObject } from '../utils';
+import { colorize } from '../logger';
 
 import type { AccessTokens, Region } from '../config/types';
 
@@ -29,7 +30,9 @@ export class RedoclyClient {
   loadRegion(region?: Region) {
     if (region && !DOMAINS[region]) {
       throw new Error(
-        `Invalid argument: region in config file.\nGiven: ${green(region)}, choices: "us", "eu".`
+        `Invalid argument: region in config file.\nGiven: ${colorize.green(
+          region
+        )}, choices: "us", "eu".`
       );
     }
 
diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts
index e3c9f87d..98c46742 100644
--- a/packages/core/src/utils.ts
+++ b/packages/core/src/utils.ts
@@ -6,8 +6,8 @@ import * as pluralize from 'pluralize';
 import { parseYaml } from './js-yaml';
 import { UserContext } from './walk';
 import { HttpResolveConfig } from './config';
-import { env } from './config';
-import { green, yellow } from 'colorette';
+import { env } from './env';
+import { logger, colorize } from './logger';
 
 export { parseYaml, stringifyYaml } from './js-yaml';
 
@@ -207,8 +207,8 @@ export function doesYamlFileExist(filePath: string): boolean {
 }
 
 export function showWarningForDeprecatedField(deprecatedField: string, updatedField: string) {
-  process.stderr.write(
-    `The ${yellow(deprecatedField)} field is deprecated. Use ${green(
+  logger.warn(
+    `The ${colorize.red(deprecatedField)} field is deprecated. Use ${colorize.green(
       updatedField
     )} instead. Read more about this change: https://redocly.com/docs/api-registry/guides/migration-guide-config-file/#changed-properties\n`
   );
@@ -223,3 +223,7 @@ export type Falsy = undefined | null | false | '' | 0;
 export function isTruthy<Truthy>(value: Truthy | Falsy): value is Truthy {
   return !!value;
 }
+
+export function identity<T>(value: T): T {
+  return value;
+}


Test patch:
diff --git a/packages/core/src/__tests__/logger-browser.test.ts b/packages/core/src/__tests__/logger-browser.test.ts
new file mode 100644
index 00000000..15de710a
--- /dev/null
+++ b/packages/core/src/__tests__/logger-browser.test.ts
@@ -0,0 +1,53 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import * as colorette from 'colorette';
+import { logger, colorize } from '../logger';
+
+describe('Logger in Browser', () => {
+  it('should call "console.error"', () => {
+    const error = jest.spyOn(console, 'error').mockImplementation();
+
+    logger.error('error');
+
+    expect(error).toBeCalledTimes(1);
+    expect(error).toBeCalledWith('error');
+
+    error.mockRestore();
+  });
+
+  it('should call "console.log"', () => {
+    const log = jest.spyOn(console, 'log').mockImplementation();
+
+    logger.info('info');
+
+    expect(log).toBeCalledTimes(1);
+    expect(log).toBeCalledWith('info');
+
+    log.mockRestore();
+  });
+
+  it('should call "console.warn"', () => {
+    const warn = jest.spyOn(console, 'warn').mockImplementation();
+
+    logger.warn('warn');
+
+    expect(warn).toBeCalledTimes(1);
+    expect(warn).toBeCalledWith('warn');
+
+    warn.mockRestore();
+  });
+});
+
+describe('colorize in Browser', () => {
+  it('should not call original colorette lib', () => {
+    const color = 'cyan';
+    const spyingCyan = jest.spyOn(colorette, color);
+
+    const colorized = colorize.cyan(color);
+
+    expect(spyingCyan).not.toBeCalled();
+    expect(colorized).toEqual(color);
+  });
+});
diff --git a/packages/core/src/__tests__/logger.test.ts b/packages/core/src/__tests__/logger.test.ts
new file mode 100644
index 00000000..ed5365d6
--- /dev/null
+++ b/packages/core/src/__tests__/logger.test.ts
@@ -0,0 +1,47 @@
+import * as colorette from 'colorette';
+import { logger, colorize } from '../logger';
+
+describe('Logger in nodejs', () => {
+  let spyingStderr: jest.SpyInstance;
+
+  beforeEach(() => {
+    spyingStderr = jest.spyOn(process.stderr, 'write').mockImplementation();
+  });
+
+  afterEach(() => {
+    spyingStderr.mockRestore();
+  });
+
+  it('should call "process.stderr.write" for error severity', () => {
+    logger.error('error');
+
+    expect(spyingStderr).toBeCalledTimes(1);
+    expect(spyingStderr).toBeCalledWith(colorette.red('error'));
+  });
+
+  it('should call "process.stderr.write" for warn severity', () => {
+    logger.warn('warn');
+
+    expect(spyingStderr).toBeCalledTimes(1);
+    expect(spyingStderr).toBeCalledWith(colorette.yellow('warn'));
+  });
+
+  it('should call "process.stderr.write" for info severity', () => {
+    logger.info('info');
+
+    expect(spyingStderr).toBeCalledTimes(1);
+    expect(spyingStderr).toBeCalledWith('info');
+  });
+});
+
+describe('colorize in nodejs', () => {
+  it('should call original colorette lib', () => {
+    const color = 'cyan';
+    const spyingCyan = jest.spyOn(colorette, color);
+
+    const colorized = colorize.cyan(color);
+
+    expect(spyingCyan).toBeCalledWith(color);
+    expect(colorized).toEqual(colorette[color](color));
+  });
+});
diff --git a/packages/core/src/__tests__/output-browser.test.ts b/packages/core/src/__tests__/output-browser.test.ts
new file mode 100644
index 00000000..79380f9f
--- /dev/null
+++ b/packages/core/src/__tests__/output-browser.test.ts
@@ -0,0 +1,18 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { output } from '../output';
+
+describe('output', () => {
+  it('should ignore all parsable data in browser', () => {
+    const spyingStdout = jest.spyOn(process.stdout, 'write').mockImplementation();
+    const data = '{ "errors" : [] }';
+
+    output.write(data);
+
+    expect(spyingStdout).not.toBeCalled();
+
+    spyingStdout.mockRestore();
+  });
+});
diff --git a/packages/core/src/__tests__/output.test.ts b/packages/core/src/__tests__/output.test.ts
new file mode 100644
index 00000000..f080c267
--- /dev/null
+++ b/packages/core/src/__tests__/output.test.ts
@@ -0,0 +1,15 @@
+import { output } from '../output';
+
+describe('output', () => {
+  it('should write all parsable data to stdout', () => {
+    const spyingStdout = jest.spyOn(process.stdout, 'write').mockImplementation();
+    const data = '{ "errors" : [] }';
+
+    output.write(data);
+
+    expect(spyingStdout).toBeCalledTimes(1);
+    expect(spyingStdout).toBeCalledWith(data);
+
+    spyingStdout.mockRestore();
+  });
+});
diff --git a/packages/core/src/__tests__/utils-browser.test.ts b/packages/core/src/__tests__/utils-browser.test.ts
new file mode 100644
index 00000000..eaa874b1
--- /dev/null
+++ b/packages/core/src/__tests__/utils-browser.test.ts
@@ -0,0 +1,11 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { isBrowser } from '../env';
+
+describe('isBrowser', () => {
+  it('should be browser', () => {
+    expect(isBrowser).toBe(true);
+  });
+});
diff --git a/packages/core/src/__tests__/utils.test.ts b/packages/core/src/__tests__/utils.test.ts
index 8bdef49c..34c0cb4c 100644
--- a/packages/core/src/__tests__/utils.test.ts
+++ b/packages/core/src/__tests__/utils.test.ts
@@ -5,6 +5,7 @@ import {
   getMatchingStatusCodeRange,
   doesYamlFileExist,
 } from '../utils';
+import { isBrowser } from '../env';
 import * as fs from 'fs';
 import * as path from 'path';
 
@@ -122,5 +123,11 @@ describe('utils', () => {
         expect(doesYamlFileExist('redocly.yam')).toBe(false);
       });
     });
+
+    describe('isBrowser', () => {
+      it('should not be browser', () => {
+        expect(isBrowser).toBe(false);
+      });
+    });
   });
 });