From 50c6ee30471341945b92bdee049989c9555c3c76 Mon Sep 17 00:00:00 2001 From: carson2222 Date: Fri, 29 Aug 2025 12:51:02 +0200 Subject: [PATCH 1/4] fix: inline internal JSON Pointer refs under #/paths/ for OpenAPI bundling - Add shouldInlineInternal helper to determine when internal refs should be copied inline - Internal refs under #/paths/ are now dereferenced (copied) instead of left as - Components/schemas, definitions, and declarations still preserved as refs --- lib/bundle.ts | 20 +++++++++++++++++++- package.json | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/bundle.ts b/lib/bundle.ts index 4ad729a1..f65844f6 100644 --- a/lib/bundle.ts +++ b/lib/bundle.ts @@ -23,6 +23,24 @@ export interface InventoryEntry { value: any; } +/** + * Determines whether an internal $ref should be inlined (copied) rather than left as a $ref. + * Currently inlines OpenAPI path items ("#/paths/..."), while keeping components/definitions/declarations as refs. + */ +const shouldInlineInternal = (entry: InventoryEntry) => { + if (entry.external) { + return false; + } + const h = entry.hash as string | undefined; + if (!h || h === "#") { + return false; + } + if (h.startsWith("#/components/schemas") || h.indexOf("/definitions") !== -1 || h.startsWith("#/declarations")) { + return false; + } + return h.startsWith("#/paths/"); +}; + /** * TODO */ @@ -300,7 +318,7 @@ function remap(inventory: InventoryEntry[]) { for (const entry of inventory) { // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot); - if (!entry.external) { + if (!entry.external && !shouldInlineInternal(entry)) { // This $ref already resolves to the main JSON Schema file entry.$ref.$ref = entry.hash; } else if (entry.file === file && entry.hash === hash) { diff --git a/package.json b/package.json index 675fad54..863d8879 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ ], "scripts": { "build": "rimraf dist && tsc", + "dev": "rimraf dist && tsc --watch", "lint": "eslint lib", "prepublishOnly": "yarn build", "prettier": "prettier --write \"**/*.+(js|jsx|ts|tsx|har||json|css|md)\"", From 5fb8e24210d1735b642d25bff0f4dee98d6e0c54 Mon Sep 17 00:00:00 2001 From: carson2222 Date: Fri, 29 Aug 2025 13:12:06 +0200 Subject: [PATCH 2/4] chore: add test case --- lib/__tests__/pointer.test.ts | 26 +++++++++++++ lib/__tests__/spec/openapi-paths-ref.json | 46 +++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 lib/__tests__/pointer.test.ts create mode 100644 lib/__tests__/spec/openapi-paths-ref.json diff --git a/lib/__tests__/pointer.test.ts b/lib/__tests__/pointer.test.ts new file mode 100644 index 00000000..e8d8a718 --- /dev/null +++ b/lib/__tests__/pointer.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { $RefParser } from ".."; +import path from "path"; + +describe("pointer", () => { + it("inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling", async () => { + const refParser = new $RefParser(); + const pathOrUrlOrSchema = path.resolve("lib", "__tests__", "spec", "openapi-paths-ref.json"); + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; + + // The GET endpoint should have its schema defined inline + const getSchema = schema.paths["/foo"].get.responses["200"].content["application/json"].schema; + expect(getSchema.$ref).toBeUndefined(); + expect(getSchema.type).toBe("object"); + expect(getSchema.properties.bar.type).toBe("string"); + + // The POST endpoint should have its schema inlined (copied) instead of a $ref + const postSchema = schema.paths["/foo"].post.responses["200"].content["application/json"].schema; + expect(postSchema.$ref).toBeUndefined(); + expect(postSchema.type).toBe("object"); + expect(postSchema.properties.bar.type).toBe("string"); + + // Both schemas should be identical objects + expect(postSchema).toEqual(getSchema); + }); +}); diff --git a/lib/__tests__/spec/openapi-paths-ref.json b/lib/__tests__/spec/openapi-paths-ref.json new file mode 100644 index 00000000..e3b6bb90 --- /dev/null +++ b/lib/__tests__/spec/openapi-paths-ref.json @@ -0,0 +1,46 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample API", + "version": "1.0.0" + }, + "paths": { + "/foo": { + "get": { + "summary": "Get foo", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } + } + } + } + }, + "post": { + "summary": "Create foo", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/paths/~1foo/get/responses/200/content/application~1json/schema" + } + } + } + } + } + } + } + } +} From a549ba5463e94ee02cbd4eb66f6ff3910997a5fa Mon Sep 17 00:00:00 2001 From: carson2222 Date: Mon, 1 Sep 2025 11:02:05 +0200 Subject: [PATCH 3/4] chore: code cleanup --- lib/bundle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bundle.ts b/lib/bundle.ts index f65844f6..874ea78e 100644 --- a/lib/bundle.ts +++ b/lib/bundle.ts @@ -35,7 +35,7 @@ const shouldInlineInternal = (entry: InventoryEntry) => { if (!h || h === "#") { return false; } - if (h.startsWith("#/components/schemas") || h.indexOf("/definitions") !== -1 || h.startsWith("#/declarations")) { + if (h.startsWith("#/components/schemas") || h.startsWith("#/definitions")) { return false; } return h.startsWith("#/paths/"); From d6f843878ef5a6618543c1ed7e240b70220cfc0e Mon Sep 17 00:00:00 2001 From: carson2222 Date: Mon, 1 Sep 2025 11:40:46 +0200 Subject: [PATCH 4/4] chore: code cleanup --- lib/bundle.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/lib/bundle.ts b/lib/bundle.ts index 874ea78e..911dcf67 100644 --- a/lib/bundle.ts +++ b/lib/bundle.ts @@ -23,24 +23,6 @@ export interface InventoryEntry { value: any; } -/** - * Determines whether an internal $ref should be inlined (copied) rather than left as a $ref. - * Currently inlines OpenAPI path items ("#/paths/..."), while keeping components/definitions/declarations as refs. - */ -const shouldInlineInternal = (entry: InventoryEntry) => { - if (entry.external) { - return false; - } - const h = entry.hash as string | undefined; - if (!h || h === "#") { - return false; - } - if (h.startsWith("#/components/schemas") || h.startsWith("#/definitions")) { - return false; - } - return h.startsWith("#/paths/"); -}; - /** * TODO */ @@ -318,7 +300,7 @@ function remap(inventory: InventoryEntry[]) { for (const entry of inventory) { // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot); - if (!entry.external && !shouldInlineInternal(entry)) { + if (!entry.external && !entry.hash?.startsWith("#/paths/")) { // This $ref already resolves to the main JSON Schema file entry.$ref.$ref = entry.hash; } else if (entry.file === file && entry.hash === hash) {