@scalar/json-magic 0.6.1 β†’ 0.8.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 (67) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +26 -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 +4 -4
  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/bundle/value-generator.d.ts +6 -4
  20. package/dist/bundle/value-generator.d.ts.map +1 -1
  21. package/dist/bundle/value-generator.js +2 -5
  22. package/dist/bundle/value-generator.js.map +2 -2
  23. package/dist/diff/diff.d.ts.map +1 -1
  24. package/dist/diff/diff.js.map +2 -2
  25. package/dist/diff/utils.d.ts.map +1 -1
  26. package/dist/diff/utils.js.map +2 -2
  27. package/dist/helpers/generate-hash.d.ts +11 -0
  28. package/dist/helpers/generate-hash.d.ts.map +1 -0
  29. package/dist/helpers/generate-hash.js +16 -0
  30. package/dist/helpers/generate-hash.js.map +7 -0
  31. package/dist/magic-proxy/proxy.d.ts +1 -0
  32. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  33. package/dist/magic-proxy/proxy.js +11 -1
  34. package/dist/magic-proxy/proxy.js.map +2 -2
  35. package/esbuild.ts +1 -0
  36. package/package.json +10 -4
  37. package/src/bundle/bundle.test.ts +293 -274
  38. package/src/bundle/bundle.ts +6 -5
  39. package/src/bundle/plugins/fetch-urls/index.test.ts +21 -24
  40. package/src/bundle/plugins/fetch-urls/index.ts +1 -0
  41. package/src/bundle/plugins/parse-json/index.test.ts +3 -1
  42. package/src/bundle/plugins/parse-json/index.ts +7 -6
  43. package/src/bundle/plugins/parse-yaml/index.test.ts +3 -1
  44. package/src/bundle/plugins/parse-yaml/index.ts +7 -6
  45. package/src/bundle/plugins/read-files/index.test.ts +4 -3
  46. package/src/bundle/plugins/read-files/index.ts +1 -0
  47. package/src/bundle/value-generator.test.ts +7 -8
  48. package/src/bundle/value-generator.ts +11 -15
  49. package/src/dereference/dereference.test.ts +10 -10
  50. package/src/diff/diff.ts +0 -1
  51. package/src/diff/utils.test.ts +2 -2
  52. package/src/diff/utils.ts +0 -2
  53. package/src/helpers/escape-json-pointer.test.ts +1 -1
  54. package/src/helpers/generate-hash.test.ts +74 -0
  55. package/src/helpers/generate-hash.ts +29 -0
  56. package/src/helpers/unescape-json-pointer.test.ts +1 -1
  57. package/src/magic-proxy/proxy.ts +14 -0
  58. package/dist/polyfills/index.d.ts +0 -2
  59. package/dist/polyfills/index.d.ts.map +0 -1
  60. package/dist/polyfills/index.js +0 -25
  61. package/dist/polyfills/index.js.map +0 -7
  62. package/dist/polyfills/path.d.ts +0 -24
  63. package/dist/polyfills/path.d.ts.map +0 -1
  64. package/dist/polyfills/path.js +0 -174
  65. package/dist/polyfills/path.js.map +0 -7
  66. package/src/polyfills/index.ts +0 -12
  67. package/src/polyfills/path.ts +0 -248
@@ -1,7 +1,8 @@
1
+ import { path } from '@scalar/helpers/node/path'
2
+
1
3
  import { convertToLocalRef } from '@/helpers/convert-to-local-ref'
2
4
  import { getId, getSchemas } from '@/helpers/get-schemas'
3
5
  import { getValueByPath } from '@/helpers/get-value-by-path'
4
- import path from '@/polyfills/path'
5
6
  import type { UnknownObject } from '@/types'
6
7
 
7
8
  import { escapeJsonPointer } from '../helpers/escape-json-pointer'
@@ -65,7 +66,7 @@ export function isLocalRef(value: string): boolean {
65
66
  return value.startsWith('#')
66
67
  }
67
68
 
68
- export type ResolveResult = { ok: true; data: unknown } | { ok: false }
69
+ export type ResolveResult = { ok: true; data: unknown; raw: string } | { ok: false }
69
70
 
70
71
  /**
71
72
  * Resolves a string by finding and executing the appropriate plugin.
@@ -80,16 +81,16 @@ export type ResolveResult = { ok: true; data: unknown } | { ok: false }
80
81
  * // No matching plugin returns { ok: false }
81
82
  * await resolveContents('#/components/schemas/User', [urlPlugin, filePlugin])
82
83
  */
