diff --git a/.changeset/happy-chairs-drop.md b/.changeset/happy-chairs-drop.md new file mode 100644 index 0000000000..abe6d73e2c --- /dev/null +++ b/.changeset/happy-chairs-drop.md @@ -0,0 +1,5 @@ +--- +"@redocly/openapi-core": patch +--- + +Fixed an issue where the `remove-unused-components` decorator was failing to resolve `$ref` references to schemas from `components`. diff --git a/docs/@v2/commands/bundle.md b/docs/@v2/commands/bundle.md index 528087f5de..ce16d6ae45 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 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. diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index cdb7b06446..f6fa9afcdd 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -223,14 +223,17 @@ export function makeBundleVisitor({ } function resolveBundledComponent(node: OasRef, resolved: ResolveResult, ctx: UserContext) { - const newRefId = makeRefId(ctx.location.source.absoluteRef, node.$ref); - resolvedRefMap.set(newRefId, { + 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( 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 new file mode 100644 index 0000000000..6e90e6ae31 --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/openapi.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.0 +info: + title: Sample API + version: 1.0.0 +paths: + /users: + get: + parameters: + - $ref: parameters.yaml#/Param + responses: + '200': + 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..04188a821e --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/parameters.yaml @@ -0,0 +1,6 @@ +Param: + name: 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..a84335aa81 --- /dev/null +++ b/tests/e2e/bundle/parameters-reference-to-schemas/snapshot.txt @@ -0,0 +1,28 @@ +openapi: 3.0.0 +info: + title: Sample API + version: 1.0.0 +paths: + /users: + get: + parameters: + - $ref: '#/components/parameters/Param' + responses: + '200': + 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. 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 bd924712e6..982e949ced 100644 --- a/tests/e2e/commands.test.ts +++ b/tests/e2e/commands.test.ts @@ -742,6 +742,26 @@ 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('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');