@hey-api/json-schema-ref-parser 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,14 +3,41 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const node_path_1 = __importDefault(require("node:path"));
6
+ const path_1 = __importDefault(require("path"));
7
7
  const vitest_1 = require("vitest");
8
8
  const __1 = require("..");
9
- (0, vitest_1.describe)('bundle', () => {
10
- (0, vitest_1.it)('handles circular reference with description', async () => {
9
+ (0, vitest_1.describe)("bundle", () => {
10
+ (0, vitest_1.it)("handles circular reference with description", async () => {
11
11
  const refParser = new __1.$RefParser();
12
- const pathOrUrlOrSchema = node_path_1.default.resolve('lib', '__tests__', 'spec', 'circular-ref-with-description.json');
12
+ const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "circular-ref-with-description.json");
13
13
  const schema = await refParser.bundle({ pathOrUrlOrSchema });
14
14
  (0, vitest_1.expect)(schema).not.toBeUndefined();
15
15
  });
16
+ (0, vitest_1.it)("bundles multiple references to the same file correctly", async () => {
17
+ const refParser = new __1.$RefParser();
18
+ const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "multiple-refs.json");
19
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema }));
20
+ // First reference should be fully resolved (no $ref)
21
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].name).toBe("pathId");
22
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.type).toBe("string");
23
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.format).toBe("uuid");
24
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].$ref).toBeUndefined();
25
+ // Second reference should be remapped to point to the first reference
26
+ (0, vitest_1.expect)(schema.paths["/test2/{pathId}"].get.parameters[0].$ref).toBe("#/paths/~1test1~1%7BpathId%7D/get/parameters/0");
27
+ // Both should effectively resolve to the same data
28
+ const firstParam = schema.paths["/test1/{pathId}"].get.parameters[0];
29
+ const secondParam = schema.paths["/test2/{pathId}"].get.parameters[0];
30
+ // The second parameter should resolve to the same data as the first
31
+ (0, vitest_1.expect)(secondParam.$ref).toBeDefined();
32
+ (0, vitest_1.expect)(firstParam).toEqual({
33
+ name: "pathId",
34
+ in: "path",
35
+ required: true,
36
+ schema: {
37
+ type: "string",
38
+ format: "uuid",
39
+ description: "Unique identifier for the path",
40
+ },
41
+ });
42
+ });
16
43
  });
@@ -6,38 +6,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const node_path_1 = __importDefault(require("node:path"));
7
7
  const vitest_1 = require("vitest");
8
8
  const index_1 = require("../index");