83
- async function resolveContents(value: string, plugins: LoaderPlugin[]): Promise<ResolveResult> {
84
+ function resolveContents(value: string, plugins: LoaderPlugin[]): Promise<ResolveResult> {
84
85
  const plugin = plugins.find((p) => p.validate(value))
85
86
 
86
87
  if (plugin) {
87
88
  return plugin.exec(value)
88
89
  }
89
90
 
90
- return {
91
+ return Promise.resolve({
91
92
  ok: false,
92
- }
93
+ })
93
94
  }
94
95
 
95
96
  /**
@@ -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,27 +1,23 @@
1
+ import { generateHash } from '@/helpers/generate-hash'
2
+
1
3
  /**
2
- * Generates a short SHA-1 hash from a string value.
4
+ * Generates a short hash from a string value using xxhash.
5
+ *
3
6
  * This function is used to create unique identifiers for external references
4
- * while keeping the hash length manageable. It uses the Web Crypto API to
5
- * generate a SHA-1 hash and returns the first 7 characters of the hex string.
7
+ * while keeping the hash length manageable. It uses xxhash-wasm instead of
8
+ * crypto.subtle because crypto.subtle is only available in secure contexts (HTTPS) or on localhost.
9
+ * Returns the first 7 characters of the hash string.
6
10
  * If the hash would be all numbers, it ensures at least one letter is included.
7
11
  *
8
12
  * @param value - The string to hash
9
- * @returns A 7-character hexadecimal hash with at least one letter
13
+ * @returns A Promise that resolves to a 7-character hexadecimal hash with at least one letter
10
14
  * @example
11
15
  * // Returns "2ae91d7"
12
16
  * await getHash("https://example.com/schema.json")
13
17
  */
14
- export async function getHash(value: string) {
15
- // Convert string to ArrayBuffer
16
- const encoder = new TextEncoder()
17
- const data = encoder.encode(value)
18
-
19
- // Hash the data
20
- const hashBuffer = await crypto.subtle.digest('SHA-1', data)
21
-
22
- // Convert buffer to hex string
23
- const hashArray = Array.from(new Uint8Array(hashBuffer))
24
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
18
+ export async function getHash(value: string): Promise<string> {
19
+ // Hash the data using xxhash
20
+ const hashHex = await generateHash(value)
25
21
 
26
22
  // Return first 7 characters of the hash, ensuring at least one letter
27
23
  const hash = hashHex.substring(0, 7)
@@ -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
 
@@ -101,7 +101,7 @@ describe('dereference', () => {
101
101
  success: true,
102
102
  data: {
103
103
  profile: {
104
- '$ref': '#/x-ext/5bd1cdd',
104
+ '$ref': '#/x-ext/f87db7e',
105
105
  '$ref-value': {
106
106
  name: 'Jane Doe',
107
107
  age: 25,
@@ -112,17 +112,17 @@ describe('dereference', () => {
112
112
  },
113
113
  },
114
114
  address: {
115
- '$ref': '#/x-ext/5bd1cdd/address',
115
+ '$ref': '#/x-ext/f87db7e/address',
116
116
  '$ref-value': {
117
117
  city: 'Los Angeles',
118
118
  street: 'Sunset Boulevard',
119
119
  },
120
120
  },
121
121
  'x-ext': {
122
- '5bd1cdd': userProfile,
122
+ 'f87db7e': userProfile,
123
123
  },
124
124
  'x-ext-urls': {
125
- '5bd1cdd': `${url}/users`,
125
+ 'f87db7e': `${url}/users`,
126
126
  },
127
127
  },
128
128
  })
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
  })
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { generateHash } from './generate-hash'
4
+
5
+ describe('generateHash', () => {
6
+ it('generates a hash from a simple string', async () => {
7
+ const result = await generateHash('hello world')
8
+
9
+ // Should return a non-empty string
10
+ expect(result).toBeTruthy()
11
+ expect(typeof result).toBe('string')
12
+ expect(result.length).toBeGreaterThan(0)
13
+ })
14
+
15
+ it('produces consistent hashes for the same input', async () => {
16
+ const input = 'consistent-test-string'
17
+ const hash1 = await generateHash(input)
18
+ const hash2 = await generateHash(input)
19
+ const hash3 = await generateHash(input)
20
+
21
+ // Same input should always produce the same hash
22
+ expect(hash1).toBe(hash2)
23
+ expect(hash2).toBe(hash3)
24
+ })
25
+
26
+ it('produces different hashes for different inputs', async () => {
27
+ const hash1 = await generateHash('first string')
28
+ const hash2 = await generateHash('second string')
29
+ const hash3 = await generateHash('first strinG') // Case-sensitive
30
+
31
+ // Different inputs should produce different hashes
32
+ expect(hash1).not.toBe(hash2)
33
+ expect(hash1).not.toBe(hash3)
34
+ expect(hash2).not.toBe(hash3)
35
+ })
36
+
37
+ it('handles empty string', async () => {
38
+ const result = await generateHash('')
39
+
40
+ // Should handle empty strings without throwing
41
+ expect(result).toBeTruthy()
42
+ expect(typeof result).toBe('string')
43
+
44
+ // Empty string should produce a consistent hash
45
+ const result2 = await generateHash('')
46
+ expect(result).toBe(result2)
47
+ })
48
+
49
+ it('handles special characters and unicode', async () => {
50
+ const inputs = [
51
+ 'πŸš€ emoji test',
52
+ 'special chars: !@#$%^&*()',
53
+ 'unicode: γ“γ‚“γ«γ‘γ―δΈ–η•Œ',
54
+ 'newlines\nand\ttabs',
55
+ 'mixed: πŸ‘Ύ special!@# unicode: δ½ ε₯½',
56
+ ]
57
+
58
+ const hashes = await Promise.all(inputs.map((input) => generateHash(input)))
59
+
60
+ // All should produce valid hashes
61
+ hashes.forEach((hash) => {
62
+ expect(hash).toBeTruthy()
63
+ expect(typeof hash).toBe('string')
64
+ })
65
+
66
+ // All should be unique
67
+ const uniqueHashes = new Set(hashes)
68
+ expect(uniqueHashes.size).toBe(inputs.length)
69
+
70
+ // Should be consistent on repeated calls
71
+ const repeatedHash = await generateHash('πŸš€ emoji test')
72
+ expect(repeatedHash).toBe(hashes[0])
73
+ })
74
+ })
@@ -0,0 +1,29 @@
1
+ import xxhash from 'xxhash-wasm'
2
+
3
+ let hasherInstance: Awaited<ReturnType<typeof xxhash>> | null = null
4
+
5
+ /**
6
+ * Initialize the xxhash hasher instance lazily
7
+ *
8
+ * This is just a workaround because we cannot use top level await in a UMD module (standalone references)
9
+ */
10
+ const getHasher = async () => {
11
+ if (!hasherInstance) {
12
+ hasherInstance = await xxhash()
13
+ }
14
+ return hasherInstance
15
+ }
16
+
17
+ /**
18
+ * Generate a hash from a string using the xxhash algorithm
19
+ *
20
+ * We cannot use crypto.subtle because it is only available in secure contexts (HTTPS) or on localhost.
21
+ * So this is just a wrapper around the xxhash-wasm library instead.
22
+ *
23
+ * @param input - The string to hash
24
+ * @returns A Promise that resolves to the hash of the input string
25
+ */
26
+ export const generateHash = async (input: string): Promise<string> => {
27
+ const { h64ToString } = await getHasher()
28
+ return h64ToString(input)
29
+ }
@@ -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.
@@ -1,2 +0,0 @@
1
- export { resolve, normalize, isAbsolute, join, relative, sep, delimiter, dirname, basename, extname, } from './path.js';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/polyfills/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,SAAS,EACT,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,GAAG,EACH,SAAS,EACT,OAAO,EACP,QAAQ,EACR,OAAO,GACR,MAAM,QAAQ,CAAA"}
@@ -1,25 +0,0 @@
1
- import {
2
- resolve,
3
- normalize,
4
- isAbsolute,
5
- join,
6
- relative,
7
- sep,
8
- delimiter,
9
- dirname,
10
- basename,
11
- extname
12
- } from "./path.js";
13
- export {
14
- basename,
15
- delimiter,
16
- dirname,
17
- extname,
18
- isAbsolute,
19
- join,
20
- normalize,
21
- relative,
22
- resolve,
23
- sep
24
- };
25
- //# sourceMappingURL=index.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/polyfills/index.ts"],
4
- "sourcesContent": ["export {\n resolve,\n normalize,\n isAbsolute,\n join,\n relative,\n sep,\n delimiter,\n dirname,\n basename,\n extname,\n} from './path'\n"],
5
- "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
6
- "names": []
7
- }