@scalar/json-magic 0.6.1 → 0.7.0

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.
Files changed (45) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +13 -0
  3. package/dist/bundle/bundle.d.ts +1 -0
  4. package/dist/bundle/bundle.d.ts.map +1 -1
  5. package/dist/bundle/bundle.js +3 -3
  6. package/dist/bundle/bundle.js.map +2 -2
  7. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
  8. package/dist/bundle/plugins/fetch-urls/index.js +2 -1
  9. package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
  10. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
  11. package/dist/bundle/plugins/parse-json/index.js +7 -6
  12. package/dist/bundle/plugins/parse-json/index.js.map +2 -2
  13. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
  14. package/dist/bundle/plugins/parse-yaml/index.js +7 -6
  15. package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
  16. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
  17. package/dist/bundle/plugins/read-files/index.js +2 -1
  18. package/dist/bundle/plugins/read-files/index.js.map +2 -2
  19. package/dist/diff/diff.d.ts.map +1 -1
  20. package/dist/diff/diff.js.map +2 -2
  21. package/dist/diff/utils.d.ts.map +1 -1
  22. package/dist/diff/utils.js.map +2 -2
  23. package/dist/magic-proxy/proxy.d.ts +1 -0
  24. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  25. package/dist/magic-proxy/proxy.js +11 -1
  26. package/dist/magic-proxy/proxy.js.map +2 -2
  27. package/package.json +4 -4
  28. package/src/bundle/bundle.test.ts +278 -259
  29. package/src/bundle/bundle.ts +4 -4
  30. package/src/bundle/plugins/fetch-urls/index.test.ts +21 -24
  31. package/src/bundle/plugins/fetch-urls/index.ts +1 -0
  32. package/src/bundle/plugins/parse-json/index.test.ts +3 -1
  33. package/src/bundle/plugins/parse-json/index.ts +7 -6
  34. package/src/bundle/plugins/parse-yaml/index.test.ts +3 -1
  35. package/src/bundle/plugins/parse-yaml/index.ts +7 -6
  36. package/src/bundle/plugins/read-files/index.test.ts +4 -3
  37. package/src/bundle/plugins/read-files/index.ts +1 -0
  38. package/src/bundle/value-generator.test.ts +7 -8
  39. package/src/dereference/dereference.test.ts +6 -6
  40. package/src/diff/diff.ts +0 -1
  41. package/src/diff/utils.test.ts +2 -2
  42. package/src/diff/utils.ts +0 -2
  43. package/src/helpers/escape-json-pointer.test.ts +1 -1
  44. package/src/helpers/unescape-json-pointer.test.ts +1 -1
  45. package/src/magic-proxy/proxy.ts +14 -0
@@ -65,7 +65,7 @@ export function isLocalRef(value: string): boolean {
65
65
  return value.startsWith('#')
66
66
  }
67
67
 
68
- export type ResolveResult = { ok: true; data: unknown } | { ok: false }
68
+ export type ResolveResult = { ok: true; data: unknown; raw: string } | { ok: false }
69
69
 
70
70
  /**
71
71
  * Resolves a string by finding and executing the appropriate plugin.
@@ -80,16 +80,16 @@ export type ResolveResult = { ok: true; data: unknown } | { ok: false }
80
80
  * // No matching plugin returns { ok: false }
81
81
  * await resolveContents('#/components/schemas/User', [urlPlugin, filePlugin])
82
82
  */
