@scalar/json-magic 0.8.2 → 0.8.3

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 (84) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/bundle/index.d.ts +1 -0
  3. package/dist/bundle/index.d.ts.map +1 -1
  4. package/dist/bundle/index.js.map +1 -1
  5. package/dist/bundle/plugins/browser.js.map +1 -1
  6. package/dist/bundle/plugins/node.d.ts +1 -1
  7. package/dist/bundle/plugins/node.js +1 -1
  8. package/dist/bundle/plugins/node.js.map +1 -1
  9. package/dist/dereference/index.d.ts.map +1 -1
  10. package/dist/dereference/index.js.map +2 -2
  11. package/dist/diff/index.d.ts +1 -1
  12. package/dist/diff/index.d.ts.map +1 -1
  13. package/dist/diff/index.js +1 -1
  14. package/dist/diff/index.js.map +2 -2
  15. package/dist/helpers/escape-json-pointer.d.ts +1 -1
  16. package/dist/helpers/escape-json-pointer.js.map +1 -1
  17. package/dist/magic-proxy/index.d.ts.map +1 -1
  18. package/dist/magic-proxy/index.js.map +2 -2
  19. package/dist/magic-proxy/proxy.d.ts +0 -1
  20. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  21. package/dist/magic-proxy/proxy.js +1 -2
  22. package/dist/magic-proxy/proxy.js.map +2 -2
  23. package/package.json +12 -13
  24. package/.turbo/turbo-build.log +0 -10
  25. package/esbuild.ts +0 -15
  26. package/src/bundle/bundle.test.ts +0 -2917
  27. package/src/bundle/bundle.ts +0 -916
  28. package/src/bundle/create-limiter.test.ts +0 -28
  29. package/src/bundle/create-limiter.ts +0 -52
  30. package/src/bundle/index.ts +0 -3
  31. package/src/bundle/plugins/browser.ts +0 -4
  32. package/src/bundle/plugins/fetch-urls/index.test.ts +0 -141
  33. package/src/bundle/plugins/fetch-urls/index.ts +0 -105
  34. package/src/bundle/plugins/node.ts +0 -5
  35. package/src/bundle/plugins/parse-json/index.test.ts +0 -24
  36. package/src/bundle/plugins/parse-json/index.ts +0 -32
  37. package/src/bundle/plugins/parse-yaml/index.test.ts +0 -26
  38. package/src/bundle/plugins/parse-yaml/index.ts +0 -34
  39. package/src/bundle/plugins/read-files/index.test.ts +0 -36
  40. package/src/bundle/plugins/read-files/index.ts +0 -58
  41. package/src/bundle/value-generator.test.ts +0 -165
  42. package/src/bundle/value-generator.ts +0 -143
  43. package/src/dereference/dereference.test.ts +0 -142
  44. package/src/dereference/dereference.ts +0 -84
  45. package/src/dereference/index.ts +0 -2
  46. package/src/diff/apply.test.ts +0 -262
  47. package/src/diff/apply.ts +0 -83
  48. package/src/diff/diff.test.ts +0 -328
  49. package/src/diff/diff.ts +0 -93
  50. package/src/diff/index.test.ts +0 -150
  51. package/src/diff/index.ts +0 -5
  52. package/src/diff/merge.test.ts +0 -1109
  53. package/src/diff/merge.ts +0 -136
  54. package/src/diff/trie.test.ts +0 -30
  55. package/src/diff/trie.ts +0 -113
  56. package/src/diff/utils.test.ts +0 -169
  57. package/src/diff/utils.ts +0 -111
  58. package/src/helpers/convert-to-local-ref.test.ts +0 -211
  59. package/src/helpers/convert-to-local-ref.ts +0 -43
  60. package/src/helpers/escape-json-pointer.test.ts +0 -13
  61. package/src/helpers/escape-json-pointer.ts +0 -8
  62. package/src/helpers/get-schemas.test.ts +0 -356
  63. package/src/helpers/get-schemas.ts +0 -80
  64. package/src/helpers/get-segments-from-path.test.ts +0 -17
  65. package/src/helpers/get-segments-from-path.ts +0 -17
  66. package/src/helpers/get-value-by-path.test.ts +0 -338
  67. package/src/helpers/get-value-by-path.ts +0 -44
  68. package/src/helpers/is-json-object.ts +0 -31
  69. package/src/helpers/is-object.test.ts +0 -27
  70. package/src/helpers/is-object.ts +0 -4
  71. package/src/helpers/is-yaml.ts +0 -18
  72. package/src/helpers/json-path-utils.test.ts +0 -57
  73. package/src/helpers/json-path-utils.ts +0 -50
  74. package/src/helpers/normalize.test.ts +0 -92
  75. package/src/helpers/normalize.ts +0 -35
  76. package/src/helpers/unescape-json-pointer.test.ts +0 -23
  77. package/src/helpers/unescape-json-pointer.ts +0 -9
  78. package/src/magic-proxy/index.ts +0 -2
  79. package/src/magic-proxy/proxy.test.ts +0 -1987
  80. package/src/magic-proxy/proxy.ts +0 -323
  81. package/src/types.ts +0 -1
  82. package/tsconfig.build.json +0 -12
  83. package/tsconfig.json +0 -16
  84. package/vite.config.ts +0 -8
