Repository: Redocly/redocly-cli
Pull request #688 (2022-05-16 12:08:10)

add bundle option to keep url references
## What/Why/How?
By default bundling resolves all references into a single file. With the new `keep-url-references` flag we can keep external url references intact.

This is useful to keep 'global' references intact that are coming from e.g. an internal registry. With this option we can basically bundle multiple files inside a repo into a single file without loosing trace of external dependencies.

## Reference

## Testing
New test is added to test the functionality.

## Screenshots (optional)

## Check yourself

- [ ] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [ ] All new/updated code is covered with tests

## Security

- [ ] Security impact of change has been considered
- [ ] Code follows company security practices and guidelines


pvonr: Note: I didn't run `npm run prettier` as the command was changing many untouched files. Happy to do this if you think these changes should go into this PR as well. I would prefer a separate PR to bring the repo to compliance with the prettier rules.

Patch:
diff --git a/docs/commands/bundle.md b/docs/commands/bundle.md
index 88ac61c7..bff68c56 100644
--- a/docs/commands/bundle.md
+++ b/docs/commands/bundle.md
@@ -29,6 +29,7 @@ Option | Type | Description
 entrypoints | [string] | List of API root definition filenames or names assigned in the `apis` section of your Redocly configuration file. Default values are all names defined in the `apis` section within your configuration file.
 --config | string | Specify path to the [config file](#custom-configuration-file).
 --dereferenced, -d | boolean | Generate fully dereferenced bundle.
+--keep-url-references, -k | boolean | Keep absolute url references.
 --ext | string | Specify bundled file extension. Possible values are `json`, `yaml`, or `yml`. Default value is `yaml`.
 --extends | [string] | Can be used in combination with `--lint` to [extend a specific configuration](./lint.md#extend-configuration).  Default values are taken from the Redocly configuration file.
 --force, -f | boolean | Generate bundle output even when errors occur.
diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts
index 4403b845..3911fbb7 100644
--- a/packages/cli/src/commands/bundle.ts
+++ b/packages/cli/src/commands/bundle.ts
@@ -39,6 +39,7 @@ export async function handleBundle(
     metafile?: string;
     extends?: string[];
     'remove-unused-components'?: boolean;
+    'keep-url-references'?: boolean;
   },
   version: string,
 ) {
@@ -98,6 +99,7 @@ export async function handleBundle(
         ref: path,
         dereference: argv.dereferenced,
         removeUnusedComponents,
+        keepUrlRefs: argv['keep-url-references'],
       });
 
       const fileTotals = getTotals(problems);
diff --git a/packages/core/src/bundle.ts b/packages/core/src/bundle.ts
index a5615389..09623090 100755
--- a/packages/core/src/bundle.ts
+++ b/packages/core/src/bundle.ts
@@ -7,7 +7,7 @@ import { Oas3_1Types } from './types/oas3_1';
 import { NormalizedNodeType, normalizeTypes, NodeType } from './types';
 import { WalkContext, walkDocument, UserContext, ResolveResult } from './walk';
 import { detectOpenAPI, openAPIMajor, OasMajorVersion } from './oas-types';
-import { isRef, Location, refBaseName } from './ref-utils';
+import {isAbsoluteUrl, isRef, Location, refBaseName} from './ref-utils';
 import { initRules } from './config/rules';
 import { reportUnresolvedRef } from './rules/no-unresolved-refs';
 import { isPlainObject } from './utils';
@@ -35,6 +35,7 @@ export async function bundle(opts: {
   base?: string;
   skipRedoclyRegistryRefs?: boolean;
   removeUnusedComponents?: boolean;
+  keepUrlRefs?: boolean;
 }) {
   const {
     ref,
@@ -71,6 +72,7 @@ export async function bundleDocument(opts: {
   dereference?: boolean;
   skipRedoclyRegistryRefs?: boolean;
   removeUnusedComponents?: boolean;
+  keepUrlRefs?: boolean;
 }) {
   const {
     document,
@@ -80,6 +82,7 @@ export async function bundleDocument(opts: {
     dereference = false,
     skipRedoclyRegistryRefs = false,
     removeUnusedComponents = false,
+    keepUrlRefs = false,
   } = opts;
   const oasVersion = detectOpenAPI(document.parsed);
   const oasMajorVersion = openAPIMajor(oasVersion);
@@ -128,7 +131,14 @@ export async function bundleDocument(opts: {
       {
         severity: 'error',
         ruleId: 'bundler',
-        visitor: makeBundleVisitor(oasMajorVersion, dereference, skipRedoclyRegistryRefs, document, resolvedRefMap),
+        visitor: makeBundleVisitor(
+          oasMajorVersion,
+          dereference,
+          skipRedoclyRegistryRefs,
+          document,
+          resolvedRefMap,
+          keepUrlRefs,
+        ),
       },
       ...decorators,
     ] as any,
@@ -199,7 +209,8 @@ function makeBundleVisitor(
   dereference: boolean,
   skipRedoclyRegistryRefs: boolean,
   rootDocument: Document,
-  resolvedRefMap: ResolvedRefMap
+  resolvedRefMap: ResolvedRefMap,
+  keepUrlRefs: boolean,
 ) {
   let components: Record<string, Record<string, any>>;
 
@@ -224,6 +235,10 @@ function makeBundleVisitor(
           return;
         }
 
+        if (keepUrlRefs && isAbsoluteUrl(node.$ref)) {
+          return;
+        }
+
         const componentType = mapTypeToComponent(ctx.type.name, version);
         if (!componentType) {
           replaceRef(node, resolved, ctx);
diff --git a/packages/core/src/ref-utils.ts b/packages/core/src/ref-utils.ts
index 663baeab..6b20b1a0 100644
--- a/packages/core/src/ref-utils.ts
+++ b/packages/core/src/ref-utils.ts
@@ -72,6 +72,7 @@ export function isMappingRef(mapping: string) {
   return (
     mapping.startsWith('#') ||
     mapping.startsWith('https://') ||
+    mapping.startsWith('http://') ||
     mapping.startsWith('./') ||
     mapping.startsWith('../') ||
     mapping.indexOf('/') > -1


Test patch:
diff --git a/packages/core/__tests__/__snapshots__/bundle.test.ts.snap b/packages/core/__tests__/__snapshots__/bundle.test.ts.snap
index a4069fd6..8b867cf0 100644
--- a/packages/core/__tests__/__snapshots__/bundle.test.ts.snap
+++ b/packages/core/__tests__/__snapshots__/bundle.test.ts.snap
@@ -100,6 +100,32 @@ components:
 
 `;
 
+exports[`bundle should not bundle url refs if used with keepUrlRefs 1`] = `
+openapi: 3.0.0
+paths:
+  /pet:
+    parameters:
+      - $ref: '#/components/parameters/path-param'
+    put:
+      parameters:
+        - $ref: https://someexternal.schema
+        - $ref: '#/components/parameters/param-b'
+        - name: test
+    get:
+      parameters:
+        - $ref: http://someexternal.schema
+        - $ref: '#/components/parameters/param-c'
+components:
+  parameters:
+    path-param:
+      name: path_param
+    param-c:
+      name: param_c
+    param-b:
+      name: param_b
+
+`;
+
 exports[`bundle should not place referened schema inline when component in question is not of type "schemas" 1`] = `
 openapi: 3.0.0
 paths:
diff --git a/packages/core/__tests__/bundle.test.ts b/packages/core/__tests__/bundle.test.ts
index 8d38dda8..76bc8d95 100644
--- a/packages/core/__tests__/bundle.test.ts
+++ b/packages/core/__tests__/bundle.test.ts
@@ -128,5 +128,30 @@ describe('bundle', () => {
       }
     );
     expect(res.parsed).toMatchSnapshot();
-  })
+  });
+
+  it('should not bundle url refs if used with keepUrlRefs', async () => {
+    const fetchMock = jest.fn(() =>
+        Promise.resolve({
+          ok: true,
+          text: () => 'External schema content',
+          headers: {
+            get: () => '',
+          },
+        }),
+    );
+    const { bundle: res, problems } = await bundle({
+      config: new Config({} as ResolvedConfig),
+      externalRefResolver: new BaseResolver({
+        http: {
+          customFetch: fetchMock,
+          headers: [],
+        },
+      }),
+      ref: path.join(__dirname, 'fixtures/refs/openapi-with-url-refs.yaml'),
+      keepUrlRefs: true,
+    });
+    expect(problems).toHaveLength(0);
+    expect(res.parsed).toMatchSnapshot();
+  });
 });
diff --git a/packages/core/__tests__/fixtures/refs/openapi-with-url-refs.yaml b/packages/core/__tests__/fixtures/refs/openapi-with-url-refs.yaml
new file mode 100644
index 00000000..7f268151
--- /dev/null
+++ b/packages/core/__tests__/fixtures/refs/openapi-with-url-refs.yaml
@@ -0,0 +1,18 @@
+openapi: 3.0.0
+paths:
+  /pet:
+    parameters:
+      - $ref: '#/components/parameters/path-param'
+    put:
+      parameters:
+        - $ref: 'https://someexternal.schema'
+        - $ref: ./param-b.yaml
+        - name: test
+    get:
+      parameters:
+        - $ref: 'http://someexternal.schema'
+        - $ref: ./param-c.yaml
+components:
+  parameters:
+    path-param:
+      name: path_param