Repository: Redocly/openapi-cli
Pull request #366 (2021-09-13 12:22:52)

chore: v1.0.0-beta.59
## What/Why/How?
Up version to v1.0.0-beta.59

## Reference

## Testing

## Screenshots (optional)

## Check yourself

- [x] Code is linted
- [x] 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



Patch:
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..2698675d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,6 @@
+*
+!package.json
+!tsconfig.build.json
+!tsconfig.json
+!package-lock.json
+!packages/*
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index fe5c5e1b..ffe7490e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,16 +2,30 @@ FROM node:alpine
 
 WORKDIR /build
 
-COPY webpack.config.ts tsconfig.json tsconfig.build.json /build/
+# Copy files required for npm install only
 COPY package.json package-lock.json /build/
-COPY packages /build/packages
+COPY packages/cli/package.json /build/packages/cli/
+COPY packages/core/package.json   /build/packages/core/
+COPY packages/cli/bin/ /build/packages/cli/bin/
 
-RUN npm ci --no-optional
-RUN npm run webpack-bundle
+RUN npm ci --no-optional --ignore-scripts
 
-FROM node:alpine
+# Copy rest of the files
+COPY . /build/
+RUN npm run prepare
+
+# Install openapi-cli globally, similar to npm install --global @redocly/openapi-cli
+# but the local package is used here
+RUN mv -- "$(npm pack packages/cli/)" redocly-openapi-cli.tgz && \
+	  npm install --global redocly-openapi-cli.tgz
+
+# npm pack in the previous RUN command does not include these assets
+RUN cp packages/cli/src/commands/preview-docs/preview-server/default.hbs /usr/local/lib/node_modules/@redocly/openapi-cli/lib/commands/preview-docs/preview-server/default.hbs && \
+	  cp packages/cli/src/commands/preview-docs/preview-server/hot.js /usr/local/lib/node_modules/@redocly/openapi-cli/lib/commands/preview-docs/preview-server/hot.js
+
+# Clean up to reduce image size
+RUN npm cache clean --force && rm -rf /build
 
 WORKDIR /spec
-COPY --from=0 /build/dist/bundle.js /bin/openapi-cli
 
-ENTRYPOINT [ "node", "/bin/openapi-cli"]
+ENTRYPOINT [ "openapi" ]
diff --git a/docs/commands/index.md b/docs/commands/index.md
index 5f80152a..4d5b92cf 100644
--- a/docs/commands/index.md
+++ b/docs/commands/index.md
@@ -5,22 +5,27 @@ tocMaxDepth: 2
 
 Redocly OpenAPI CLI currently supports the following commands:
 
-- [bundle](bundle.md)
-- [join](join.md)
-- [lint](lint.md)
-- [login](login.md)
-- [logout](logout.md)
-- [preview-docs](preview-docs.md)
-- [push](push.md)
-- [split](split.md)
-- [stats](stats.md)
+* [bundle](bundle.md)
+* [join](join.md)
+* [lint](lint.md) 
+* [login](login.md)
+* [logout](logout.md)
+* [preview-docs](preview-docs.md)
+* [push](push.md)
+* [split](split.md)
+* [stats](stats.md)
 
+To define the behavior of the commands, you can use configuration files:
 
-The following configuration files define the behavior of the CLI tool:
+- `.redocly.yaml` - define the location of your root files, linting rules, and reference docs configuration information.
+- `.redocly.lint-ignore.yaml` - ignore specific lint messages.
 
-- `.redocly.yaml` - used to define the location of your root files, linting rules, and reference docs configuration information.
-- `.redocly.lint-ignore.yaml` - used to ignore specific lint messages.
+For more information, refer to the [configuration section](../configuration/index.mdx).
 
-The CLI tool looks for configuration files in the current working directory. If it detects them, it will use the options set in those configuration files for the commands. Learn more about the [configuration file structure and options](../configuration/index.mdx).
+:::attention Note
+
+The CLI tool looks for configuration files in the current working directory. If it detects them, it will use the options set in those configuration files for the commands. Learn more about the [configuration file structure and options](../configuration/configuration-file.mdx).
 
 When executing any of the commands, you can override the default configuration file by providing a path to another configuration file with the `--config` option.
+
+:::
\ No newline at end of file
diff --git a/docs/commands/join.md b/docs/commands/join.md
index 01de5d30..1ca91683 100644
--- a/docs/commands/join.md
+++ b/docs/commands/join.md
@@ -1,59 +1,70 @@
+---
+tocMaxDepth: 3
+---
 # `join`
 
-<div class="warning"><b>Important</b>
+## Introduction
 
+:::warning Important
 The `join` command is considered an experimental feature. This means it's still a work in progress and may go through major changes.
-</div>
 
+The `join` command supports OpenAPI 3.x definitions only.
+:::
 
 Maintainers of multiple API definitions can benefit from storing each endpoint as a standalone API definition file. However, this approach is not supported by the majority of OpenAPI tools, as they require a single API definition file.
 
-Redocly OpenAPI CLI can combine two or more API definition files into one. The resulting file optionally helps distinguish the origin of OpenAPI objects and properties by appending custom prefixes to them.
+With Redocly OpenAPI CLI, you can solve this problem by using the `join` command that can combine two or more API definition files into a single one.
 
-The `join` command accepts YAML and JSON files, and you can mix those two file types when running the command. The resulting file is always called `openapi.yaml`, as setting a custom name is not supported. If a file called `openapi.yaml` exists in the working directory, it will be overwritten.
+To easily distinguish the origin of OpenAPI objects and properties, you can optionally instruct the `join` command to append custom prefixes to them.
 
-Apart from providing individual API definition files as the input, you can specify the path to a folder that contains multiple API definition files and match them with a wildcard (for example, `myproject/openapi/*.yaml`). The `join` command collects all matching files and combines them into one file.
+The `join` command accepts both YAML and JSON files, which you can mix in the resulting `openapi.yaml` file. Setting a custom name for this file is not supported, which also means that if there is a file called `openapi.yaml` in the working directory, it will be overwritten.
 
-Only OpenAPI 3.x definitions are supported by this command.
+Apart from providing individual API definition files as the input, you can also specify the path to a folder that contains multiple API definition files and match them with a wildcard (for example, `myproject/openapi/*.yaml`). The `join` command will collect all matching files and combine them into one file.
 
+### Usage
 
-### `join` usage
-
+```bash
+openapi join <entrypoint> <entrypoint>...
+openapi join <path-to-folder>/<wildcard-pattern> [--lint]
+openapi join [--help] [--prefix-components-with-info-prop] [--prefix-tags-with-info-prop] [--prefix-tags-with-filename]
 
-```
-Positionals:
-entrypoints                                              [array] [default: []]
-
-Options:
---version                           Show version number.             [boolean]
---help                              Show help.                       [boolean]
---lint                              Lint definitions[boolean] [default: false]
---prefix-tags-with-info-prop        Prefix tags with property value from info
-object                            [string]
---prefix-tags-with-filename         Prefix tags with property value from file
-name            [boolean] [default: false]
---prefix-components-with-info-prop  Prefix components with property value from
-info object                       [string]
+openapi join first-api.yaml second-api.yaml
+openapi join first-api.yaml second-api.json
+openapi join ./*.yaml
+openapi join --version
 ```
 
+## Options
 
-The command:
+Option                               | Type               | Required     | Default     | Description
+-------------------------------------|:------------------:|:------------:|:-----------:|------------
+`entrypoints`                        | `array`, wildcard  | yes          | -           | 1. Array of paths to API definition files that you want to join. At least two input files are required.<br />2. A wildcard pattern to match API definition files within a specific folder
+`--help`                             | `boolean`          | no           | -           | Show help
+`--lint`                             | `boolean`          | no           | `false`     | Lint definition files
+`--prefix-components-with-info-prop` | `string`           | no           | -           | Prefix components with property value from info object. See the [prefix-components-with-info-prop section](#prefix-components-with-info-prop) below
+`--prefix-tags-with-info-prop`       | `string`           | no           | -           | Prefix tags with property value from info object. See the [prefix-tags-with-info-prop section](#prefix-tags-with-info-prop) below
+`--prefix-tags-with-filename`        | `boolean`          | no           | `false`     | Prefix tags with property value from file name. See the [prefix-tags-with-filename section](#prefix-tags-with-filename) below
+`--version`                          | `boolean`          | no           | -           | Show version number
 
+## Examples
 
-```bash
+### Array of paths
+
+```bash request
 openapi join first-api.yaml second-api.json
 ```
 
+```bash output
+openapi join first-api.yaml second-api.json
 
-The command creates the output file `openapi.yaml` in the working directory.
-
-At least two input files are required.
+openapi.yaml: join processed in 56ms
+```
 
-The order of input files affects how their content is processed. The first provided file is always treated as the "main" file, and its content has precedence over other input files when combining them.
+The command creates the output `openapi.yaml` file in the working directory.
 
-More specifically, the following properties of the API definition are always taken only from the first input file:
+The order of input files affects how their content is processed. The first provided file is always treated as the "main" file, and its content has precedence over other input files when combining them. Specifically, the following properties of the API definition are always taken only from the first input file:
 
-```
+```yaml
 info:
   version
   title
@@ -63,13 +74,11 @@ info:
 externalDocs
 ```
 
+By default, the `info.description` property is taken only from the first file, even if it exists in other input files.
 
-By default, the `info: description` property is taken only from the first file, even if it exists in other input files.
-
-However, if any of the input files contains the `tags` object, the `join` command automatically creates the `x-tagGroups` object in the output file. This object contains the `info: description` property from each of the input files.
-
+However, if any of the input files contain the `tags` object, the `join` command automatically creates the `x-tagGroups` object in the output file. This object contains the `info.description` property from each of the input files.
 
-```
+```yaml
 x-tagGroups:
   - name: first-api
     tags:
@@ -82,10 +91,10 @@ x-tagGroups:
     description: "Text from info: description of the second input file"
 ```
 
-
-If some operations in an input file don't have a tag assigned to them, the `join` command automatically adds the `other` tag to those operations in the output file. The `other` tag is also included in the `x-tagGroups` object.
-
-If any of the input files contains the `x-tagGroups` object, the content of this object is ignored by the `join` command, and not included in the output file.
+:::info
+* If some operations in an input file don't have a tag assigned to them, the `join` command automatically adds the `other` tag to those operations in the output file. The `other` tag is also included in the `x-tagGroups` object.
+* If any of the input files contain the `x-tagGroups` object, the content of this object is ignored by the `join` command and not included in the output file.
+:::
 
 The `servers` object combines the content from all input files, starting with the content from the first file. Commented lines are not included in the output file.
 
@@ -93,87 +102,78 @@ Path names and component names must be unique in all input files, but their cont
 
 If the `join` command detects any conflicting content while trying to combine the input files, it displays informative messages about the conflicts and exits without creating an output file. To prevent this, use optional parameters to add prefixes to tags and components.
 
+```bash example of conflicts
+Conflict on tags => all : pets in files: petstore.yaml,test.yaml 
 
-### Options
+    1 conflict(s) on tags.
+    Suggestion: please use prefix-tags-with-filename or prefix-tags-with-info-prop to prevent naming conflicts. 
 
-The following optional parameters are supported by the `join` command.
+Conflict on paths => /pets : get in files: petstore.yaml,test.yaml 
+Conflict on paths => /pets : post in files: petstore.yaml,test.yaml 
+Conflict on paths => operationIds : listPets in files: petstore.yaml,test.yaml 
+Conflict on paths => operationIds : createPets in files: petstore.yaml,test.yaml 
+Conflict on paths => operationIds : showPetById in files: petstore.yaml,test.yaml 
+Conflict on paths => /pets/{petId} : get in files: petstore.yaml,test.yaml 
 
+openapi.yaml: join processed in 49ms
+```
 
-**prefix-tags-with-info-prop**
+### prefix-tags-with-info-prop
 
-If any of the input files contains the `tags` object, tags in the output file will be prefixed by the selected property from the `info` object of the corresponding input file.
+If any of the input files contain the `tags` object, tags in the output file will be prefixed by the selected property from the `info` object of the corresponding input file.
 
 The output file preserves the original tag names as the value of the `x-displayName` property for each tag.
 
+#### Usage
 
-Usage:
-
-
-```bash
+```bash request
 openapi join first-api.yaml second-api.json --prefix-tags-with-info-prop title
 ```
 
+```yaml output file example
+- name: First Document title_endpoints
+  description: endpoints tag description
+  x-displayName: endpoints
 
-Example from the output file:
-
-
-```
-  - name: First Document Title_endpoints
-    description: endpoints tag description
-    x-displayName: endpoints
-
-  - name: Second document title_pets
-    description: pets tag description
-    x-displayName: pets
+- name: Second document title_pets
+  description: pets tag description
+  x-displayName: pets
 ```
 
 
-**prefix-tags-with-filename**
+### prefix-tags-with-filename
 
-If any of the input files contains the `tags` object, tags in the output file will be prefixed by the filename of the corresponding input file.
+If any of the input files contain the `tags` object, tags in the output file will be prefixed by the filename of the corresponding input file.
 
 The output file preserves the original tag names as the value of the `x-displayName` property for each tag.
 
+#### Usage
 
-Usage:
-
-
-```bash
+```bash request
 openapi join first-api.yaml second-api.json --prefix-tags-with-filename true
 ```
 
+```yaml output file example
+- name: first-api_endpoints
+  description: endpoints tag description
+  x-displayName: endpoints
 
-Example from the output file:
-
-
+- name: second-api_pets
+  description: pets tag description
+  x-displayName: pets
 ```
-  - name: first-api_endpoints
-    description: endpoints tag description
-    x-displayName: endpoints
 
-  - name: second-api_pets
-    description: pets tag description
-    x-displayName: pets
-```
-
-
-**prefix-components-with-info-prop**
+### prefix-components-with-info-prop
 
 If any of the input files have conflicting component names, this option can be used to resolve that issue and generate the output file. All component names in the output file will be prefixed by the selected property from the `info` object of the corresponding input file(s).
 
+#### Usage
 
-Usage:
-
-
-```bash
+```bash request
 openapi join first-api.yaml second-api.json --prefix-components-with-info-prop version
 ```
 
-
-Example from the output file:
-
-
-```yaml
+```yaml output file example
 components:
   schemas:
     1.0.1_Pet:
diff --git a/docs/commands/login.md b/docs/commands/login.md
index 3b4d6928..286fc87b 100644
--- a/docs/commands/login.md
+++ b/docs/commands/login.md
@@ -3,7 +3,7 @@
 ## Introduction
 
 :::warning Note
-Before proceeding with the command, [generate a personal API key](../../workflows/personal-api-keys.md).
+Before proceeding with the command, [generate a personal API key](../../settings/personal-api-keys.md).
 :::
 
 Use the `login` command to authenticate to the API registry.
diff --git a/docs/commands/push.md b/docs/commands/push.md
index bd9f4ee0..831e6b2a 100644
--- a/docs/commands/push.md
+++ b/docs/commands/push.md
@@ -49,7 +49,7 @@ The prerequisite for using the `push` command is an active user account in a Red
 
 To find your organization ID required for the command, log into Workflows and access the **API registry** page. In your browser's address bar, find the URL of this page. The segment after `app.redoc.ly/org/` is your organization ID. For example, if the URL is `app.redoc.ly/org/test_docs`, the organization ID is `test_docs`. When using the `push` command, you would provide this ID as `@test_docs`. Note that the organization ID can differ from the organization name. Owners can change the organization name at any time in the Workflows **Org settings** page, but the organization ID cannot be changed.
 
-To authenticate to the API registry, you can use the `REDOCLY_AUTHORIZATION` environment variable. It can be set to either your [personal API key](../../workflows/personal-api-keys.md) or to an organization-wide API key (configurable by organization owners in **Redocly Workflows > Org settings > API keys**).
+To authenticate to the API registry, you can use the `REDOCLY_AUTHORIZATION` environment variable. It can be set to either your [personal API key](../../settings/personal-api-keys.md) or to an organization-wide API key (configurable by organization owners in **Redocly Workflows > Org settings > API keys**).
 
 Treat the API keys as secrets and work with them accordingly. Consult the documentation for your CI system to learn more about handling secrets:
 
diff --git a/docs/guides/replace-server-url.md b/docs/guides/replace-server-url.md
index f30a4af5..7fd340a8 100644
--- a/docs/guides/replace-server-url.md
+++ b/docs/guides/replace-server-url.md
@@ -90,7 +90,7 @@ In this step, add the environment variable `SERVERS_URL` (defined in the previou
 
 ![Environment variables](./images/envt-variable-after.png)
 
-To create API versions for different server URLs, use the **API registry**. [Add another version](../../workflows/api-registry/versions.md) of the API with a different environment variable value.
+To create API versions for different server URLs, use the **API registry**. [Add another version](../../api-registry/guides/versions.md) of the API with a different environment variable value.
 
 ## Step 4: Register the plugin for Redocly use
 
diff --git a/package-lock.json b/package-lock.json
index 267290ab..d1073590 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@redocly/openapi",
-  "version": "1.0.0-beta.58",
+  "version": "1.0.0-beta.59",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@redocly/openapi",
-      "version": "1.0.0-beta.58",
+      "version": "1.0.0-beta.59",
       "license": "MIT",
       "workspaces": [
         "packages/*"
@@ -9631,7 +9631,7 @@
     },
     "packages/cli": {
       "name": "@redocly/openapi-cli",
-      "version": "1.0.0-beta.58",
+      "version": "1.0.0-beta.59",
       "license": "MIT",
       "dependencies": {
         "@redocly/openapi-core": "^1.0.0-beta.55",
@@ -9725,7 +9725,7 @@
     },
     "packages/core": {
       "name": "@redocly/openapi-core",
-      "version": "1.0.0-beta.58",
+      "version": "1.0.0-beta.59",
       "license": "MIT",
       "dependencies": {
         "@redocly/ajv": "^8.6.2",
diff --git a/package.json b/package.json
index c0e6210a..fad9cc51 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@redocly/openapi",
-  "version": "1.0.0-beta.58",
+  "version": "1.0.0-beta.59",
   "description": "",
   "private": true,
   "engines": {
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 8e09ece1..38019dc9 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@redocly/openapi-cli",
-  "version": "1.0.0-beta.58",
+  "version": "1.0.0-beta.59",
   "description": "",
   "license": "MIT",
   "bin": {
diff --git a/packages/cli/src/commands/preview-docs/preview-server/preview-server.ts b/packages/cli/src/commands/preview-docs/preview-server/preview-server.ts
index 68f47abc..d0bf4623 100644
--- a/packages/cli/src/commands/preview-docs/preview-server/preview-server.ts
+++ b/packages/cli/src/commands/preview-docs/preview-server/preview-server.ts
@@ -6,6 +6,7 @@ import * as path from 'path';
 
 import { startHttpServer, startWsServer, respondWithGzip, mimeTypes } from './server';
 import type { IncomingMessage } from 'http';
+import { isSubdir } from '../../../utils';
 
 function getPageHTML(
   htmlTemplate: string,
@@ -104,6 +105,12 @@ export default async function startPreviewServer(
         }[request.url || ''] ||
         path.resolve(htmlTemplate ? path.dirname(htmlTemplate) : process.cwd(), `.${request.url}`);
 
+      if (!isSubdir(process.cwd(), filePath)) {
+        respondWithGzip('404 Not Found', request, response, { 'Content-Type': 'text/html' }, 404);
+        console.timeEnd(colorette.dim(`GET ${request.url}`));
+        return;
+      }
+
       const extname = String(path.extname(filePath)).toLowerCase() as keyof typeof mimeTypes;
 
       const contentType = mimeTypes[extname] || 'application/octet-stream';
diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts
index f17834aa..5a35e9a8 100644
--- a/packages/cli/src/utils.ts
+++ b/packages/cli/src/utils.ts
@@ -290,4 +290,12 @@ export function slash(path: string): string {
   }
 
   return path.replace(/\\/g, '/');
+}
+
+/**
+ * Checks if dir is subdir of parent
+ */
+export function isSubdir(parent: string, dir: string): boolean {
+  const relative = path.relative(parent, dir);
+  return !!relative && !/^..($|\/)/.test(relative) && !path.isAbsolute(relative);
 }