@@ -1,165 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { generateUniqueValue, uniqueValueGeneratorFactory } from './value-generator'
4
-
5
- describe('generateUniqueHash', () => {
6
- it('should generate hash values from the function we pass in', async () => {
7
- const hashFunction = (value: string) => {
8
- if (value === 'a') {
9
- return 'a'
10
- }
11
- throw 'Ops'
12
- }
13
-
14
- const result = await generateUniqueValue(hashFunction, 'a', {})
15
-
16
- expect(result).toBe('a')
17
- })
18
-
19
- it('should return same value for the same input', async () => {
20
- const hashFunction = (value: string) => {
21
- if (value === 'a') {
22
- return value
23
- }
24
- throw 'Ops'
25
- }
26
-
27
- const map = {}
28
-
29
- const result1 = await generateUniqueValue(hashFunction, 'a', map)
30
- expect(result1).toBe('a')
31
-
32
- const result2 = await generateUniqueValue(hashFunction, 'a', map)
33
- expect(result2).toBe('a')
34
- })
35
-
36
- it('should handle hash collisions', async () => {
37
- const hashFunction = (value: string) => {
38
- // Hash a, b and c will produce collisions
39
- if (value === 'a' || value === 'b' || value === 'c') {
40
- return 'd'
41
- }
42
-
43
- // Hashing the hashed result
44
- if (value === 'd') {
45
- return 'e'
46
- }
47
-
48
- if (value === 'e') {
49
- return 'f'
50
- }
51
-
52
- if (value === 'f') {
53
- return 'g'
54
- }
55
-
56
- if (value === 'g') {
57
- return 'h'
58
- }
59
-
60
- throw 'Ops'
61
- }
62
-
63
- const map = {}
64
-
65
- const res1 = await generateUniqueValue(hashFunction, 'd', map)
66
- expect(res1).toBe('e')
67
-
68
- const res2 = await generateUniqueValue(hashFunction, 'b', map)
69
- expect(res2).toBe('d')
70
-
71
- // Takes the first available spot
72
- const res3 = await generateUniqueValue(hashFunction, 'a', map)
73
- expect(res3).toBe('f')
74
-
75
- // Produces the same output for the same input
76
- const res4 = await generateUniqueValue(hashFunction, 'a', map)
77
- expect(res4).toBe('f')
78
-
79
- const res5 = await generateUniqueValue(hashFunction, 'c', map)
80
- expect(res5).toBe('g')
81
-
82
- const res6 = await generateUniqueValue(hashFunction, 'g', map)
83
- expect(res6).toBe('h')
84
- })
85
-
86
- it('should throw when it reaches max depth', async () => {
87
- const hashFunction = () => 'a'
88
-
89
- const map = {}
90
- const result1 = await generateUniqueValue(hashFunction, 'a', map)
91
- expect(result1).toBe('a')
92
-
93
- const result2 = await generateUniqueValue(hashFunction, 'a', map)
94
- expect(result2).toBe('a')
95
-
96
- await expect(() => generateUniqueValue(hashFunction, 'b', map)).rejects.toThrowError()
97
- })
98
- })
99
-
100
- describe('uniqueValueGeneratorFactory', () => {
101
- it('should generate unique values while caching the previously generated values', async () => {
102
- let calls = 0
103
- const compress = (value: string) => {
104
- // Ensure we return the same value only once for the input 'b'
105
- if (value === 'b' && calls === 0) {
106
- calls++
107
- return 'another-value'
108
- }
109
- calls++
110
- return 'should not be returned'
111
- }
112
- const { generate } = uniqueValueGeneratorFactory(compress, {
113
- 'b': 'a',
114
- })
115
-
116
- // Should use the value from the cache
117
- expect(await generate('a')).toBe('b')
118
-
119
- // should use the generator function to generate a unique value
120
- expect(await generate('b')).toBe('another-value')
121
-
122
- // should generate the same value when we call generate with the same input
123
- expect(await generate('b')).toBe('another-value')
124
- })
125
-
126
- it('should handle conflicts', async () => {
127
- const compress = (value: string) => {
128
- if (value === 'a') {
129
- return 'c'
130
- }
131
-
132
- if (value === 'b') {
133
- return 'c'
134
- }
135
-
136
- if (value === 'c') {
137
- return 'd'
138
- }
139
- return 'should not be returned'
140
- }
141
- const { generate } = uniqueValueGeneratorFactory(compress, {})
142
-
143
- expect(await generate('a')).toBe('c')
144
- expect(await generate('b')).toBe('d')
145
- })
146
-
147
- it('should handle numeric strings by prefixing them with letters', async () => {
148
- const compress = (value: string) => {
149
- if (!value) {
150
- return '92819102'
151
- }
152
- return 'radom value'
153
- }
154
-
155
- const { generate } = uniqueValueGeneratorFactory(compress, {})
156
-
157
- // Prefix with a letter
158
- expect(await generate('')).toBe('a92819102')
159
-
160
- // Should return the same value for same input
161
- expect(await generate('')).toBe('a92819102')
162
-
163
- expect(await generate('abc')).toBe('radom value')
164
- })
165
- })
@@ -1,143 +0,0 @@
1
- import { generateHash } from '@scalar/helpers/string/generate-hash'
2
-
3
- /**
4
- * Generates a short hash from a string value using xxhash.
5
- *
6
- * This function is used to create unique identifiers for external references
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.
10
- * If the hash would be all numbers, it ensures at least one letter is included.
11
- *
12
- * @param value - The string to hash
13
- * @returns A Promise that resolves to a 7-character hexadecimal hash with at least one letter
14
- * @example
15
- * // Returns "2ae91d7"
16
- * getHash("https://example.com/schema.json")
17
- */
18
- export function getHash(value: string): string {
19
- // Hash the data using xxhash
20
- const hashHex = generateHash(value)
21
-
22
- // Return first 7 characters of the hash, ensuring at least one letter
23
- const hash = hashHex.substring(0, 7)
24
- return hash.match(/^\d+$/) ? 'a' + hash.substring(1) : hash
25
- }
26
-
27
- /**
28
- * Generates a unique compressed value for a string, handling collisions by recursively compressing
29
- * until a unique value is found. This is used to create unique identifiers for external
30
- * references in the bundled OpenAPI document.
31
- *
32
- * @param compress - Function that generates a compressed value from a string
33
- * @param value - The original string value to compress
34
- * @param compressedToValue - Object mapping compressed values to their original values
35
- * @param prevCompressedValue - Optional previous compressed value to use as input for generating a new value
36
- * @param depth - Current recursion depth to prevent infinite loops
37
- * @returns A unique compressed value that doesn't conflict with existing values
38
- *
39
- * @example
40
- * const valueMap = {}
41
- * // First call generates compressed value for "example.com/schema.json"
42
- * const value1 = await generateUniqueValue(compress, "example.com/schema.json", valueMap)
43
- * // Returns something like "2ae91d7"
44
- *
45
- * // Second call with same value returns same compressed value
46
- * const value2 = await generateUniqueValue(compress, "example.com/schema.json", valueMap)
47
- * // Returns same value as value1
48
- *
49
- * // Call with different value generates new unique compressed value
50
- * const value3 = await generateUniqueValue(compress, "example.com/other.json", valueMap)
51
- * // Returns different value like "3bf82e9"
52
- */
53
- export async function generateUniqueValue(
54
- compress: (value: string) => Promise<string> | string,
55
- value: string,
56
- compressedToValue: Record<string, string>,
57
- prevCompressedValue?: string,
58
- depth = 0,
59
- ) {
60
- // Prevent infinite recursion by limiting depth
61
- const MAX_DEPTH = 100
62
-
63
- if (depth >= MAX_DEPTH) {
64
- throw 'Can not generate unique compressed values'
65
- }
66
-
67
- // Compress the value, using previous compressed value if provided
68
- const compressedValue = await compress(prevCompressedValue ?? value)
69
-
70
- // Handle collision by recursively trying with compressed value as input
71
- if (compressedToValue[compressedValue] !== undefined && compressedToValue[compressedValue] !== value) {
72
- return generateUniqueValue(compress, value, compressedToValue, compressedValue, depth + 1)
73
- }
74
-
75
- // Store mapping and return unique compressed value
76
- compressedToValue[compressedValue] = value
77
- return compressedValue
78
- }
79
-
80
- /**
81
- * Factory function that creates a value generator with caching capabilities.
82
- * The generator maintains a bidirectional mapping between original values and their compressed forms.
83
- *
84
- * @param compress - Function that generates a compressed value from a string
85
- * @param compressedToValue - Initial mapping of compressed values to their original values
86
- * @returns An object with a generate method that produces unique compressed values
87
- *
88
- * @example
89
- * const compress = (value) => value.substring(0, 6) // Simple compression example
90
- * const initialMap = { 'abc123': 'example.com/schema.json' }
91
- * const generator = uniqueValueGeneratorFactory(compress, initialMap)
92
- *
93
- * // Generate compressed value for new string
94
- * const compressed = await generator.generate('example.com/other.json')
95
- * // Returns something like 'example'
96
- *
97
- * // Generate compressed value for existing string
98
- * const cached = await generator.generate('example.com/schema.json')
99
- * // Returns 'abc123' from cache
100
- */
101
- export const uniqueValueGeneratorFactory = (
102
- compress: (value: string) => Promise<string> | string,
103
- compressedToValue: Record<string, string>,
104
- ) => {
105
- // Create a reverse mapping from original values to their compressed forms
106
- const valueToCompressed = Object.fromEntries(Object.entries(compressedToValue).map(([key, value]) => [value, key]))
107
-
108
- return {
109
- /**
110
- * Generates a unique compressed value for the given input string.
111
- * First checks if a compressed value already exists in the cache.
112
- * If not, generates a new unique compressed value and stores it in the cache.
113
- *
114
- * @param value - The original string value to compress
115
- * @returns A Promise that resolves to the compressed string value
116
- *
117
- * @example
118
- * const generator = uniqueValueGeneratorFactory(compress, {})
119
- * const compressed = await generator.generate('example.com/schema.json')
120
- * // Returns a unique compressed value like 'example'
121
- */
122
- generate: async (value: string) => {
123
- // Check if we already have a compressed value for this input
124
- const cache = valueToCompressed[value]
125
- if (cache) {
126
- return cache
127
- }
128
-
129
- // Generate a new unique compressed value
130
- const generatedValue = await generateUniqueValue(compress, value, compressedToValue)
131
-
132
- // Ensure the generated string contains at least one non-numeric character
133
- // This prevents the `setValueAtPath` function from interpreting the value as an array index
134
- // by forcing it to be treated as an object property name
135
- const compressedValue = generatedValue.match(/^\d+$/) ? `a${generatedValue}` : generatedValue
136
-
137
- // Store the new mapping in our cache
138
- valueToCompressed[value] = compressedValue
139
-
140
- return compressedValue
141
- },
142
- }
143
- }
@@ -1,142 +0,0 @@
1
- import { type FastifyInstance, fastify } from 'fastify'
2
- import { beforeEach, describe, expect, it } from 'vitest'
3
-
4
- import { dereference } from '@/dereference/dereference'
5
-
6
- describe('dereference', () => {
7
- describe('sync', () => {
8
- it('should dereference JSON pointers', () => {
9
- const data = {
10
- users: {
11
- name: 'John Doe',
12
- age: 30,
13
- address: {
14
- city: 'New York',
15
- street: '5th Avenue',
16
- },
17
- },
18
- profile: {
19
- $ref: '#/users',
20
- },
21
- address: {
22
- $ref: '#/users/address',
23
- },
24
- }
25
-
26
- const result = dereference(data, { sync: true })
27
-
28
- expect(result).toEqual({
29
- success: true,
30
- data: {
31
- users: {
32
- name: 'John Doe',
33
- age: 30,
34
- address: {
35
- city: 'New York',
36
- street: '5th Avenue',
37
- },
38
- },
39
- profile: {
40
- '$ref': '#/users',
41
- '$ref-value': {
42
- name: 'John Doe',
43
- age: 30,
44
- address: {
45
- city: 'New York',
46
- street: '5th Avenue',
47
- },
48
- },
49
- },
50
- address: {
51
- '$ref': '#/users/address',
52
- '$ref-value': {
53
- city: 'New York',
54
- street: '5th Avenue',
55
- },
56
- },
57
- },
58
- })
59
- })
60
- })
61
-
62
- describe('async', () => {
63
- let server: FastifyInstance
64
- const port = 7299
65
- const url = `http://localhost:${port}`
66
-
67
- beforeEach(() => {
68
- server = fastify({ logger: false })
69
-
70
- return async () => {
71
- await server.close()
72
- }
73
- })
74
-
75
- it('should dereference JSON pointers asynchronously', async () => {
76
- const userProfile = {
77
- name: 'Jane Doe',
78
- age: 25,
79
- address: {
80
- city: 'Los Angeles',
81
- street: 'Sunset Boulevard',
82
- },
83
- }
84
- server.get('/users', () => userProfile)
85
-
86
- await server.listen({ port: port })
87
-
88
- const data = {
89
- profile: {
90
- $ref: `${url}/users#`,
91
- },
92
- address: {
93
- $ref: `${url}/users#/address`,
94
- },
95
- }
96
- const result = await dereference(data, { sync: false })
97
- expect(result).toEqual({
98
- success: true,
99
- data: {
100
- profile: {
101
- '$ref': '#/x-ext/f053c6d',
102
- '$ref-value': {
103
- name: 'Jane Doe',
104
- age: 25,
105
- address: {
106
- city: 'Los Angeles',
107
- street: 'Sunset Boulevard',
108
- },
109
- },
110
- },
111
- address: {
112
- '$ref': '#/x-ext/f053c6d/address',
113
- '$ref-value': {
114
- city: 'Los Angeles',
115
- street: 'Sunset Boulevard',
116
- },
117
- },
118
- 'x-ext': {
119
- 'f053c6d': userProfile,
120
- },
121
- 'x-ext-urls': {
122
- 'f053c6d': `${url}/users`,
123
- },
124
- },
125
- })
126
- })
127
-
128
- it('should handle errors when dereferencing remote refs', async () => {
129
- const data = {
130
- profile: {
131
- $ref: `${url}/nonexistent`,
132
- },
133
- }
134
-
135
- const result = await dereference(data, { sync: false })
136
- expect(result).toEqual({
137
- success: false,
138
- errors: [`Failed to resolve ${url}/nonexistent`],
139
- })
140
- })
141
- })
142
- })
@@ -1,84 +0,0 @@
1
- import { bundle } from '@/bundle'
2
- import { fetchUrls } from '@/bundle/plugins/fetch-urls'
3
- import { createMagicProxy } from '@/magic-proxy'
4
- import type { UnknownObject } from '@/types'
5
-
6
- type DereferenceResult =
7
- | {
8
- success: true
9
- data: UnknownObject
10
- }
11
- | {
12
- success: false
13
- errors: string[]
14
- }
15
-
16
- type ReturnDereferenceResult<Opt extends { sync?: boolean }> = Opt['sync'] extends true
17
- ? DereferenceResult
18
- : Promise<DereferenceResult>
19
-
20
- /**
21
- * Dereferences a JSON object, resolving all $ref pointers.
22
- *
23
- * This function can operate synchronously (no remote refs, no async plugins) or asynchronously (with remote refs).
24
- * If `options.sync` is true, it simply wraps the input in a magic proxy and returns it.
25
- * Otherwise, it bundles the document, resolving all $refs (including remote ones), and returns a promise.
26
- *
27
- * @param input - JSON Schema object to dereference.
28
- * @param options - Optional settings. If `sync` is true, dereferencing is synchronous.
29
- * @returns A DereferenceResult (or Promise thereof) indicating success and the dereferenced data, or errors.
30
- *
31
- * @example
32
- * // Synchronous dereference (no remote refs)
33
- * const result = dereference({ openapi: '3.0.0', info: { title: 'My API', version: '1.0.0' } }, { sync: true });
34
- * if (result.success) {
35
- * console.log(result.data); // Magic proxy-wrapped document
36
- * }
37
- *
38
- * @example
39
- * // Asynchronous dereference (with remote refs)
40
- * dereference({ $ref: 'https://example.com/api.yaml' })
41
- * .then(result => {
42
- * if (result.success) {
43
- * console.log(result.data); // Fully dereferenced document
44
- * } else {
45
- * console.error(result.errors);
46
- * }
47
- * });
48
- */
49
- export const dereference = <Opts extends { sync?: boolean }>(
50
- input: UnknownObject,
51
- options?: Opts,
52
- ): ReturnDereferenceResult<Opts> => {
53
- if (options?.sync) {
54
- return {
55
- success: true,
56
- data: createMagicProxy(input),
57
- } as ReturnDereferenceResult<Opts>
58
- }
59
-
60
- const errors: string[] = []
61
-
62
- return bundle(input, {
63
- plugins: [fetchUrls()],
64
- treeShake: false,
65
- urlMap: true,
66
- hooks: {
67
- onResolveError(node) {
68
- errors.push(`Failed to resolve ${node.$ref}`)
69
- },
70
- },
71
- }).then((result) => {
72
- if (errors.length > 0) {
73
- return {
74
- success: false,
75
- errors,
76
- }
77
- }
78
-
79
- return {
80
- success: true,
81
- data: createMagicProxy(result as UnknownObject),
82
- }
83
- }) as ReturnDereferenceResult<Opts>
84
- }
@@ -1,2 +0,0 @@
1
- // biome-ignore lint/performance/noBarrelFile: <explanation>
2
- export { dereference } from './dereference'