@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +26 -0
- package/dist/bundle/bundle.d.ts +1 -0
- package/dist/bundle/bundle.d.ts.map +1 -1
- package/dist/bundle/bundle.js +4 -4
- package/dist/bundle/bundle.js.map +2 -2
- package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
- package/dist/bundle/plugins/fetch-urls/index.js +2 -1
- package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
- package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
- package/dist/bundle/plugins/parse-json/index.js +7 -6
- package/dist/bundle/plugins/parse-json/index.js.map +2 -2
- package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
- package/dist/bundle/plugins/parse-yaml/index.js +7 -6
- package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
- package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
- package/dist/bundle/plugins/read-files/index.js +2 -1
- package/dist/bundle/plugins/read-files/index.js.map +2 -2
- package/dist/bundle/value-generator.d.ts +6 -4
- package/dist/bundle/value-generator.d.ts.map +1 -1
- package/dist/bundle/value-generator.js +2 -5
- package/dist/bundle/value-generator.js.map +2 -2
- package/dist/diff/diff.d.ts.map +1 -1
- package/dist/diff/diff.js.map +2 -2
- package/dist/diff/utils.d.ts.map +1 -1
- package/dist/diff/utils.js.map +2 -2
- package/dist/helpers/generate-hash.d.ts +11 -0
- package/dist/helpers/generate-hash.d.ts.map +1 -0
- package/dist/helpers/generate-hash.js +16 -0
- package/dist/helpers/generate-hash.js.map +7 -0
- package/dist/magic-proxy/proxy.d.ts +1 -0
- package/dist/magic-proxy/proxy.d.ts.map +1 -1
- package/dist/magic-proxy/proxy.js +11 -1
- package/dist/magic-proxy/proxy.js.map +2 -2
- package/esbuild.ts +1 -0
- package/package.json +10 -4
- package/src/bundle/bundle.test.ts +293 -274
- package/src/bundle/bundle.ts +6 -5
- package/src/bundle/plugins/fetch-urls/index.test.ts +21 -24
- package/src/bundle/plugins/fetch-urls/index.ts +1 -0
- package/src/bundle/plugins/parse-json/index.test.ts +3 -1
- package/src/bundle/plugins/parse-json/index.ts +7 -6
- package/src/bundle/plugins/parse-yaml/index.test.ts +3 -1
- package/src/bundle/plugins/parse-yaml/index.ts +7 -6
- package/src/bundle/plugins/read-files/index.test.ts +4 -3
- package/src/bundle/plugins/read-files/index.ts +1 -0
- package/src/bundle/value-generator.test.ts +7 -8
- package/src/bundle/value-generator.ts +11 -15
- package/src/dereference/dereference.test.ts +10 -10
- package/src/diff/diff.ts +0 -1
- package/src/diff/utils.test.ts +2 -2
- package/src/diff/utils.ts +0 -2
- package/src/helpers/escape-json-pointer.test.ts +1 -1
- package/src/helpers/generate-hash.test.ts +74 -0
- package/src/helpers/generate-hash.ts +29 -0
- package/src/helpers/unescape-json-pointer.test.ts +1 -1
- package/src/magic-proxy/proxy.ts +14 -0
- package/dist/polyfills/index.d.ts +0 -2
- package/dist/polyfills/index.d.ts.map +0 -1
- package/dist/polyfills/index.js +0 -25
- package/dist/polyfills/index.js.map +0 -7
- package/dist/polyfills/path.d.ts +0 -24
- package/dist/polyfills/path.d.ts.map +0 -1
- package/dist/polyfills/path.js +0 -174
- package/dist/polyfills/path.js.map +0 -7
- package/src/polyfills/index.ts +0 -12
- package/src/polyfills/path.ts +0 -248
package/src/bundle/bundle.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
91
|
-
expect(
|
|
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
|
|
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
|
-
|
|
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(
|
|
122
|
-
expect(
|
|
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
|
|
135
|
+
const customFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }))
|
|
135
136
|
|
|
136
137
|
await fetchUrl('https://example.com', (fn) => fn(), {
|
|
137
|
-
fetch:
|
|
138
|
-
fn(input, init)
|
|
139
|
-
|
|
140
|
-
return new Response('{}', { status: 200 })
|
|
141
|
-
},
|
|
138
|
+
fetch: customFetch,
|
|
142
139
|
})
|
|
143
140
|
|
|
144
|
-
expect(
|
|
145
|
-
expect(
|
|
141
|
+
expect(customFetch).toHaveBeenCalledOnce()
|
|
142
|
+
expect(customFetch).toHaveBeenCalledWith('https://example.com', { headers: undefined })
|
|
146
143
|
})
|
|
147
144
|
})
|
|
@@ -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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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 () => {
|
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
5
|
-
*
|
|
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
|
-
//
|
|
16
|
-
const
|
|
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',
|
|
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',
|
|
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/
|
|
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/
|
|
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
|
-
'
|
|
122
|
+
'f87db7e': userProfile,
|
|
123
123
|
},
|
|
124
124
|
'x-ext-urls': {
|
|
125
|
-
'
|
|
125
|
+
'f87db7e': `${url}/users`,
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
128
|
})
|
package/src/diff/diff.ts
CHANGED
package/src/diff/utils.test.ts
CHANGED
|
@@ -148,7 +148,7 @@ describe('isArrayEqual', () => {
|
|
|
148
148
|
[1, 2, 3],
|
|
149
149
|
[1, 2, 3],
|
|
150
150
|
],
|
|
151
|
-
// @ts-
|
|
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-
|
|
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',
|
|
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',
|
|
5
|
+
describe('unescapeJsonPointer', () => {
|
|
6
6
|
it('unescapes a slash', () => {
|
|
7
7
|
expect(unescapeJsonPointer('/foo~1bar~1baz')).toBe('/foo/bar/baz')
|
|
8
8
|
})
|
package/src/magic-proxy/proxy.ts
CHANGED
|
@@ -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 +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"}
|
package/dist/polyfills/index.js
DELETED
|
@@ -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
|
-
}
|