\ No newline at end of file
diff --git a/packages/core/package.json b/packages/core/package.json
index 47f333da..96bf2ccb 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@redocly/openapi-core",
-  "version": "1.0.0-beta.58",
+  "version": "1.0.0-beta.59",
   "description": "",
   "main": "lib/index.js",
   "engines": {
diff --git a/packages/core/src/redocly/index.ts b/packages/core/src/redocly/index.ts
index f8158708..5dfd3db6 100644
--- a/packages/core/src/redocly/index.ts
+++ b/packages/core/src/redocly/index.ts
@@ -36,7 +36,7 @@ export class RedoclyClient {
 
   async verifyToken(accessToken: string, verbose: boolean = false): Promise<boolean> {
     if (!accessToken) return false;
-    const authDetails = await RedoclyClient.authorize(accessToken, verbose);
+    const authDetails = await RedoclyClient.authorize(accessToken, { verbose });
     if (!authDetails) return false;
     return true;
   }
@@ -91,9 +91,10 @@ export class RedoclyClient {
     });
   }
 
-  static async authorize(accessToken: string, verbose: boolean = false) {
+  static async authorize(accessToken: string, options: { queryName?: string; verbose?: boolean }) {
+    const { queryName = '', verbose = false } = options;
     try {
-      const queryStr = `{ viewer { id } }`;
+      const queryStr = `query ${queryName}{ viewer { id } }`;
 
       return await query(queryStr, {}, { Authorization: accessToken });
     } catch (e) {


Test patch:
diff --git a/packages/cli/src/__tests__/utils.test.ts b/packages/cli/src/__tests__/utils.test.ts
index 508185c5..c0f2f2b4 100644
--- a/packages/cli/src/__tests__/utils.test.ts
+++ b/packages/cli/src/__tests__/utils.test.ts
@@ -1,4 +1,7 @@
-import { slash } from '../utils';
+import { isSubdir, slash } from '../utils';
+
+jest.mock("os");
+
 
 describe('slash path', () => {
   it('can correctly slash path', () => {
@@ -17,3 +20,42 @@ describe('slash path', () => {
     expect(slash(extended)).toBe(extended);
   });
 });
+
+describe('isSubdir', () => {
+  it('can correctly determine if subdir', () => {
+    (
+      [
+        ['/foo', '/foo', false],
+        ['/foo', '/bar', false],
+        ['/foo', '/foobar', false],
+        ['/foo', '/foo/bar', true],
+        ['/foo', '/foo/../bar', false],
+        ['/foo', '/foo/./bar', true],
+        ['/bar/../foo', '/foo/bar', true],
+        ['/foo', './bar', false],
+        ['/foo', '/foo/..bar', true],
+      ] as [string, string, boolean][]
+    ).forEach(([parent, child, expectRes]) => {
+      expect(isSubdir(parent, child)).toBe(expectRes);
+    });
+  });
+
+  it('can correctly determine if subdir for windows-based paths', () => {
+    const os = require('os');
+    os.platform.mockImplementation(() => 'win32');
+
+    (
+      [
+        ['C:/Foo', 'C:/Foo/Bar', true],
+        ['C:\\Foo', 'C:\\Bar', false],
+        ['C:\\Foo', 'D:\\Foo\\Bar', false],
+      ] as [string, string, boolean][]
+    ).forEach(([parent, child, expectRes]) => {
+      expect(isSubdir(parent, child)).toBe(expectRes);
+    });
+  });
+
+  afterEach(() => {
+    jest.resetModules()
+  })
+});