9
- (0, vitest_1.describe)('getResolvedInput', () => {
10
- (0, vitest_1.it)('handles url', async () => {
11
- const pathOrUrlOrSchema = 'https://foo.com';
9
+ (0, vitest_1.describe)("getResolvedInput", () => {
10
+ (0, vitest_1.it)("handles url", async () => {
11
+ const pathOrUrlOrSchema = "https://foo.com";
12
12
  const resolvedInput = await (0, index_1.getResolvedInput)({ pathOrUrlOrSchema });
13
- (0, vitest_1.expect)(resolvedInput.type).toBe('url');
13
+ (0, vitest_1.expect)(resolvedInput.type).toBe("url");
14
14
  (0, vitest_1.expect)(resolvedInput.schema).toBeUndefined();
15
- (0, vitest_1.expect)(resolvedInput.path).toBe('https://foo.com/');
15
+ (0, vitest_1.expect)(resolvedInput.path).toBe("https://foo.com/");
16
16
  });
17
- (0, vitest_1.it)('handles file', async () => {
18
- const pathOrUrlOrSchema = './path/to/openapi.json';
17
+ (0, vitest_1.it)("handles file", async () => {
18
+ const pathOrUrlOrSchema = "./path/to/openapi.json";
19
19
  const resolvedInput = await (0, index_1.getResolvedInput)({ pathOrUrlOrSchema });
20
- (0, vitest_1.expect)(resolvedInput.type).toBe('file');
20
+ (0, vitest_1.expect)(resolvedInput.type).toBe("file");
21
21
  (0, vitest_1.expect)(resolvedInput.schema).toBeUndefined();
22
- (0, vitest_1.expect)(resolvedInput.path).toBe(node_path_1.default.resolve('./path/to/openapi.json'));
22
+ (0, vitest_1.expect)(node_path_1.default.normalize(resolvedInput.path).toLowerCase()).toBe(node_path_1.default.normalize(node_path_1.default.resolve("./path/to/openapi.json")).toLowerCase());
23
23
  });
24
- (0, vitest_1.it)('handles raw spec', async () => {
24
+ (0, vitest_1.it)("handles raw spec", async () => {
25
25
  const pathOrUrlOrSchema = {
26
26
  info: {
27
- version: '1.0.0',
27
+ version: "1.0.0",
28
28
  },
29
- openapi: '3.1.0',
29
+ openapi: "3.1.0",
30
30
  paths: {},
31
31
  };
32
32
  const resolvedInput = await (0, index_1.getResolvedInput)({ pathOrUrlOrSchema });
33
- (0, vitest_1.expect)(resolvedInput.type).toBe('json');
33
+ (0, vitest_1.expect)(resolvedInput.type).toBe("json");
34
34
  (0, vitest_1.expect)(resolvedInput.schema).toEqual({
35
35
  info: {
36
- version: '1.0.0',
36
+ version: "1.0.0",
37
37
  },
38
- openapi: '3.1.0',
38
+ openapi: "3.1.0",
39
39
  paths: {},
40
40
  });
41
- (0, vitest_1.expect)(resolvedInput.path).toBe('');
41
+ (0, vitest_1.expect)(resolvedInput.path).toBe("");
42
42
  });
43
43
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const __1 = require("..");
8
+ const path_1 = __importDefault(require("path"));
9
+ (0, vitest_1.describe)("pointer", () => {
10
+ (0, vitest_1.it)("inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling", async () => {
11
+ const refParser = new __1.$RefParser();
12
+ const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "openapi-paths-ref.json");
13
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema }));
14
+ // The GET endpoint should have its schema defined inline
15
+ const getSchema = schema.paths["/foo"].get.responses["200"].content["application/json"].schema;
16
+ (0, vitest_1.expect)(getSchema.$ref).toBeUndefined();
17
+ (0, vitest_1.expect)(getSchema.type).toBe("object");
18
+ (0, vitest_1.expect)(getSchema.properties.bar.type).toBe("string");
19
+ // The POST endpoint should have its schema inlined (copied) instead of a $ref
20
+ const postSchema = schema.paths["/foo"].post.responses["200"].content["application/json"].schema;
21
+ (0, vitest_1.expect)(postSchema.$ref).toBeUndefined();
22
+ (0, vitest_1.expect)(postSchema.type).toBe("object");
23
+ (0, vitest_1.expect)(postSchema.properties.bar.type).toBe("string");
24
+ // Both schemas should be identical objects
25
+ (0, vitest_1.expect)(postSchema).toEqual(getSchema);
26
+ });
27
+ });
@@ -74,9 +74,10 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
74
74
  const external = file !== $refs._root$Ref.path;
75
75
  const extended = ref_js_1.default.isExtended$Ref($ref);
76
76
  indirections += pointer.indirections;
77
+ // Check if this exact location (parent + key + pathFromRoot) has already been inventoried
77
78
  const existingEntry = findInInventory(inventory, $refParent, $refKey);
78
- if (existingEntry) {
79
- // This $Ref has already been inventoried, so we don't need to process it again
79
+ if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
80
+ // This exact location has already been inventoried, so we don't need to process it again
80
81
  if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
81
82
  removeFromInventory(inventory, existingEntry);
82
83
  }
@@ -248,7 +249,7 @@ function remap(inventory) {
248
249
  let file, hash, pathFromRoot;
249
250
  for (const entry of inventory) {
250
251
  // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
251
- if (!entry.external) {
252
+ if (!entry.external && !entry.hash?.startsWith("#/paths/")) {
252
253
  // This $ref already resolves to the main JSON Schema file
253
254
  entry.$ref.$ref = entry.hash;
254
255
  }
@@ -312,7 +313,7 @@ const bundle = (parser, options) => {
312
313
  const inventory = [];
313
314
  crawl({
314
315
  parent: parser,
315
- key: 'schema',
316
+ key: "schema",
316
317
  path: parser.$refs._root$Ref.path + "#",
317
318
  pathFromRoot: "#",
318
319
  indirections: 0,
@@ -1,14 +1,48 @@
1
- import path from 'node:path';
1
+ import path from "path";
2
2
 
3
- import { describe, expect, it } from 'vitest';
3
+ import { describe, expect, it } from "vitest";
4
4
 
5
- import { $RefParser } from '..';
5
+ import { $RefParser } from "..";
6
6
 
7
- describe('bundle', () => {
8
- it('handles circular reference with description', async () => {
7
+ describe("bundle", () => {
8
+ it("handles circular reference with description", async () => {
9
9
  const refParser = new $RefParser();
10
- const pathOrUrlOrSchema = path.resolve('lib', '__tests__', 'spec', 'circular-ref-with-description.json');
10
+ const pathOrUrlOrSchema = path.resolve("lib", "__tests__", "spec", "circular-ref-with-description.json");
11
11
  const schema = await refParser.bundle({ pathOrUrlOrSchema });
12
12
  expect(schema).not.toBeUndefined();
13
13
  });
14
+
15
+ it("bundles multiple references to the same file correctly", async () => {
16
+ const refParser = new $RefParser();
17
+ const pathOrUrlOrSchema = path.resolve("lib", "__tests__", "spec", "multiple-refs.json");
18
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
19
+
20
+ // First reference should be fully resolved (no $ref)
21
+ expect(schema.paths["/test1/{pathId}"].get.parameters[0].name).toBe("pathId");
22
+ expect(schema.paths["/test1/{pathId}"].get.parameters[0].schema.type).toBe("string");
23
+ expect(schema.paths["/test1/{pathId}"].get.parameters[0].schema.format).toBe("uuid");
24
+ expect(schema.paths["/test1/{pathId}"].get.parameters[0].$ref).toBeUndefined();
25
+
26
+ // Second reference should be remapped to point to the first reference
27
+ expect(schema.paths["/test2/{pathId}"].get.parameters[0].$ref).toBe(
28
+ "#/paths/~1test1~1%7BpathId%7D/get/parameters/0",
29
+ );
30
+
31
+ // Both should effectively resolve to the same data
32
+ const firstParam = schema.paths["/test1/{pathId}"].get.parameters[0];
33
+ const secondParam = schema.paths["/test2/{pathId}"].get.parameters[0];
34
+
35
+ // The second parameter should resolve to the same data as the first
36
+ expect(secondParam.$ref).toBeDefined();
37
+ expect(firstParam).toEqual({
38
+ name: "pathId",
39
+ in: "path",
40
+ required: true,
41
+ schema: {
42
+ type: "string",
43
+ format: "uuid",
44
+ description: "Unique identifier for the path",
45
+ },
46
+ });
47
+ });
14
48
  });
@@ -1,43 +1,45 @@
1
- import path from 'node:path';
1
+ import path from "node:path";
2
2
 
3
- import { describe, expect, it } from 'vitest';
3
+ import { describe, expect, it } from "vitest";
4
4
 
5
- import { getResolvedInput } from '../index';
5
+ import { getResolvedInput } from "../index";
6
6
 
7
- describe('getResolvedInput', () => {
8
- it('handles url', async () => {
9
- const pathOrUrlOrSchema = 'https://foo.com';
7
+ describe("getResolvedInput", () => {
8
+ it("handles url", async () => {
9
+ const pathOrUrlOrSchema = "https://foo.com";
10
10
  const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
11
- expect(resolvedInput.type).toBe('url');
11
+ expect(resolvedInput.type).toBe("url");
12
12
  expect(resolvedInput.schema).toBeUndefined();
13
- expect(resolvedInput.path).toBe('https://foo.com/');
13
+ expect(resolvedInput.path).toBe("https://foo.com/");
14
14
  });
15
15
 
16
- it('handles file', async () => {
17
- const pathOrUrlOrSchema = './path/to/openapi.json';
16
+ it("handles file", async () => {
17
+ const pathOrUrlOrSchema = "./path/to/openapi.json";
18
18
  const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
19
- expect(resolvedInput.type).toBe('file');
19
+ expect(resolvedInput.type).toBe("file");
20
20
  expect(resolvedInput.schema).toBeUndefined();
21
- expect(resolvedInput.path).toBe(path.resolve('./path/to/openapi.json'));
21
+ expect(path.normalize(resolvedInput.path).toLowerCase()).toBe(
22
+ path.normalize(path.resolve("./path/to/openapi.json")).toLowerCase(),
23
+ );
22
24
  });
23
25
 
24
- it('handles raw spec', async () => {
25
- const pathOrUrlOrSchema = {
26
+ it("handles raw spec", async () => {
27
+ const pathOrUrlOrSchema = {
26
28
  info: {
27
- version: '1.0.0',
29
+ version: "1.0.0",
28
30
  },
29
- openapi: '3.1.0',
31
+ openapi: "3.1.0",
30
32
  paths: {},
31
33
  };
32
34
  const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
33
- expect(resolvedInput.type).toBe('json');
35
+ expect(resolvedInput.type).toBe("json");
34
36
  expect(resolvedInput.schema).toEqual({
35
37
  info: {
36
- version: '1.0.0',
38
+ version: "1.0.0",
37
39
  },
38
- openapi: '3.1.0',
40
+ openapi: "3.1.0",
39
41
  paths: {},
40
42
  });
41
- expect(resolvedInput.path).toBe('');
43
+ expect(resolvedInput.path).toBe("");
42
44
  });
43
45
  });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { $RefParser } from "..";
3
+ import path from "path";
4
+
5
+ describe("pointer", () => {
6
+ it("inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling", async () => {
7
+ const refParser = new $RefParser();
8
+ const pathOrUrlOrSchema = path.resolve("lib", "__tests__", "spec", "openapi-paths-ref.json");
9
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
10
+
11
+ // The GET endpoint should have its schema defined inline
12
+ const getSchema = schema.paths["/foo"].get.responses["200"].content["application/json"].schema;
13
+ expect(getSchema.$ref).toBeUndefined();
14
+ expect(getSchema.type).toBe("object");
15
+ expect(getSchema.properties.bar.type).toBe("string");
16
+
17
+ // The POST endpoint should have its schema inlined (copied) instead of a $ref
18
+ const postSchema = schema.paths["/foo"].post.responses["200"].content["application/json"].schema;
19
+ expect(postSchema.$ref).toBeUndefined();
20
+ expect(postSchema.type).toBe("object");
21
+ expect(postSchema.properties.bar.type).toBe("string");
22
+
23
+ // Both schemas should be identical objects
24
+ expect(postSchema).toEqual(getSchema);
25
+ });
26
+ });
@@ -0,0 +1,34 @@
1
+ {
2
+ "paths": {
3
+ "/test1/{pathId}": {
4
+ "get": {
5
+ "summary": "First endpoint using the same pathId schema",
6
+ "parameters": [
7
+ {
8
+ "$ref": "path-parameter.json#/pathId"
9
+ }
10
+ ],
11
+ "responses": {
12
+ "200": {
13
+ "description": "Test 1 response"
14
+ }
15
+ }
16
+ }
17
+ },
18
+ "/test2/{pathId}": {
19
+ "get": {
20
+ "summary": "Second endpoint using the same pathId schema",
21
+ "parameters": [
22
+ {
23
+ "$ref": "path-parameter.json#/pathId"
24
+ }
25
+ ],
26
+ "responses": {
27
+ "200": {
28
+ "description": "Test 2 response"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "openapi": "3.1.0",
3
+ "info": {
4
+ "title": "Sample API",
5
+ "version": "1.0.0"
6
+ },
7
+ "paths": {
8
+ "/foo": {
9
+ "get": {
10
+ "summary": "Get foo",
11
+ "responses": {
12
+ "200": {
13
+ "description": "OK",
14
+ "content": {
15
+ "application/json": {
16
+ "schema": {
17
+ "type": "object",
18
+ "properties": {
19
+ "bar": {
20
+ "type": "string"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "post": {
30
+ "summary": "Create foo",
31
+ "responses": {
32
+ "200": {
33
+ "description": "OK",
34
+ "content": {
35
+ "application/json": {
36
+ "schema": {
37
+ "$ref": "#/paths/~1foo/get/responses/200/content/application~1json/schema"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "pathId": {
3
+ "name": "pathId",
4
+ "in": "path",
5
+ "required": true,
6
+ "schema": {
7
+ "type": "string",
8
+ "format": "uuid",
9
+ "description": "Unique identifier for the path"
10
+ }
11
+ }
12
+ }
package/lib/bundle.ts CHANGED
@@ -94,9 +94,10 @@ const inventory$Ref = <S extends object = JSONSchema>({
94
94
  const extended = $Ref.isExtended$Ref($ref);
95
95
  indirections += pointer.indirections;
96
96
 
97
+ // Check if this exact location (parent + key + pathFromRoot) has already been inventoried
97
98
  const existingEntry = findInInventory(inventory, $refParent, $refKey);
98
- if (existingEntry) {
99
- // This $Ref has already been inventoried, so we don't need to process it again
99
+ if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
100
+ // This exact location has already been inventoried, so we don't need to process it again
100
101
  if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
101
102
  removeFromInventory(inventory, existingEntry);
102
103
  } else {
@@ -172,7 +173,7 @@ const crawl = <S extends object = JSONSchema>({
172
173
  pathFromRoot: string;
173
174
  }) => {
174
175
  const obj = key === null ? parent : parent[key as keyof typeof parent];
175
-
176
+
176
177
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
177
178
  if ($Ref.isAllowed$Ref(obj)) {
178
179
  inventory$Ref({
@@ -299,7 +300,7 @@ function remap(inventory: InventoryEntry[]) {
299
300
  for (const entry of inventory) {
300
301
  // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
301
302
 
302
- if (!entry.external) {
303
+ if (!entry.external && !entry.hash?.startsWith("#/paths/")) {
303
304
  // This $ref already resolves to the main JSON Schema file
304
305
  entry.$ref.$ref = entry.hash;
305
306
  } else if (entry.file === file && entry.hash === hash) {
@@ -359,17 +360,13 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
359
360
  * @param parser
360
361
  * @param options
361
362
  */
362
- export const bundle = (
363
- parser: $RefParser,
364
- options: ParserOptions,
365
- ) => {
363
+ export const bundle = (parser: $RefParser, options: ParserOptions) => {
366
364
  // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
367
-
368
365
  // Build an inventory of all $ref pointers in the JSON Schema
369
366
  const inventory: InventoryEntry[] = [];
370
367
  crawl<JSONSchema>({
371
368
  parent: parser,
372
- key: 'schema',
369
+ key: "schema",
373
370
  path: parser.$refs._root$Ref.path + "#",
374
371
  pathFromRoot: "#",
375
372
  indirections: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hey-api/json-schema-ref-parser",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "homepage": "https://heyapi.dev/",
6
6
  "repository": {
@@ -42,6 +42,7 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "build": "rimraf dist && tsc",
45
+ "dev": "rimraf dist && tsc --watch",
45
46
  "lint": "eslint lib",
46
47
  "prepublishOnly": "yarn build",
47
48
  "prettier": "prettier --write \"**/*.+(js|jsx|ts|tsx|har||json|css|md)\"",