From 082d968bcc8b4c45020970f6c5127dd8b8309db9 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:09:13 +0200 Subject: [PATCH 01/28] fix: can't resolve ref error when using --- packages/core/src/bundle/bundle-visitor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index bcd16978d4..33b1ae4e2e 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -126,7 +126,7 @@ export function makeBundleVisitor( replaceRef(node, resolved, ctx); } else { node.$ref = saveComponent(componentType, resolved, ctx); - resolveBundledComponent(node, resolved, ctx); + resolveBundledComponent(node, resolved); } } }, @@ -186,8 +186,8 @@ export function makeBundleVisitor( }; } - function resolveBundledComponent(node: OasRef, resolved: ResolveResult, ctx: UserContext) { - const newRefId = makeRefId(ctx.location.source.absoluteRef, node.$ref); + function resolveBundledComponent(node: OasRef, resolved: ResolveResult) { + const newRefId = makeRefId(rootDocument.source.absoluteRef, node.$ref); resolvedRefMap.set(newRefId, { document: rootDocument, isRemote: false, From fc7f396f0f437170f4713b0a6f151e803e3dc230 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:58:08 +0200 Subject: [PATCH 02/28] feat: add test for the specific case --- .../openapi.yaml | 17 +++++++++++ .../parameters.yaml | 5 ++++ .../redocly.yaml | 6 ++++ .../schemas.yaml | 2 ++ .../snapshot.txt | 30 +++++++++++++++++++ tests/e2e/commands.test.ts | 10 +++++++ 6 files changed, 70 insertions(+) create mode 100644 tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml create mode 100644 tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml create mode 100644 tests/e2e/bundle/parameters-reference-to-schemas/redocly.yaml create mode 100644 tests/e2e/bundle/parameters-reference-to-schemas/schemas.yaml create mode 100644 tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml b/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml new file mode 100644 index 0000000000..c5909327c4 --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml @@ -0,0 +1,17 @@ +openapi: 3.0.0 +info: + title: Sample API + version: 1.0.0 +paths: + /users: + get: + operationId: getUsers + summary: Get a list of users + parameters: + - $ref: parameters.yaml#/Param + responses: + '200': + description: A list of users + content: + application/json: + schema: {} diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml b/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml new file mode 100644 index 0000000000..a74e5dda63 --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml @@ -0,0 +1,5 @@ +Param: + in: query + required: true + schema: + $ref: schemas.yaml#/Schema diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/redocly.yaml b/tests/e2e/bundle/parameters-reference-to-schemas/redocly.yaml new file mode 100644 index 0000000000..3c417243f8 --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/redocly.yaml @@ -0,0 +1,6 @@ +apis: + test-api: + root: openapi.yaml + +decorators: + remove-unused-components: on diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/schemas.yaml b/tests/e2e/bundle/parameters-reference-to-schemas/schemas.yaml new file mode 100644 index 0000000000..72170c47b6 --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/schemas.yaml @@ -0,0 +1,2 @@ +Schema: + type: string diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt new file mode 100644 index 0000000000..bc6f67dc94 --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt @@ -0,0 +1,30 @@ +openapi: 3.0.0 +info: + title: Sample API + version: 1.0.0 +paths: + /users: + get: + operationId: getUsers + summary: Get a list of users + parameters: + - $ref: '#/components/parameters/Param' + responses: + '200': + description: A list of users + content: + application/json: + schema: {} +components: + schemas: + Schema: + type: string + parameters: + Param: + in: query + required: true + schema: + $ref: '#/components/schemas/Schema' + +bundling openapi.yaml using configuration for api 'test-api'... +📦 Created a bundle for openapi.yaml at stdout ms. diff --git a/tests/e2e/commands.test.ts b/tests/e2e/commands.test.ts index bba15e30f4..080abd7501 100644 --- a/tests/e2e/commands.test.ts +++ b/tests/e2e/commands.test.ts @@ -672,6 +672,16 @@ describe('E2E', () => { }); }); + describe('bundle with parameter, which has reference to schemas', () => { + it('should correctly bundle the api and not remove components', async () => { + const testPath = join(__dirname, `bundle/parameters-reference-to-schemas`); + const args = getParams(indexEntryPoint, ['bundle', '--config=redocly.yaml']); + + const result = getCommandOutput(args, { testPath }); + await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt')); + }); + }); + describe('miscellaneous', () => { const folderPath = join(__dirname, 'miscellaneous'); From 982edb924901b7736f5851a9da1efbbbcc10a6be Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:09:16 +0200 Subject: [PATCH 03/28] feat: add changeset --- .changeset/happy-chairs-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/happy-chairs-drop.md diff --git a/.changeset/happy-chairs-drop.md b/.changeset/happy-chairs-drop.md new file mode 100644 index 0000000000..73d83c71c1 --- /dev/null +++ b/.changeset/happy-chairs-drop.md @@ -0,0 +1,5 @@ +--- +"@redocly/openapi-core": patch +--- + +Fixed an issue where the `remove-unused-compoents` decorator throws a `Can't resolve $ref` error. From b6843a8a87368f8a3723968d1a8f0deb062600b9 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:33:05 +0200 Subject: [PATCH 04/28] chore: add comment --- packages/core/src/bundle/bundle-visitor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index 33b1ae4e2e..8f6b6be787 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -187,6 +187,7 @@ export function makeBundleVisitor( } function resolveBundledComponent(node: OasRef, resolved: ResolveResult) { + // make ref from the root document to the bundled component const newRefId = makeRefId(rootDocument.source.absoluteRef, node.$ref); resolvedRefMap.set(newRefId, { document: rootDocument, From 1a8fea3f567a0955e7963f772b4bf40023576bfa Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:41:19 +0200 Subject: [PATCH 05/28] chore: remove comment --- packages/core/src/bundle/bundle-visitor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index 8f6b6be787..33b1ae4e2e 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -187,7 +187,6 @@ export function makeBundleVisitor( } function resolveBundledComponent(node: OasRef, resolved: ResolveResult) { - // make ref from the root document to the bundled component const newRefId = makeRefId(rootDocument.source.absoluteRef, node.$ref); resolvedRefMap.set(newRefId, { document: rootDocument, From 673289f3b7cb1806fd62092a873e61448ba9ca94 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:07:21 +0200 Subject: [PATCH 06/28] Update .changeset/happy-chairs-drop.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jacek Łękawa <164185257+JLekawa@users.noreply.github.com> --- .changeset/happy-chairs-drop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/happy-chairs-drop.md b/.changeset/happy-chairs-drop.md index 73d83c71c1..d1d0f9f100 100644 --- a/.changeset/happy-chairs-drop.md +++ b/.changeset/happy-chairs-drop.md @@ -2,4 +2,4 @@ "@redocly/openapi-core": patch --- -Fixed an issue where the `remove-unused-compoents` decorator throws a `Can't resolve $ref` error. +Fixed an issue where the `remove-unused-compoents` decorator threw a `Can't resolve $ref` error. This issue occurred when `components.parameters` had a $ref to `components.schemas`. From d03375ab444ed83de87cb906cf76365eeb2ccc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81=C4=99kawa?= <164185257+JLekawa@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:16:03 +0100 Subject: [PATCH 07/28] Update .changeset/happy-chairs-drop.md --- .changeset/happy-chairs-drop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/happy-chairs-drop.md b/.changeset/happy-chairs-drop.md index d1d0f9f100..8535bb8ad5 100644 --- a/.changeset/happy-chairs-drop.md +++ b/.changeset/happy-chairs-drop.md @@ -2,4 +2,4 @@ "@redocly/openapi-core": patch --- -Fixed an issue where the `remove-unused-compoents` decorator threw a `Can't resolve $ref` error. This issue occurred when `components.parameters` had a $ref to `components.schemas`. +Fixed an issue where the `remove-unused-compoents` decorator threw a `Can't resolve $ref` error. This issue occurred when `components.parameters` had a `$ref` to `components.schemas`. From 06df369aefc44c11c09483807b03b96d47c787b6 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:46:59 +0200 Subject: [PATCH 08/28] chore: add docs for the bundle command --- docs/@v2/commands/bundle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/@v2/commands/bundle.md b/docs/@v2/commands/bundle.md index 20670d8b3f..62d08566e4 100644 --- a/docs/@v2/commands/bundle.md +++ b/docs/@v2/commands/bundle.md @@ -10,7 +10,7 @@ Redocly CLI can help you combine separate API description files (such as if you The `bundle` command pulls the relevant parts of an API description into a single file output in JSON or YAML format. The `bundle` command differs from the [`join`](./join.md) command. -The `bundle` command takes a root OpenAPI file as input and follows the `$ref` mentions to include all the referenced components into a single output file. +The `bundle` command takes a root OpenAPI file as input and follows the `$ref` mentions to include all the referenced components into a single output file. All components are automatically resolved and included without requiring explicit specification. The `join` command can combine multiple OpenAPI files into a single unified API description file. The `bundle` command first executes preprocessors, then rules, then decorators. From 842eba1fde007d518ea63a5fe1d53df652fe55a7 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:49:11 +0200 Subject: [PATCH 09/28] docs: change naming --- docs/@v2/commands/bundle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/@v2/commands/bundle.md b/docs/@v2/commands/bundle.md index 62d08566e4..4eb8d3405f 100644 --- a/docs/@v2/commands/bundle.md +++ b/docs/@v2/commands/bundle.md @@ -10,7 +10,7 @@ Redocly CLI can help you combine separate API description files (such as if you The `bundle` command pulls the relevant parts of an API description into a single file output in JSON or YAML format. The `bundle` command differs from the [`join`](./join.md) command. -The `bundle` command takes a root OpenAPI file as input and follows the `$ref` mentions to include all the referenced components into a single output file. All components are automatically resolved and included without requiring explicit specification. +The `bundle` command takes a root OpenAPI file as input and follows the `$ref` mentions to include all the referenced components into a single output file. All components are automatically resolved and included without requiring explicit definitions. The `join` command can combine multiple OpenAPI files into a single unified API description file. The `bundle` command first executes preprocessors, then rules, then decorators. From d7d1092d5ec36303c81c4f5fe10800f18c65dade Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:55:27 +0200 Subject: [PATCH 10/28] fix: add to param name and change snapshot --- tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml | 1 + tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml b/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml index a74e5dda63..022bf0dd40 100644 --- a/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml +++ b/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml @@ -1,5 +1,6 @@ Param: in: query + name: param required: true schema: $ref: schemas.yaml#/Schema diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt index bc6f67dc94..25ce156050 100644 --- a/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt +++ b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt @@ -22,6 +22,7 @@ components: parameters: Param: in: query + name: param required: true schema: $ref: '#/components/schemas/Schema' From e4acbe3a9a4afddba80c49ade283c3b2cba6d2ca Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:57:27 +0200 Subject: [PATCH 11/28] chore: change order --- .../e2e/bundle/parameters-reference-to-schemas/parameters.yaml | 2 +- tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml b/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml index 022bf0dd40..04188a821e 100644 --- a/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml +++ b/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml @@ -1,6 +1,6 @@ Param: - in: query name: param + in: query required: true schema: $ref: schemas.yaml#/Schema diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt index 25ce156050..fded788c30 100644 --- a/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt +++ b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt @@ -21,8 +21,8 @@ components: type: string parameters: Param: - in: query name: param + in: query required: true schema: $ref: '#/components/schemas/Schema' From 3a43128e9b41a53f7ec9f0758ad3d21e70247046 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:34:34 +0200 Subject: [PATCH 12/28] feat: implement new fix to cover parameters + other components --- .../fixtures/responses-with-schemas/openapi.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml diff --git a/packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml b/packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml new file mode 100644 index 0000000000..93ea1e4c3b --- /dev/null +++ b/packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.0 +info: + title: Test + version: 1.0.0 +paths: + /users: + get: + responses: + '200': + $ref: './responses.yaml#/SuccessResponse' From 12191671516af315ba054fd7d75c304d86efe678 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:37:04 +0200 Subject: [PATCH 13/28] Revert "feat: implement new fix to cover parameters + other components" This reverts commit 3a43128e9b41a53f7ec9f0758ad3d21e70247046. --- .../fixtures/responses-with-schemas/openapi.yaml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml diff --git a/packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml b/packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml deleted file mode 100644 index 93ea1e4c3b..0000000000 --- a/packages/core/src/decorators/oas3/__tests__/fixtures/responses-with-schemas/openapi.yaml +++ /dev/null @@ -1,10 +0,0 @@ -openapi: 3.0.0 -info: - title: Test - version: 1.0.0 -paths: - /users: - get: - responses: - '200': - $ref: './responses.yaml#/SuccessResponse' From 515961970774a0f74e7b4ed7ad26ed5b9977c3d2 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:38:13 +0200 Subject: [PATCH 14/28] feat: implement new fix to cover parameters + other components --- packages/core/src/bundle/bundle-visitor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index 33b1ae4e2e..239dab866f 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -127,6 +127,10 @@ export function makeBundleVisitor( } else { node.$ref = saveComponent(componentType, resolved, ctx); resolveBundledComponent(node, resolved); + + if (!dequal(ctx.location.source, rootDocument.source)) { + resolveBundledComponent(node, resolved); + } } } }, From 9744269f42dec69a3847f4ff82ca71cb3a56aab9 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:00:10 +0200 Subject: [PATCH 15/28] feat: implement new fix to cover parameters + other components --- packages/core/src/bundle/bundle-visitor.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index 239dab866f..36a2bb6a2e 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -126,10 +126,12 @@ export function makeBundleVisitor( replaceRef(node, resolved, ctx); } else { node.$ref = saveComponent(componentType, resolved, ctx); - resolveBundledComponent(node, resolved); - - if (!dequal(ctx.location.source, rootDocument.source)) { - resolveBundledComponent(node, resolved); + // Register the bundled component from the original location + resolveBundledComponent(node, resolved, ctx.location.source.absoluteRef); + // Also register from root document location if this is an external ref + // This ensures refs inside bundled components can be resolved when visited later + if (ctx.location.source !== rootDocument.source) { + resolveBundledComponent(node, resolved, rootDocument.source.absoluteRef); } } } @@ -190,8 +192,8 @@ export function makeBundleVisitor( }; } - function resolveBundledComponent(node: OasRef, resolved: ResolveResult) { - const newRefId = makeRefId(rootDocument.source.absoluteRef, node.$ref); + function resolveBundledComponent(node: OasRef, resolved: ResolveResult, location: string) { + const newRefId = makeRefId(location, node.$ref); resolvedRefMap.set(newRefId, { document: rootDocument, isRemote: false, From bc4d4815682255858562795406ccd24ed559b5df Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Thu, 6 Nov 2025 18:06:59 +0200 Subject: [PATCH 16/28] Update .changeset/happy-chairs-drop.md Co-authored-by: Andrew Tatomyr --- .changeset/happy-chairs-drop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/happy-chairs-drop.md b/.changeset/happy-chairs-drop.md index 8535bb8ad5..abe6d73e2c 100644 --- a/.changeset/happy-chairs-drop.md +++ b/.changeset/happy-chairs-drop.md @@ -2,4 +2,4 @@ "@redocly/openapi-core": patch --- -Fixed an issue where the `remove-unused-compoents` decorator threw a `Can't resolve $ref` error. This issue occurred when `components.parameters` had a `$ref` to `components.schemas`. +Fixed an issue where the `remove-unused-components` decorator was failing to resolve `$ref` references to schemas from `components`. From 40dfc66c3c94c5993154f2e85625c7b367c0fef2 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:48:38 +0200 Subject: [PATCH 17/28] feat: implement proper logic for decorator and bundle-visitor --- packages/core/src/bundle/bundle-visitor.ts | 7 +--- .../oas3/remove-unused-components.ts | 40 +++++++++++++++++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index 36a2bb6a2e..4a27c040ef 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -127,12 +127,7 @@ export function makeBundleVisitor( } else { node.$ref = saveComponent(componentType, resolved, ctx); // Register the bundled component from the original location - resolveBundledComponent(node, resolved, ctx.location.source.absoluteRef); - // Also register from root document location if this is an external ref - // This ensures refs inside bundled components can be resolved when visited later - if (ctx.location.source !== rootDocument.source) { - resolveBundledComponent(node, resolved, rootDocument.source.absoluteRef); - } + resolveBundledComponent(node, resolved, rootDocument.source.absoluteRef); } } }, diff --git a/packages/core/src/decorators/oas3/remove-unused-components.ts b/packages/core/src/decorators/oas3/remove-unused-components.ts index 82f3d73673..d679c60c68 100644 --- a/packages/core/src/decorators/oas3/remove-unused-components.ts +++ b/packages/core/src/decorators/oas3/remove-unused-components.ts @@ -1,4 +1,5 @@ import { isEmptyObject } from '../../utils/is-empty-object.js'; +import { getOwn } from '../../utils/get-own.js'; import type { Location } from '../../ref-utils.js'; import type { Oas3Decorator } from '../../visitors.js'; @@ -13,6 +14,7 @@ import type { type AnyOas3Definition = Oas3Definition | Oas3_1Definition | Oas3_2Definition; export const RemoveUnusedComponents: Oas3Decorator = () => { + let componentKey: string | null = null; const components = new Map< string, { @@ -117,8 +119,23 @@ export const RemoveUnusedComponents: Oas3Decorator = () => { }, }, NamedResponses: { - Response(_response, { location, key }) { - registerComponent(location, 'responses', key.toString()); + Response: { + enter(response, { location, key }) { + componentKey = key.toString(); + if (!getOwn(response, 'content')) { + registerComponent(location, 'responses', key.toString()); + } + }, + leave() { + componentKey = null; + }, + MediaTypesMap: { + MediaType: { + Schema(_schema, { parentLocations }) { + registerComponent(parentLocations.Response, 'responses', componentKey!); + }, + }, + }, }, }, NamedExamples: { @@ -127,8 +144,23 @@ export const RemoveUnusedComponents: Oas3Decorator = () => { }, }, NamedRequestBodies: { - RequestBody(_requestBody, { location, key }) { - registerComponent(location, 'requestBodies', key.toString()); + RequestBody: { + enter(requestBody, { location, key }) { + componentKey = key.toString(); + if (!getOwn(requestBody, 'content')) { + registerComponent(location, 'requestBodies', key.toString()); + } + }, + leave() { + componentKey = null; + }, + MediaTypesMap: { + MediaType: { + Schema(_schema, { parentLocations }) { + registerComponent(parentLocations.RequestBody, 'requestBodies', componentKey!); + }, + }, + }, }, }, NamedHeaders: { From 1c4dc8aafa65dbdb69712a633b54415b7219fbc8 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:50:48 +0200 Subject: [PATCH 18/28] refactor: resolveBundledComponent location --- packages/core/src/bundle/bundle-visitor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index 4a27c040ef..eea043843e 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -127,7 +127,7 @@ export function makeBundleVisitor( } else { node.$ref = saveComponent(componentType, resolved, ctx); // Register the bundled component from the original location - resolveBundledComponent(node, resolved, rootDocument.source.absoluteRef); + resolveBundledComponent(node, resolved); } } }, @@ -187,8 +187,8 @@ export function makeBundleVisitor( }; } - function resolveBundledComponent(node: OasRef, resolved: ResolveResult, location: string) { - const newRefId = makeRefId(location, node.$ref); + function resolveBundledComponent(node: OasRef, resolved: ResolveResult) { + const newRefId = makeRefId(rootDocument.source.absoluteRef, node.$ref); resolvedRefMap.set(newRefId, { document: rootDocument, isRemote: false, From 803c12cbf34f706fd64bedba1e898453933c53e5 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:47:53 +0200 Subject: [PATCH 19/28] fix: change in all places to make ref id from the root document --- packages/core/src/resolve.ts | 6 +++--- packages/core/src/walk.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/resolve.ts b/packages/core/src/resolve.ts index 051bfe8ca3..a409514da2 100644 --- a/packages/core/src/resolve.ts +++ b/packages/core/src/resolve.ts @@ -377,7 +377,7 @@ export async function resolveDocument(opts: { document, nodePointer: ref.$ref, }; - const refId = makeRefId(document.source.absoluteRef, ref.$ref); + const refId = makeRefId(rootDocument.source.absoluteRef, ref.$ref); resolvedRefMap.set(refId, resolvedRef); return resolvedRef; } @@ -404,7 +404,7 @@ export async function resolveDocument(opts: { document: undefined, error: error, }; - const refId = makeRefId(document.source.absoluteRef, ref.$ref); + const refId = makeRefId(rootDocument.source.absoluteRef, ref.$ref); resolvedRefMap.set(refId, resolvedRef); return resolvedRef; } @@ -446,7 +446,7 @@ export async function resolveDocument(opts: { resolvedRef.node = target; resolvedRef.document = targetDoc; - const refId = makeRefId(document.source.absoluteRef, ref.$ref); + const refId = makeRefId(rootDocument.source.absoluteRef, ref.$ref); if (resolvedRef.document && isRef(target)) { resolvedRef = await followRef(resolvedRef.document, target, pushRef(refStack, target)); } diff --git a/packages/core/src/walk.ts b/packages/core/src/walk.ts index c4519a0330..fcce745ddd 100644 --- a/packages/core/src/walk.ts +++ b/packages/core/src/walk.ts @@ -141,7 +141,7 @@ export function walkDocument(opts: { parent: any, key: string | number ) { - const resolve: ResolveFn = (ref, from = currentLocation.source.absoluteRef) => { + const resolve: ResolveFn = (ref, from = document.source.absoluteRef) => { if (!isRef(ref)) return { location, node: ref }; const refId = makeRefId(from, ref.$ref); const resolvedRef = resolvedRefMap.get(refId); From 878586c0f9557a0185b31ab89328dde46f0a281e Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:51:21 +0200 Subject: [PATCH 20/28] chore: remove changes in decorator due to proper resolve --- .../oas3/remove-unused-components.ts | 40 ++----------------- 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/packages/core/src/decorators/oas3/remove-unused-components.ts b/packages/core/src/decorators/oas3/remove-unused-components.ts index d679c60c68..82f3d73673 100644 --- a/packages/core/src/decorators/oas3/remove-unused-components.ts +++ b/packages/core/src/decorators/oas3/remove-unused-components.ts @@ -1,5 +1,4 @@ import { isEmptyObject } from '../../utils/is-empty-object.js'; -import { getOwn } from '../../utils/get-own.js'; import type { Location } from '../../ref-utils.js'; import type { Oas3Decorator } from '../../visitors.js'; @@ -14,7 +13,6 @@ import type { type AnyOas3Definition = Oas3Definition | Oas3_1Definition | Oas3_2Definition; export const RemoveUnusedComponents: Oas3Decorator = () => { - let componentKey: string | null = null; const components = new Map< string, { @@ -119,23 +117,8 @@ export const RemoveUnusedComponents: Oas3Decorator = () => { }, }, NamedResponses: { - Response: { - enter(response, { location, key }) { - componentKey = key.toString(); - if (!getOwn(response, 'content')) { - registerComponent(location, 'responses', key.toString()); - } - }, - leave() { - componentKey = null; - }, - MediaTypesMap: { - MediaType: { - Schema(_schema, { parentLocations }) { - registerComponent(parentLocations.Response, 'responses', componentKey!); - }, - }, - }, + Response(_response, { location, key }) { + registerComponent(location, 'responses', key.toString()); }, }, NamedExamples: { @@ -144,23 +127,8 @@ export const RemoveUnusedComponents: Oas3Decorator = () => { }, }, NamedRequestBodies: { - RequestBody: { - enter(requestBody, { location, key }) { - componentKey = key.toString(); - if (!getOwn(requestBody, 'content')) { - registerComponent(location, 'requestBodies', key.toString()); - } - }, - leave() { - componentKey = null; - }, - MediaTypesMap: { - MediaType: { - Schema(_schema, { parentLocations }) { - registerComponent(parentLocations.RequestBody, 'requestBodies', componentKey!); - }, - }, - }, + RequestBody(_requestBody, { location, key }) { + registerComponent(location, 'requestBodies', key.toString()); }, }, NamedHeaders: { From bb9e3e76502bda7ab13cf3402bf24ae6c45a1ad7 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:00:36 +0200 Subject: [PATCH 21/28] fix: ajv instance resolve ref and unit test snapshots --- packages/core/src/__tests__/resolve.test.ts | 26 ++++++++++----------- packages/core/src/rules/ajv.ts | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/core/src/__tests__/resolve.test.ts b/packages/core/src/__tests__/resolve.test.ts index 4e6268aa6c..5f9f424a87 100644 --- a/packages/core/src/__tests__/resolve.test.ts +++ b/packages/core/src/__tests__/resolve.test.ts @@ -163,7 +163,7 @@ describe('collect refs', () => { // expect(resolvedRefs.size).toEqual(2); expect(Array.from(resolvedRefs.keys()).map((ref) => ref.substring(cwd.length + 1))).toEqual([ 'foobar.yaml::./externalInfo.yaml#/info', - 'externalInfo.yaml::./externalLicense.yaml', + 'foobar.yaml::./externalLicense.yaml', ]); expect(Array.from(resolvedRefs.values()).map((info) => info.node)).toEqual([ @@ -201,9 +201,9 @@ describe('collect refs', () => { .sort() ).toMatchInlineSnapshot(` [ + "openapi-with-back.yaml::../openapi-with-back.yaml#/components/schemas/TypeB", "openapi-with-back.yaml::./schemas/type-a.yaml#/", "openapi-with-back.yaml::./schemas/type-b.yaml#/", - "schemas/type-a.yaml::../openapi-with-back.yaml#/components/schemas/TypeB", ] `); @@ -272,15 +272,15 @@ describe('collect refs', () => { expect(resolvedRefs).toBeDefined(); expect(Array.from(resolvedRefs.keys()).map((ref) => ref.substring(cwd.length + 1))) .toMatchInlineSnapshot(` - [ - "openapi.yaml::#/components/schemas/Local", - "openapi.yaml::#/components/schemas/Local/properties/string", - "openapi.yaml::./External.yaml#/properties/string", - "openapi.yaml::./External.yaml", - "External.yaml::./External2.yaml", - "External2.yaml::./External.yaml#/properties", - ] - `); + [ + "openapi.yaml::#/components/schemas/Local", + "openapi.yaml::#/components/schemas/Local/properties/string", + "openapi.yaml::./External.yaml#/properties/string", + "openapi.yaml::./External.yaml", + "openapi.yaml::./External2.yaml", + "openapi.yaml::./External.yaml#/properties", + ] + `); expect(Array.from(resolvedRefs.values()).map((val) => val.node)).toMatchInlineSnapshot(` [ @@ -402,8 +402,8 @@ describe('collect refs', () => { expect(resolvedRefs).toBeDefined(); expect(resolvedRefs.size).toEqual(3); expect(Array.from(resolvedRefs.keys()).map((ref) => ref.substring(cwd.length + 1))).toEqual([ - 'transitive/components.yaml::./schemas.yaml#/schemas', - 'transitive/schemas.yaml::a.yaml', + 'foobar.yaml::./schemas.yaml#/schemas', + 'foobar.yaml::a.yaml', 'foobar.yaml::./transitive/components.yaml#/components/schemas/a', ]); diff --git a/packages/core/src/rules/ajv.ts b/packages/core/src/rules/ajv.ts index 45dc793539..bd532247dd 100644 --- a/packages/core/src/rules/ajv.ts +++ b/packages/core/src/rules/ajv.ts @@ -25,8 +25,8 @@ function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) { allowUnionTypes: true, validateFormats: true, defaultUnevaluatedProperties: allowAdditionalProperties, - loadSchemaSync(base: string, $ref: string, $id: string) { - const resolvedRef = resolve({ $ref }, base.split('#')[0]); + loadSchemaSync(_: string, $ref: string, $id: string) { + const resolvedRef = resolve({ $ref }); if (!resolvedRef || !resolvedRef.location) return false; return { $id: resolvedRef.location.source.absoluteRef + '#' + $id, ...resolvedRef.node }; }, From 2379a4e277e215d19fd336d2c981baf532789683 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:21:21 +0200 Subject: [PATCH 22/28] chore: revert previous fix --- packages/core/src/__tests__/resolve.test.ts | 8 ++++---- packages/core/src/resolve.ts | 6 +++--- packages/core/src/rules/ajv.ts | 4 ++-- packages/core/src/walk.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/__tests__/resolve.test.ts b/packages/core/src/__tests__/resolve.test.ts index 5f9f424a87..d940bd206b 100644 --- a/packages/core/src/__tests__/resolve.test.ts +++ b/packages/core/src/__tests__/resolve.test.ts @@ -163,7 +163,7 @@ describe('collect refs', () => { // expect(resolvedRefs.size).toEqual(2); expect(Array.from(resolvedRefs.keys()).map((ref) => ref.substring(cwd.length + 1))).toEqual([ 'foobar.yaml::./externalInfo.yaml#/info', - 'foobar.yaml::./externalLicense.yaml', + 'externalInfo.yaml::./externalLicense.yaml', ]); expect(Array.from(resolvedRefs.values()).map((info) => info.node)).toEqual([ @@ -201,9 +201,9 @@ describe('collect refs', () => { .sort() ).toMatchInlineSnapshot(` [ - "openapi-with-back.yaml::../openapi-with-back.yaml#/components/schemas/TypeB", "openapi-with-back.yaml::./schemas/type-a.yaml#/", "openapi-with-back.yaml::./schemas/type-b.yaml#/", + "schemas/type-a.yaml::../openapi-with-back.yaml#/components/schemas/TypeB", ] `); @@ -277,8 +277,8 @@ describe('collect refs', () => { "openapi.yaml::#/components/schemas/Local/properties/string", "openapi.yaml::./External.yaml#/properties/string", "openapi.yaml::./External.yaml", - "openapi.yaml::./External2.yaml", - "openapi.yaml::./External.yaml#/properties", + "External.yaml::./External2.yaml", + "External2.yaml::./External.yaml#/properties", ] `); diff --git a/packages/core/src/resolve.ts b/packages/core/src/resolve.ts index a409514da2..051bfe8ca3 100644 --- a/packages/core/src/resolve.ts +++ b/packages/core/src/resolve.ts @@ -377,7 +377,7 @@ export async function resolveDocument(opts: { document, nodePointer: ref.$ref, }; - const refId = makeRefId(rootDocument.source.absoluteRef, ref.$ref); + const refId = makeRefId(document.source.absoluteRef, ref.$ref); resolvedRefMap.set(refId, resolvedRef); return resolvedRef; } @@ -404,7 +404,7 @@ export async function resolveDocument(opts: { document: undefined, error: error, }; - const refId = makeRefId(rootDocument.source.absoluteRef, ref.$ref); + const refId = makeRefId(document.source.absoluteRef, ref.$ref); resolvedRefMap.set(refId, resolvedRef); return resolvedRef; } @@ -446,7 +446,7 @@ export async function resolveDocument(opts: { resolvedRef.node = target; resolvedRef.document = targetDoc; - const refId = makeRefId(rootDocument.source.absoluteRef, ref.$ref); + const refId = makeRefId(document.source.absoluteRef, ref.$ref); if (resolvedRef.document && isRef(target)) { resolvedRef = await followRef(resolvedRef.document, target, pushRef(refStack, target)); } diff --git a/packages/core/src/rules/ajv.ts b/packages/core/src/rules/ajv.ts index bd532247dd..45dc793539 100644 --- a/packages/core/src/rules/ajv.ts +++ b/packages/core/src/rules/ajv.ts @@ -25,8 +25,8 @@ function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) { allowUnionTypes: true, validateFormats: true, defaultUnevaluatedProperties: allowAdditionalProperties, - loadSchemaSync(_: string, $ref: string, $id: string) { - const resolvedRef = resolve({ $ref }); + loadSchemaSync(base: string, $ref: string, $id: string) { + const resolvedRef = resolve({ $ref }, base.split('#')[0]); if (!resolvedRef || !resolvedRef.location) return false; return { $id: resolvedRef.location.source.absoluteRef + '#' + $id, ...resolvedRef.node }; }, diff --git a/packages/core/src/walk.ts b/packages/core/src/walk.ts index fcce745ddd..c4519a0330 100644 --- a/packages/core/src/walk.ts +++ b/packages/core/src/walk.ts @@ -141,7 +141,7 @@ export function walkDocument(opts: { parent: any, key: string | number ) { - const resolve: ResolveFn = (ref, from = document.source.absoluteRef) => { + const resolve: ResolveFn = (ref, from = currentLocation.source.absoluteRef) => { if (!isRef(ref)) return { location, node: ref }; const refId = makeRefId(from, ref.$ref); const resolvedRef = resolvedRefMap.get(refId); From 5d4f0093d7451435a1755c413dcf92c1495378bf Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:23:59 +0200 Subject: [PATCH 23/28] chore: revert tests --- packages/core/src/__tests__/resolve.test.ts | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core/src/__tests__/resolve.test.ts b/packages/core/src/__tests__/resolve.test.ts index d940bd206b..4e6268aa6c 100644 --- a/packages/core/src/__tests__/resolve.test.ts +++ b/packages/core/src/__tests__/resolve.test.ts @@ -272,15 +272,15 @@ describe('collect refs', () => { expect(resolvedRefs).toBeDefined(); expect(Array.from(resolvedRefs.keys()).map((ref) => ref.substring(cwd.length + 1))) .toMatchInlineSnapshot(` - [ - "openapi.yaml::#/components/schemas/Local", - "openapi.yaml::#/components/schemas/Local/properties/string", - "openapi.yaml::./External.yaml#/properties/string", - "openapi.yaml::./External.yaml", - "External.yaml::./External2.yaml", - "External2.yaml::./External.yaml#/properties", - ] - `); + [ + "openapi.yaml::#/components/schemas/Local", + "openapi.yaml::#/components/schemas/Local/properties/string", + "openapi.yaml::./External.yaml#/properties/string", + "openapi.yaml::./External.yaml", + "External.yaml::./External2.yaml", + "External2.yaml::./External.yaml#/properties", + ] + `); expect(Array.from(resolvedRefs.values()).map((val) => val.node)).toMatchInlineSnapshot(` [ @@ -402,8 +402,8 @@ describe('collect refs', () => { expect(resolvedRefs).toBeDefined(); expect(resolvedRefs.size).toEqual(3); expect(Array.from(resolvedRefs.keys()).map((ref) => ref.substring(cwd.length + 1))).toEqual([ - 'foobar.yaml::./schemas.yaml#/schemas', - 'foobar.yaml::a.yaml', + 'transitive/components.yaml::./schemas.yaml#/schemas', + 'transitive/schemas.yaml::a.yaml', 'foobar.yaml::./transitive/components.yaml#/components/schemas/a', ]); From fac1645fa2b43a8bfc5031de87aa23923e54b2c7 Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:58:46 +0200 Subject: [PATCH 24/28] feat: add wrapper for resolve function with a default value --- packages/core/src/walk.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/walk.ts b/packages/core/src/walk.ts index c4519a0330..f2b5193f19 100644 --- a/packages/core/src/walk.ts +++ b/packages/core/src/walk.ts @@ -391,11 +391,15 @@ export function walkDocument(opts: { for (const { visit: visitor, ruleId, severity, context, message } of refLeaveVisitors) { if (enteredContexts.has(context)) { const report = reportFn.bind(undefined, ruleId, severity, message); + // Use a resolve function with document.source.absoluteRef as default + const resolveForRefLeave: ResolveFn = (ref, from = document.source.absoluteRef) => { + return resolve(ref, from); + }; visitor( node, { report, - resolve, + resolve: resolveForRefLeave, rawNode: node, rawLocation, location, From 7b0d7303bf3b9ac952463ec3109d8e9fed5f3a9d Mon Sep 17 00:00:00 2001 From: Albina Blazhko <46962291+AlbinaBlazhko17@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:31:38 +0200 Subject: [PATCH 25/28] refactor: override of the default arguments in resolve function --- packages/core/src/walk.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/walk.ts b/packages/core/src/walk.ts index f2b5193f19..c5f7965978 100644 --- a/packages/core/src/walk.ts +++ b/packages/core/src/walk.ts @@ -391,15 +391,11 @@ export function walkDocument(opts: { for (const { visit: visitor, ruleId, severity, context, message } of refLeaveVisitors) { if (enteredContexts.has(context)) { const report = reportFn.bind(undefined, ruleId, severity, message); - // Use a resolve function with document.source.absoluteRef as default - const resolveForRefLeave: ResolveFn = (ref, from = document.source.absoluteRef) => { - return resolve(ref, from); - }; visitor( node, { report, - resolve: resolveForRefLeave, + resolve: (ref, from = document.source.absoluteRef) => resolve(ref, from), rawNode: node, rawLocation, location, From 46289ac25910eda4fd12390485c9d624c8d37ba3 Mon Sep 17 00:00:00 2001 From: Albina Blazhko Date: Tue, 7 Apr 2026 14:46:37 +0300 Subject: [PATCH 26/28] feat: try different approach with saving component two times --- packages/core/src/bundle/bundle-visitor.ts | 34 +++++++++++----------- packages/core/src/walk.ts | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index b2322a9ac4..9ff26b354b 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -10,12 +10,11 @@ import { } from '../ref-utils.js'; import { type ResolvedRefMap, type Document } from '../resolve.js'; import { reportUnresolvedRef } from '../rules/common/no-unresolved-refs.js'; -import { type OasRef } from '../typings/openapi.js'; import { dequal } from '../utils/dequal.js'; import { isTruthy } from '../utils/is-truthy.js'; import { makeRefId } from '../utils/make-ref-id.js'; import { type Oas3Visitor, type Oas2Visitor } from '../visitors.js'; -import { type UserContext, type ResolveResult } from '../walk.js'; +import { type UserContext } from '../walk.js'; export function mapTypeToComponent(typeName: string, version: SpecMajorVersion) { switch (version) { @@ -160,8 +159,6 @@ export function makeBundleVisitor({ replaceRef(node, resolved, ctx); } else { node.$ref = saveComponent(componentType, resolved, ctx); - // Register the bundled component from the original location - resolveBundledComponent(node, resolved); } } }, @@ -223,17 +220,6 @@ export function makeBundleVisitor({ }; } - function resolveBundledComponent(node: OasRef, resolved: ResolveResult) { - const newRefId = makeRefId(rootDocument.source.absoluteRef, node.$ref); - resolvedRefMap.set(newRefId, { - document: rootDocument, - isRemote: false, - node: resolved.node, - nodePointer: node.$ref, - resolved: true, - }); - } - function saveComponent( componentType: string, target: { node: unknown; location: Location }, @@ -242,16 +228,30 @@ export function makeBundleVisitor({ components[componentType] = components[componentType] || {}; const name = getComponentName(target, componentType, ctx); components[componentType][name] = target.node; + + let newRef: string; if ( version === 'oas3' || version === 'async2' || version === 'async3' || version === 'openrpc1' ) { - return `#/components/${componentType}/${name}`; + newRef = `#/components/${componentType}/${name}`; } else { - return `#/${componentType}/${name}`; + newRef = `#/${componentType}/${name}`; } + + const entry = { + document: rootDocument, + isRemote: false, + node: target.node, + nodePointer: newRef, + resolved: true, + }; + resolvedRefMap.set(makeRefId(rootDocument.source.absoluteRef, newRef), entry); + resolvedRefMap.set(makeRefId(ctx.location.source.absoluteRef, newRef), entry); + + return newRef; } function isEqualOrEqualRef( diff --git a/packages/core/src/walk.ts b/packages/core/src/walk.ts index 9f08f720fa..43e8f331c6 100644 --- a/packages/core/src/walk.ts +++ b/packages/core/src/walk.ts @@ -394,7 +394,7 @@ export function walkDocument(opts: { node, { report, - resolve: (ref, from = document.source.absoluteRef) => resolve(ref, from), + resolve, rawNode: node, rawLocation, location, From e29208eaf0a00aaa130a6d681ed429524f57e986 Mon Sep 17 00:00:00 2001 From: Albina Blazhko Date: Tue, 7 Apr 2026 15:01:43 +0300 Subject: [PATCH 27/28] chore: make more clean solution --- packages/core/src/bundle/bundle-visitor.ts | 36 ++++++++++++---------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index 9ff26b354b..f6fa9afcdd 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -10,11 +10,12 @@ import { } from '../ref-utils.js'; import { type ResolvedRefMap, type Document } from '../resolve.js'; import { reportUnresolvedRef } from '../rules/common/no-unresolved-refs.js'; +import { type OasRef } from '../typings/openapi.js'; import { dequal } from '../utils/dequal.js'; import { isTruthy } from '../utils/is-truthy.js'; import { makeRefId } from '../utils/make-ref-id.js'; import { type Oas3Visitor, type Oas2Visitor } from '../visitors.js'; -import { type UserContext } from '../walk.js'; +import { type UserContext, type ResolveResult } from '../walk.js'; export function mapTypeToComponent(typeName: string, version: SpecMajorVersion) { switch (version) { @@ -159,6 +160,7 @@ export function makeBundleVisitor({ replaceRef(node, resolved, ctx); } else { node.$ref = saveComponent(componentType, resolved, ctx); + resolveBundledComponent(node, resolved, ctx); } } }, @@ -220,6 +222,20 @@ export function makeBundleVisitor({ }; } + function resolveBundledComponent(node: OasRef, resolved: ResolveResult, ctx: UserContext) { + const entry = { + document: rootDocument, + isRemote: false, + node: resolved.node, + nodePointer: node.$ref, + resolved: true, + }; + // For ref.leave visitors during the initial walk (source file context) + resolvedRefMap.set(makeRefId(ctx.location.source.absoluteRef, node.$ref), entry); + // For when walker re-visits bundled components in root.components + resolvedRefMap.set(makeRefId(rootDocument.source.absoluteRef, node.$ref), entry); + } + function saveComponent( componentType: string, target: { node: unknown; location: Location }, @@ -228,30 +244,16 @@ export function makeBundleVisitor({ components[componentType] = components[componentType] || {}; const name = getComponentName(target, componentType, ctx); components[componentType][name] = target.node; - - let newRef: string; if ( version === 'oas3' || version === 'async2' || version === 'async3' || version === 'openrpc1' ) { - newRef = `#/components/${componentType}/${name}`; + return `#/components/${componentType}/${name}`; } else { - newRef = `#/${componentType}/${name}`; + return `#/${componentType}/${name}`; } - - const entry = { - document: rootDocument, - isRemote: false, - node: target.node, - nodePointer: newRef, - resolved: true, - }; - resolvedRefMap.set(makeRefId(rootDocument.source.absoluteRef, newRef), entry); - resolvedRefMap.set(makeRefId(ctx.location.source.absoluteRef, newRef), entry); - - return newRef; } function isEqualOrEqualRef( From 01f00667e15ef3d3ece299a8ec584edefeb6e8d0 Mon Sep 17 00:00:00 2001 From: Albina Blazhko Date: Tue, 7 Apr 2026 18:56:38 +0300 Subject: [PATCH 28/28] fix: location of registered components, add tests to cover this case and fix prev test --- .../oas3/remove-unused-components.ts | 28 ++++++++-------- .../openapi.yaml | 3 -- .../snapshot.txt | 3 -- .../remove-unused-schema-alias/openapi.yaml | 21 ++++++++++++ .../parameters.yaml | 6 ++++ .../remove-unused-schema-alias/redocly.yaml | 6 ++++ .../remove-unused-schema-alias/schemas.yaml | 2 ++ .../remove-unused-schema-alias/snapshot.txt | 32 +++++++++++++++++++ tests/e2e/commands.test.ts | 10 ++++++ 9 files changed, 91 insertions(+), 20 deletions(-) create mode 100644 tests/e2e/bundle/remove-unused-schema-alias/openapi.yaml create mode 100644 tests/e2e/bundle/remove-unused-schema-alias/parameters.yaml create mode 100644 tests/e2e/bundle/remove-unused-schema-alias/redocly.yaml create mode 100644 tests/e2e/bundle/remove-unused-schema-alias/schemas.yaml create mode 100644 tests/e2e/bundle/remove-unused-schema-alias/snapshot.txt diff --git a/packages/core/src/decorators/oas3/remove-unused-components.ts b/packages/core/src/decorators/oas3/remove-unused-components.ts index b31b0ced93..45c8565309 100644 --- a/packages/core/src/decorators/oas3/remove-unused-components.ts +++ b/packages/core/src/decorators/oas3/remove-unused-components.ts @@ -118,40 +118,40 @@ export const RemoveUnusedComponents: Oas3Decorator = () => { }, }, NamedSchemas: { - Schema(schema, { location, key }) { + Schema(schema, { rawLocation, key }) { if (!schema.allOf) { - registerComponent(location, 'schemas', key.toString()); + registerComponent(rawLocation, 'schemas', key.toString()); } }, }, NamedParameters: { - Parameter(_parameter, { location, key }) { - registerComponent(location, 'parameters', key.toString()); + Parameter(_parameter, { rawLocation, key }) { + registerComponent(rawLocation, 'parameters', key.toString()); }, }, NamedResponses: { - Response(_response, { location, key }) { - registerComponent(location, 'responses', key.toString()); + Response(_response, { rawLocation, key }) { + registerComponent(rawLocation, 'responses', key.toString()); }, }, NamedExamples: { - Example(_example, { location, key }) { - registerComponent(location, 'examples', key.toString()); + Example(_example, { rawLocation, key }) { + registerComponent(rawLocation, 'examples', key.toString()); }, }, NamedRequestBodies: { - RequestBody(_requestBody, { location, key }) { - registerComponent(location, 'requestBodies', key.toString()); + RequestBody(_requestBody, { rawLocation, key }) { + registerComponent(rawLocation, 'requestBodies', key.toString()); }, }, NamedHeaders: { - Header(_header, { location, key }) { - registerComponent(location, 'headers', key.toString()); + Header(_header, { rawLocation, key }) { + registerComponent(rawLocation, 'headers', key.toString()); }, }, NamedMediaTypes: { - MediaTypesMap(_mediaTypesMap, { location, key }) { - registerComponent(location, 'mediaTypes', key.toString()); + MediaTypesMap(_mediaTypesMap, { rawLocation, key }) { + registerComponent(rawLocation, 'mediaTypes', key.toString()); }, }, }; diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml b/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml index c5909327c4..6e90e6ae31 100644 --- a/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml +++ b/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml @@ -5,13 +5,10 @@ info: paths: /users: get: - operationId: getUsers - summary: Get a list of users parameters: - $ref: parameters.yaml#/Param responses: '200': - description: A list of users content: application/json: schema: {} diff --git a/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt index fded788c30..a84335aa81 100644 --- a/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt +++ b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt @@ -5,13 +5,10 @@ info: paths: /users: get: - operationId: getUsers - summary: Get a list of users parameters: - $ref: '#/components/parameters/Param' responses: '200': - description: A list of users content: application/json: schema: {} diff --git a/tests/e2e/bundle/remove-unused-schema-alias/openapi.yaml b/tests/e2e/bundle/remove-unused-schema-alias/openapi.yaml new file mode 100644 index 0000000000..f4b29c07c4 --- /dev/null +++ b/tests/e2e/bundle/remove-unused-schema-alias/openapi.yaml @@ -0,0 +1,21 @@ +openapi: 3.1.0 +info: + title: Sample API + version: 1.0.0 +paths: + /users: + get: + operationId: getUsers + summary: Get a list of users + parameters: + - $ref: parameters.yaml#/Param + responses: + '200': + description: A list of users + content: + application/json: + schema: {} +components: + schemas: + User: + $ref: '#/components/schemas/Schema' diff --git a/tests/e2e/bundle/remove-unused-schema-alias/parameters.yaml b/tests/e2e/bundle/remove-unused-schema-alias/parameters.yaml new file mode 100644 index 0000000000..04188a821e --- /dev/null +++ b/tests/e2e/bundle/remove-unused-schema-alias/parameters.yaml @@ -0,0 +1,6 @@ +Param: + name: param + in: query + required: true + schema: + $ref: schemas.yaml#/Schema diff --git a/tests/e2e/bundle/remove-unused-schema-alias/redocly.yaml b/tests/e2e/bundle/remove-unused-schema-alias/redocly.yaml new file mode 100644 index 0000000000..3c417243f8 --- /dev/null +++ b/tests/e2e/bundle/remove-unused-schema-alias/redocly.yaml @@ -0,0 +1,6 @@ +apis: + test-api: + root: openapi.yaml + +decorators: + remove-unused-components: on diff --git a/tests/e2e/bundle/remove-unused-schema-alias/schemas.yaml b/tests/e2e/bundle/remove-unused-schema-alias/schemas.yaml new file mode 100644 index 0000000000..72170c47b6 --- /dev/null +++ b/tests/e2e/bundle/remove-unused-schema-alias/schemas.yaml @@ -0,0 +1,2 @@ +Schema: + type: string diff --git a/tests/e2e/bundle/remove-unused-schema-alias/snapshot.txt b/tests/e2e/bundle/remove-unused-schema-alias/snapshot.txt new file mode 100644 index 0000000000..ae192b2ad8 --- /dev/null +++ b/tests/e2e/bundle/remove-unused-schema-alias/snapshot.txt @@ -0,0 +1,32 @@ +openapi: 3.1.0 +info: + title: Sample API + version: 1.0.0 +paths: + /users: + get: + operationId: getUsers + summary: Get a list of users + parameters: + - $ref: '#/components/parameters/Param' + responses: + '200': + description: A list of users + content: + application/json: + schema: {} +components: + schemas: + Schema: + type: string + parameters: + Param: + name: param + in: query + required: true + schema: + $ref: '#/components/schemas/Schema' + +bundling openapi.yaml using configuration for api 'test-api'... +📦 Created a bundle for openapi.yaml at stdout ms. +🧹 Removed 1 unused components. diff --git a/tests/e2e/commands.test.ts b/tests/e2e/commands.test.ts index 982de6d6ce..982e949ced 100644 --- a/tests/e2e/commands.test.ts +++ b/tests/e2e/commands.test.ts @@ -752,6 +752,16 @@ describe('E2E', () => { }); }); + describe('bundle with unused named schema that is a $ref alias', () => { + it('should remove the alias schema when it is not referenced anywhere', async () => { + const testPath = join(__dirname, `bundle/remove-unused-schema-alias`); + const args = getParams(indexEntryPoint, ['bundle', '--config=redocly.yaml']); + + const result = getCommandOutput(args, { testPath }); + await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt')); + }); + }); + describe('miscellaneous', () => { const folderPath = join(__dirname, 'miscellaneous');