83
- async function resolveContents(value: string, plugins: LoaderPlugin[]): Promise<ResolveResult> {
83
+ function resolveContents(value: string, plugins: LoaderPlugin[]): Promise<ResolveResult> {
84
84
  const plugin = plugins.find((p) => p.validate(value))
85
85
 
86
86
  if (plugin) {
87
87
  return plugin.exec(value)
88
88
  }
89
89
 
90
- return {
90
+ return Promise.resolve({
91
91
  ok: false,
92
- }
92
+ })
93
93
  }
94
94
 
95
95
  /**
@@ -1,9 +1,10 @@
1
- import { fastify, type FastifyInstance } from 'fastify'
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
- import { fetchUrl } from '.'
4
- import assert from 'node:assert'
5
1
  import { setTimeout } from 'node:timers/promises'
6
2
 
3
+ import { type FastifyInstance, fastify } from 'fastify'
4
+ import { assert, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { fetchUrl } from '.'
7
+
7
8
  describe('fetchUrl', () => {
8
9
  const noLimit = <T>(fn: () => Promise<T>) => fn()
9
10
 
@@ -12,11 +13,11 @@ describe('fetchUrl', () => {
12
13
 
13
14
  beforeEach(() => {
14
15
  server = fastify({ logger: false })
15
- })
16
16
 
17
- afterEach(async () => {
18
- await server.close()
19
- await setTimeout(100)
17
+ return async () => {
18
+ await server.close()
19
+ await setTimeout(100)
20
+ }
20
21
  })
21
22
 
22
23
  it('reads json response', async () => {
@@ -71,14 +72,14 @@ describe('fetchUrl', () => {
71
72
 
72
73
  it('send headers to the specified domain', async () => {
73
74
  const url = `http://localhost:${PORT}`
74
- const fn = vi.fn()
75
+ const headersSpy = vi.fn()
75
76
 
76
77
  const response = {
77
78
  message: '200OK',
78
79
  }
79
80
 
80
81
  server.get('/', (request, reply) => {
81
- fn(request.headers)
82
+ headersSpy(request.headers)
82
83
  reply.send(response)
83
84
  })
84
85
 
@@ -87,8 +88,8 @@ describe('fetchUrl', () => {
87
88
  headers: [{ headers: { 'Authorization': 'Bearer <TOKEN>' }, domains: [`localhost:${PORT}`] }],
88
89
  })
89
90
 
90
- expect(fn).toHaveBeenCalled()
91
- expect(fn.mock.calls[0][0]).toEqual({
91
+ expect(headersSpy).toHaveBeenCalledOnce()
92
+ expect(headersSpy).toHaveBeenCalledWith({
92
93
  'accept': '*/*',
93
94
  'accept-encoding': 'gzip, deflate',
94
95
  'accept-language': '*',
@@ -102,14 +103,14 @@ describe('fetchUrl', () => {
102
103
 
103
104
  it('does not send headers to other domains', async () => {
104
105
  const url = `http://localhost:${PORT}`
105
- const fn = vi.fn()
106
+ const headersSpy = vi.fn()
106
107
 
107
108
  const response = {
108
109
  message: '200OK',
109
110
  }
110
111
 
111
112
  server.get('/', (request, reply) => {
112
- fn(request.headers)
113
+ headersSpy(request.headers)
113
114
  reply.send(response)
114
115
  })
115
116
 
@@ -118,8 +119,8 @@ describe('fetchUrl', () => {
118
119
  headers: [{ headers: { 'Authorization': 'Bearer <TOKEN>' }, domains: ['localhost:9932', 'localhost'] }],
119
120
  })
120
121
 
121
- expect(fn).toHaveBeenCalled()
122
- expect(fn.mock.calls[0][0]).toEqual({
122
+ expect(headersSpy).toHaveBeenCalledOnce()
123
+ expect(headersSpy).toHaveBeenNthCalledWith(1, {
123
124
  'accept': '*/*',
124
125
  'accept-encoding': 'gzip, deflate',
125
126
  'accept-language': '*',
@@ -131,17 +132,13 @@ describe('fetchUrl', () => {
131
132
  })
132
133
 
133
134
  it('runs custom fetcher', async () => {
134
- const fn = vi.fn()
135
+ const customFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }))
135
136
 
136
137
  await fetchUrl('https://example.com', (fn) => fn(), {
137
- fetch: async (input, init) => {
138
- fn(input, init)
139
-
140
- return new Response('{}', { status: 200 })
141
- },
138
+ fetch: customFetch,
142
139
  })
143
140
 
144
- expect(fn).toHaveBeenCalled()
145
- expect(fn).toHaveBeenCalledWith('https://example.com', { headers: undefined })
141
+ expect(customFetch).toHaveBeenCalledOnce()
142
+ expect(customFetch).toHaveBeenCalledWith('https://example.com', { headers: undefined })
146
143
  })
147
144
  })
@@ -59,6 +59,7 @@ export async function fetchUrl(
59
59
  return {
60
60
  ok: true,
61
61
  data: normalize(body),
62
+ raw: body,
62
63
  }
63
64
  }
64
65
 
@@ -1,6 +1,7 @@
1
- import { parseJson } from '@/bundle/plugins/parse-json'
2
1
  import { describe, expect, it } from 'vitest'
3
2
 
3
+ import { parseJson } from '@/bundle/plugins/parse-json'
4
+
4
5
  describe('parse-json', () => {
5
6
  it.each([
6
7
  ['{}', true],
@@ -17,6 +18,7 @@ describe('parse-json', () => {
17
18
  expect(await parseJson().exec('{ "message": "Hello World" }')).toEqual({
18
19
  ok: true,
19
20
  data: { message: 'Hello World' },
21
+ 'raw': '{ "message": "Hello World" }',
20
22
  })
21
23
  })
22
24
  })
@@ -1,4 +1,4 @@
1
- import type { LoaderPlugin, ResolveResult } from '@/bundle'
1
+ import type { LoaderPlugin } from '@/bundle'
2
2
  import { isJsonObject } from '@/helpers/is-json-object'
3
3
 
4
4
  /**
@@ -15,16 +15,17 @@ export function parseJson(): LoaderPlugin {
15
15
  return {
16
16
  type: 'loader',
17
17
  validate: isJsonObject,
18
- exec: async (value): Promise<ResolveResult> => {
18
+ exec: (value) => {
19
19
  try {
20
- return {
20
+ return Promise.resolve({
21
21
  ok: true,
22
22
  data: JSON.parse(value),
23
- }
23
+ raw: value,
24
+ })
24
25
  } catch {
25
- return {
26
+ return Promise.resolve({
26
27
  ok: false,
27
- }
28
+ })
28
29
  }
29
30
  },
30
31
  }
@@ -1,6 +1,7 @@
1
- import { parseYaml } from '@/bundle/plugins/parse-yaml'
2
1
  import { describe, expect, it } from 'vitest'
3
2
 
3
+ import { parseYaml } from '@/bundle/plugins/parse-yaml'
4
+
4
5
  describe('parse-yaml', () => {
5
6
  it.each([
6
7
  ['hi: hello\n', true],
@@ -19,6 +20,7 @@ describe('parse-yaml', () => {
19
20
  expect(await parseYaml().exec('{ "message": "Hello World" }')).toEqual({
20
21
  ok: true,
21
22
  data: { message: 'Hello World' },
23
+ 'raw': '{ "message": "Hello World" }',
22
24
  })
23
25
  })
24
26
  })
@@ -1,6 +1,6 @@
1
1
  import YAML from 'yaml'
2
2
 
3
- import type { LoaderPlugin, ResolveResult } from '@/bundle'
3
+ import type { LoaderPlugin } from '@/bundle'
4
4
  import { isYaml } from '@/helpers/is-yaml'
5
5
 
6
6
  /**
@@ -17,16 +17,17 @@ export function parseYaml(): LoaderPlugin {
17
17
  return {
18
18
  type: 'loader',
19
19
  validate: isYaml,
20
- exec: async (value): Promise<ResolveResult> => {
20
+ exec: (value) => {
21
21
  try {
22
- return {
22
+ return Promise.resolve({
23
23
  ok: true,
24
24
  data: YAML.parse(value, { merge: true, maxAliasCount: 10000 }),
25
- }
25
+ raw: value,
26
+ })
26
27
  } catch {
27
- return {
28
+ return Promise.resolve({
28
29
  ok: false,
29
- }
30
+ })
30
31
  }
31
32
  },
32
33
  }
@@ -1,8 +1,9 @@
1
- import fs from 'node:fs/promises'
2
1
  import { randomUUID } from 'node:crypto'
3
- import { describe, expect, it } from 'vitest'
2
+ import fs from 'node:fs/promises'
3
+
4
+ import { assert, describe, expect, it } from 'vitest'
5
+
4
6
  import { readFile } from '.'
5
- import assert from 'node:assert'
6
7
 
7
8
  describe('readFile', () => {
8
9
  it('reads json contents of a file', async () => {
@@ -29,6 +29,7 @@ export async function readFile(path: string): Promise<ResolveResult> {
29
29
  return {
30
30
  ok: true,
31
31
  data: normalize(fileContents),
32
+ raw: fileContents,
32
33
  }
33
34
  } catch {
34
35
  return {
@@ -1,9 +1,10 @@
1
- import { generateUniqueValue, uniqueValueGeneratorFactory } from './value-generator'
2
1
  import { describe, expect, it } from 'vitest'
3
2
 
3
+ import { generateUniqueValue, uniqueValueGeneratorFactory } from './value-generator'
4
+
4
5
  describe('generateUniqueHash', () => {
5
6
  it('should generate hash values from the function we pass in', async () => {
6
- const hashFunction = async (value: string) => {
7
+ const hashFunction = (value: string) => {
7
8
  if (value === 'a') {
8
9
  return 'a'
9
10
  }
@@ -16,7 +17,7 @@ describe('generateUniqueHash', () => {
16
17
  })
17
18
 
18
19
  it('should return same value for the same input', async () => {
19
- const hashFunction = async (value: string) => {
20
+ const hashFunction = (value: string) => {
20
21
  if (value === 'a') {
21
22
  return value
22
23
  }
@@ -33,7 +34,7 @@ describe('generateUniqueHash', () => {
33
34
  })
34
35
 
35
36
  it('should handle hash collisions', async () => {
36
- const hashFunction = async (value: string) => {
37
+ const hashFunction = (value: string) => {
37
38
  // Hash a, b and c will produce collisions
38
39
  if (value === 'a' || value === 'b' || value === 'c') {
39
40
  return 'd'
@@ -83,9 +84,7 @@ describe('generateUniqueHash', () => {
83
84
  })
84
85
 
85
86
  it('should throw when it reaches max depth', async () => {
86
- const hashFunction = async () => {
87
- return 'a'
88
- }
87
+ const hashFunction = () => 'a'
89
88
 
90
89
  const map = {}
91
90
  const result1 = await generateUniqueValue(hashFunction, 'a', map)
@@ -94,7 +93,7 @@ describe('generateUniqueHash', () => {
94
93
  const result2 = await generateUniqueValue(hashFunction, 'a', map)
95
94
  expect(result2).toBe('a')
96
95
 
97
- expect(() => generateUniqueValue(hashFunction, 'b', map)).rejects.toThrowError()
96
+ await expect(() => generateUniqueValue(hashFunction, 'b', map)).rejects.toThrowError()
98
97
  })
99
98
  })
100
99
 
@@ -1,11 +1,13 @@
1
- import { dereference } from '@/dereference/dereference'
2
- import { fastify, type FastifyInstance } from 'fastify'
3
1
  import { setTimeout } from 'node:timers/promises'
2
+
3
+ import { type FastifyInstance, fastify } from 'fastify'
4
4
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
5
 
6
+ import { dereference } from '@/dereference/dereference'
7
+
6
8
  describe('dereference', () => {
7
9
  describe('sync', () => {
8
- it('should dereference JSON pointers', async () => {
10
+ it('should dereference JSON pointers', () => {
9
11
  const data = {
10
12
  users: {
11
13
  name: 'John Doe',
@@ -82,9 +84,7 @@ describe('dereference', () => {
82
84
  street: 'Sunset Boulevard',
83
85
  },
84
86
  }
85
- server.get('/users', async () => {
86
- return userProfile
87
- })
87
+ server.get('/users', () => userProfile)
88
88
 
89
89
  await server.listen({ port: port })
90
90
 
package/src/diff/diff.ts CHANGED
@@ -76,7 +76,6 @@ export const diff = <T extends Record<string, unknown>>(doc1: Record<string, unk
76
76
  const keys = new Set([...Object.keys(el1), ...Object.keys(el2)])
77
77
 
78
78
  for (const key of keys) {
79
- // @ts-ignore
80
79
  bfs(el1[key], el2[key], [...prefix, key])
81
80
  }
82
81
  return
@@ -148,7 +148,7 @@ describe('isArrayEqual', () => {
148
148
  [1, 2, 3],
149
149
  [1, 2, 3],
150
150
  ],
151
- // @ts-ignore
151
+ // @ts-expect-error
152
152
  ])('should return true', (a, b) => expect(isArrayEqual(a, b)).toEqual(true))
153
153
 
154
154
  test.each([
@@ -164,6 +164,6 @@ describe('isArrayEqual', () => {
164
164
  [2, 2, 4],
165
165
  [1, 2, 3],
166
166
  ],
167
- // @ts-ignore
167
+ // @ts-expect-error
168
168
  ])('should return false', (a, b) => expect(isArrayEqual(a, b)).toEqual(false))
169
169
  })
package/src/diff/utils.ts CHANGED
@@ -28,9 +28,7 @@ export const isKeyCollisions = (a: unknown, b: unknown) => {
28
28
  const keys = new Set([...Object.keys(a), ...Object.keys(b)])
29
29
 
30
30
  for (const key of keys) {
31
- // @ts-ignore
32
31
  if (a[key] !== undefined && b[key] !== undefined) {
33
- // @ts-ignore
34
32
  if (isKeyCollisions(a[key], b[key])) {
35
33
  return true
36
34
  }
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import { escapeJsonPointer } from './escape-json-pointer'
4
4
 
5
- describe('escapeJsonPointer', async () => {
5
+ describe('escapeJsonPointer', () => {
6
6
  it('should escape a slash', () => {
7
7
  expect(escapeJsonPointer('application/json')).toBe('application~1json')
8
8
  })
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import { unescapeJsonPointer } from './unescape-json-pointer'
4
4
 
5
- describe('unescapeJsonPointer', async () => {
5
+ describe('unescapeJsonPointer', () => {
6
6
  it('unescapes a slash', () => {
7
7
  expect(unescapeJsonPointer('/foo~1bar~1baz')).toBe('/foo/bar/baz')
8
8
  })
@@ -140,6 +140,10 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
140
140
 
141
141
  // Resolve the reference and create a new magic proxy
142
142
  const resolvedValue = getValueByPath(args.root, parseJsonPointer(`#/${path}`))
143
+ // Return early if the value is already a magic proxy
144
+ if (isMagicProxyObject(resolvedValue.value)) {
145
+ return resolvedValue.value
146
+ }
143
147
  const proxiedValue = createMagicProxy(resolvedValue.value, options, {
144
148
  ...args,
145
149
  currentContext: resolvedValue.context,
@@ -152,6 +156,12 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
152
156
 
153
157
  // For all other properties, recursively wrap the value in a magic proxy
154
158
  const value = Reflect.get(target, prop, receiver)
159
+
160
+ // Return early if the value is already a magic proxy
161
+ if (isMagicProxyObject(value)) {
162
+ return value
163
+ }
164
+
155
165
  return createMagicProxy(value as T, options, { ...args, currentContext: id ?? args.currentContext })
156
166
  },
157
167
  /**
@@ -286,6 +296,10 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
286
296
  return proxied
287
297
  }
288
298
 
299
+ export const isMagicProxyObject = (obj: unknown): boolean => {
300
+ return typeof obj === 'object' && obj !== null && (obj as { [isMagicProxy]: boolean })[isMagicProxy] === true
301
+ }
302
+
289
303
  /**
290
304
  * Gets the raw (non-proxied) version of an object created by createMagicProxy.
291
305
  * This is useful when you need to access the original object without the magic proxy wrapper.