@scalar/json-magic 0.1.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 (185) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/LICENSE +21 -0
  4. package/README.md +356 -0
  5. package/dist/bundle/bundle.d.ts +292 -0
  6. package/dist/bundle/bundle.d.ts.map +1 -0
  7. package/dist/bundle/bundle.js +259 -0
  8. package/dist/bundle/bundle.js.map +7 -0
  9. package/dist/bundle/create-limiter.d.ts +21 -0
  10. package/dist/bundle/create-limiter.d.ts.map +1 -0
  11. package/dist/bundle/create-limiter.js +31 -0
  12. package/dist/bundle/create-limiter.js.map +7 -0
  13. package/dist/bundle/index.d.ts +2 -0
  14. package/dist/bundle/index.d.ts.map +1 -0
  15. package/dist/bundle/index.js +5 -0
  16. package/dist/bundle/index.js.map +7 -0
  17. package/dist/bundle/plugins/browser.d.ts +4 -0
  18. package/dist/bundle/plugins/browser.d.ts.map +1 -0
  19. package/dist/bundle/plugins/browser.js +9 -0
  20. package/dist/bundle/plugins/browser.js.map +7 -0
  21. package/dist/bundle/plugins/fetch-urls/index.d.ts +39 -0
  22. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -0
  23. package/dist/bundle/plugins/fetch-urls/index.js +48 -0
  24. package/dist/bundle/plugins/fetch-urls/index.js.map +7 -0
  25. package/dist/bundle/plugins/node.d.ts +5 -0
  26. package/dist/bundle/plugins/node.d.ts.map +1 -0
  27. package/dist/bundle/plugins/node.js +11 -0
  28. package/dist/bundle/plugins/node.js.map +7 -0
  29. package/dist/bundle/plugins/parse-json/index.d.ts +13 -0
  30. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -0
  31. package/dist/bundle/plugins/parse-json/index.js +22 -0
  32. package/dist/bundle/plugins/parse-json/index.js.map +7 -0
  33. package/dist/bundle/plugins/parse-yaml/index.d.ts +13 -0
  34. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -0
  35. package/dist/bundle/plugins/parse-yaml/index.js +23 -0
  36. package/dist/bundle/plugins/parse-yaml/index.js.map +7 -0
  37. package/dist/bundle/plugins/read-files/index.d.ts +29 -0
  38. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -0
  39. package/dist/bundle/plugins/read-files/index.js +30 -0
  40. package/dist/bundle/plugins/read-files/index.js.map +7 -0
  41. package/dist/bundle/value-generator.d.ts +79 -0
  42. package/dist/bundle/value-generator.d.ts.map +1 -0
  43. package/dist/bundle/value-generator.js +55 -0
  44. package/dist/bundle/value-generator.js.map +7 -0
  45. package/dist/dereference/dereference.d.ts +45 -0
  46. package/dist/dereference/dereference.d.ts.map +1 -0
  47. package/dist/dereference/dereference.js +37 -0
  48. package/dist/dereference/dereference.js.map +7 -0
  49. package/dist/dereference/index.d.ts +2 -0
  50. package/dist/dereference/index.d.ts.map +1 -0
  51. package/dist/dereference/index.js +5 -0
  52. package/dist/dereference/index.js.map +7 -0
  53. package/dist/diff/apply.d.ts +35 -0
  54. package/dist/diff/apply.d.ts.map +1 -0
  55. package/dist/diff/apply.js +40 -0
  56. package/dist/diff/apply.js.map +7 -0
  57. package/dist/diff/diff.d.ts +56 -0
  58. package/dist/diff/diff.d.ts.map +1 -0
  59. package/dist/diff/diff.js +33 -0
  60. package/dist/diff/diff.js.map +7 -0
  61. package/dist/diff/index.d.ts +5 -0
  62. package/dist/diff/index.d.ts.map +1 -0
  63. package/dist/diff/index.js +9 -0
  64. package/dist/diff/index.js.map +7 -0
  65. package/dist/diff/merge.d.ts +43 -0
  66. package/dist/diff/merge.d.ts.map +1 -0
  67. package/dist/diff/merge.js +61 -0
  68. package/dist/diff/merge.js.map +7 -0
  69. package/dist/diff/trie.d.ts +64 -0
  70. package/dist/diff/trie.d.ts.map +1 -0
  71. package/dist/diff/trie.js +82 -0
  72. package/dist/diff/trie.js.map +7 -0
  73. package/dist/diff/utils.d.ts +63 -0
  74. package/dist/diff/utils.d.ts.map +1 -0
  75. package/dist/diff/utils.js +48 -0
  76. package/dist/diff/utils.js.map +7 -0
  77. package/dist/magic-proxy/index.d.ts +2 -0
  78. package/dist/magic-proxy/index.d.ts.map +1 -0
  79. package/dist/magic-proxy/index.js +6 -0
  80. package/dist/magic-proxy/index.js.map +7 -0
  81. package/dist/magic-proxy/proxy.d.ts +63 -0
  82. package/dist/magic-proxy/proxy.d.ts.map +1 -0
  83. package/dist/magic-proxy/proxy.js +108 -0
  84. package/dist/magic-proxy/proxy.js.map +7 -0
  85. package/dist/polyfills/index.d.ts +2 -0
  86. package/dist/polyfills/index.d.ts.map +1 -0
  87. package/dist/polyfills/index.js +25 -0
  88. package/dist/polyfills/index.js.map +7 -0
  89. package/dist/polyfills/path.d.ts +24 -0
  90. package/dist/polyfills/path.d.ts.map +1 -0
  91. package/dist/polyfills/path.js +174 -0
  92. package/dist/polyfills/path.js.map +7 -0
  93. package/dist/types.d.ts +2 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +1 -0
  96. package/dist/types.js.map +7 -0
  97. package/dist/utils/escape-json-pointer.d.ts +7 -0
  98. package/dist/utils/escape-json-pointer.d.ts.map +1 -0
  99. package/dist/utils/escape-json-pointer.js +7 -0
  100. package/dist/utils/escape-json-pointer.js.map +7 -0
  101. package/dist/utils/get-segments-from-path.d.ts +5 -0
  102. package/dist/utils/get-segments-from-path.d.ts.map +1 -0
  103. package/dist/utils/get-segments-from-path.js +11 -0
  104. package/dist/utils/get-segments-from-path.js.map +7 -0
  105. package/dist/utils/is-json-object.d.ts +18 -0
  106. package/dist/utils/is-json-object.d.ts.map +1 -0
  107. package/dist/utils/is-json-object.js +16 -0
  108. package/dist/utils/is-json-object.js.map +7 -0
  109. package/dist/utils/is-object.d.ts +5 -0
  110. package/dist/utils/is-object.d.ts.map +1 -0
  111. package/dist/utils/is-object.js +5 -0
  112. package/dist/utils/is-object.js.map +7 -0
  113. package/dist/utils/is-yaml.d.ts +17 -0
  114. package/dist/utils/is-yaml.d.ts.map +1 -0
  115. package/dist/utils/is-yaml.js +7 -0
  116. package/dist/utils/is-yaml.js.map +7 -0
  117. package/dist/utils/json-path-utils.d.ts +23 -0
  118. package/dist/utils/json-path-utils.d.ts.map +1 -0
  119. package/dist/utils/json-path-utils.js +16 -0
  120. package/dist/utils/json-path-utils.js.map +7 -0
  121. package/dist/utils/normalize.d.ts +5 -0
  122. package/dist/utils/normalize.d.ts.map +1 -0
  123. package/dist/utils/normalize.js +28 -0
  124. package/dist/utils/normalize.js.map +7 -0
  125. package/dist/utils/unescape-json-pointer.d.ts +8 -0
  126. package/dist/utils/unescape-json-pointer.d.ts.map +1 -0
  127. package/dist/utils/unescape-json-pointer.js +7 -0
  128. package/dist/utils/unescape-json-pointer.js.map +7 -0
  129. package/esbuild.ts +13 -0
  130. package/package.json +65 -0
  131. package/src/bundle/bundle.test.ts +1843 -0
  132. package/src/bundle/bundle.ts +758 -0
  133. package/src/bundle/create-limiter.test.ts +28 -0
  134. package/src/bundle/create-limiter.ts +52 -0
  135. package/src/bundle/index.ts +2 -0
  136. package/src/bundle/plugins/browser.ts +4 -0
  137. package/src/bundle/plugins/fetch-urls/index.test.ts +147 -0
  138. package/src/bundle/plugins/fetch-urls/index.ts +94 -0
  139. package/src/bundle/plugins/node.ts +5 -0
  140. package/src/bundle/plugins/parse-json/index.test.ts +22 -0
  141. package/src/bundle/plugins/parse-json/index.ts +30 -0
  142. package/src/bundle/plugins/parse-yaml/index.test.ts +24 -0
  143. package/src/bundle/plugins/parse-yaml/index.ts +31 -0
  144. package/src/bundle/plugins/read-files/index.test.ts +35 -0
  145. package/src/bundle/plugins/read-files/index.ts +55 -0
  146. package/src/bundle/value-generator.test.ts +166 -0
  147. package/src/bundle/value-generator.ts +147 -0
  148. package/src/dereference/dereference.test.ts +137 -0
  149. package/src/dereference/dereference.ts +84 -0
  150. package/src/dereference/index.ts +2 -0
  151. package/src/diff/apply.test.ts +262 -0
  152. package/src/diff/apply.ts +78 -0
  153. package/src/diff/diff.test.ts +328 -0
  154. package/src/diff/diff.ts +94 -0
  155. package/src/diff/index.test.ts +150 -0
  156. package/src/diff/index.ts +5 -0
  157. package/src/diff/merge.test.ts +1109 -0
  158. package/src/diff/merge.ts +136 -0
  159. package/src/diff/trie.test.ts +30 -0
  160. package/src/diff/trie.ts +113 -0
  161. package/src/diff/utils.test.ts +169 -0
  162. package/src/diff/utils.ts +113 -0
  163. package/src/magic-proxy/index.ts +2 -0
  164. package/src/magic-proxy/proxy.test.ts +145 -0
  165. package/src/magic-proxy/proxy.ts +225 -0
  166. package/src/polyfills/index.ts +12 -0
  167. package/src/polyfills/path.ts +248 -0
  168. package/src/types.ts +1 -0
  169. package/src/utils/escape-json-pointer.test.ts +13 -0
  170. package/src/utils/escape-json-pointer.ts +8 -0
  171. package/src/utils/get-segments-from-path.test.ts +17 -0
  172. package/src/utils/get-segments-from-path.ts +17 -0
  173. package/src/utils/is-json-object.ts +31 -0
  174. package/src/utils/is-object.test.ts +27 -0
  175. package/src/utils/is-object.ts +4 -0
  176. package/src/utils/is-yaml.ts +18 -0
  177. package/src/utils/json-path-utils.test.ts +13 -0
  178. package/src/utils/json-path-utils.ts +38 -0
  179. package/src/utils/normalize.test.ts +91 -0
  180. package/src/utils/normalize.ts +34 -0
  181. package/src/utils/unescape-json-pointer.test.ts +23 -0
  182. package/src/utils/unescape-json-pointer.ts +9 -0
  183. package/tsconfig.build.json +12 -0
  184. package/tsconfig.json +16 -0
  185. package/vite.config.ts +8 -0
@@ -0,0 +1,1843 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import fs from 'node:fs/promises'
3
+ import fastify, { type FastifyInstance } from 'fastify'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import {
6
+ bundle,
7
+ getNestedValue,
8
+ isLocalRef,
9
+ isRemoteUrl,
10
+ prefixInternalRef,
11
+ prefixInternalRefRecursive,
12
+ setValueAtPath,
13
+ } from './bundle'
14
+ import { fetchUrls } from './plugins/fetch-urls'
15
+ import { readFiles } from './plugins/read-files'
16
+ import { setTimeout } from 'node:timers/promises'
17
+ import { parseJson } from '@/bundle/plugins/parse-json'
18
+ import { parseYaml } from '@/bundle/plugins/parse-yaml'
19
+ import YAML from 'yaml'
20
+ import { getHash } from '@/bundle/value-generator'
21
+
22
+ describe('bundle', () => {
23
+ describe('external urls', () => {
24
+ let server: FastifyInstance
25
+ const PORT = 7289
26
+
27
+ beforeEach(() => {
28
+ server = fastify({ logger: false })
29
+ })
30
+
31
+ afterEach(async () => {
32
+ await server.close()
33
+ await setTimeout(100)
34
+ })
35
+
36
+ it('bundles external urls', async () => {
37
+ const url = `http://localhost:${PORT}`
38
+
39
+ const external = {
40
+ prop: 'I am external json prop',
41
+ }
42
+ server.get('/', (_, reply) => {
43
+ reply.send(external)
44
+ })
45
+
46
+ await server.listen({ port: PORT })
47
+
48
+ const input = {
49
+ a: {
50
+ b: {
51
+ c: 'hello',
52
+ },
53
+ },
54
+ d: {
55
+ '$ref': `http://localhost:${PORT}#/prop`,
56
+ },
57
+ }
58
+
59
+ await bundle(input, {
60
+ plugins: [fetchUrls(), readFiles()],
61
+ treeShake: false,
62
+ })
63
+
64
+ expect(input).toEqual({
65
+ 'x-ext': {
66
+ [await getHash(url)]: {
67
+ ...external,
68
+ },
69
+ },
70
+ a: {
71
+ b: {
72
+ c: 'hello',
73
+ },
74
+ },
75
+ d: {
76
+ $ref: `#/x-ext/${await getHash(url)}/prop`,
77
+ },
78
+ })
79
+ })
80
+
81
+ it('bundles external urls from resolved external piece', async () => {
82
+ const url = `http://localhost:${PORT}`
83
+ const chunk2 = {
84
+ hey: 'hey',
85
+ nested: {
86
+ key: 'value',
87
+ },
88
+ internal: '#/nested/key',
89
+ }
90
+
91
+ const chunk1 = {
92
+ a: {
93
+ hello: 'hello',
94
+ },
95
+ b: {
96
+ '$ref': `${url}/chunk2#`,
97
+ },
98
+ }
99
+
100
+ server.get('/chunk1', (_, reply) => {
101
+ reply.send(chunk1)
102
+ })
103
+ server.get('/chunk2', (_, reply) => {
104
+ reply.send(chunk2)
105
+ })
106
+
107
+ await server.listen({ port: PORT })
108
+
109
+ const input = {
110
+ a: {
111
+ b: {
112
+ c: {
113
+ '$ref': `${url}/chunk1#`,
114
+ },
115
+ },
116
+ },
117
+ }
118
+
119
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
120
+
121
+ expect(input).toEqual({
122
+ 'x-ext': {
123
+ [await getHash(`${url}/chunk1`)]: {
124
+ ...chunk1,
125
+ b: {
126
+ $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
127
+ },
128
+ },
129
+ [await getHash(`${url}/chunk2`)]: {
130
+ ...chunk2,
131
+ internal: '#/nested/key',
132
+ },
133
+ },
134
+ a: {
135
+ b: {
136
+ c: {
137
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
138
+ },
139
+ },
140
+ },
141
+ })
142
+ })
143
+
144
+ it('should correctly handle only urls without a pointer', async () => {
145
+ const url = `http://localhost:${PORT}`
146
+
147
+ server.get('/', (_, reply) => {
148
+ reply.send({
149
+ a: 'a',
150
+ })
151
+ })
152
+
153
+ await server.listen({ port: PORT })
154
+
155
+ const input = {
156
+ a: {
157
+ b: {
158
+ '$ref': `${url}`,
159
+ },
160
+ },
161
+ }
162
+
163
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
164
+
165
+ expect(input).toEqual({
166
+ 'x-ext': {
167
+ [await getHash(url)]: {
168
+ a: 'a',
169
+ },
170
+ },
171
+ a: {
172
+ b: {
173
+ $ref: `#/x-ext/${await getHash(url)}`,
174
+ },
175
+ },
176
+ })
177
+ })
178
+
179
+ it('caches results for same resource', async () => {
180
+ const fn = vi.fn()
181
+ const url = `http://localhost:${PORT}`
182
+
183
+ server.get('/', (_, reply) => {
184
+ fn()
185
+ reply.send({
186
+ a: 'a',
187
+ b: 'b',
188
+ })
189
+ })
190
+
191
+ await server.listen({ port: PORT })
192
+
193
+ const input = {
194
+ a: {
195
+ '$ref': `${url}#/a`,
196
+ },
197
+ b: {
198
+ '$ref': `${url}#/b`,
199
+ },
200
+ }
201
+
202
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
203
+
204
+ expect(input).toEqual({
205
+ 'x-ext': {
206
+ [await getHash(url)]: {
207
+ a: 'a',
208
+ b: 'b',
209
+ },
210
+ },
211
+ a: {
212
+ $ref: `#/x-ext/${await getHash(url)}/a`,
213
+ },
214
+ b: {
215
+ $ref: `#/x-ext/${await getHash(url)}/b`,
216
+ },
217
+ })
218
+
219
+ // We expect the bundler to cache the result for the same url
220
+ expect(fn.mock.calls.length).toBe(1)
221
+ })
222
+
223
+ it('handles correctly external nested refs', async () => {
224
+ const url = `http://localhost:${PORT}`
225
+
226
+ server.get('/nested/another-file.json', (_, reply) => {
227
+ reply.send({
228
+ c: 'c',
229
+ })
230
+ })
231
+
232
+ server.get('/nested/chunk1.json', (_, reply) => {
233
+ reply.send({
234
+ b: {
235
+ '$ref': './another-file.json#',
236
+ },
237
+ })
238
+ })
239
+
240
+ await server.listen({ port: PORT })
241
+
242
+ const input = {
243
+ a: {
244
+ '$ref': `${url}/nested/chunk1.json#`,
245
+ },
246
+ }
247
+
248
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
249
+
250
+ expect(input).toEqual({
251
+ 'x-ext': {
252
+ [await getHash(`${url}/nested/another-file.json`)]: {
253
+ c: 'c',
254
+ },
255
+ [await getHash(`${url}/nested/chunk1.json`)]: {
256
+ b: {
257
+ $ref: `#/x-ext/${await getHash(`${url}/nested/another-file.json`)}`,
258
+ },
259
+ },
260
+ },
261
+ a: {
262
+ $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
263
+ },
264
+ })
265
+ })
266
+
267
+ it('does not merge paths when we use absolute urls', async () => {
268
+ const url = `http://localhost:${PORT}`
269
+
270
+ server.get('/top-level', (_, reply) => {
271
+ reply.send({
272
+ c: 'c',
273
+ })
274
+ })
275
+
276
+ server.get('/nested/chunk1.json', (_, reply) => {
277
+ reply.send({
278
+ b: {
279
+ '$ref': `${url}/top-level#`,
280
+ },
281
+ })
282
+ })
283
+
284
+ await server.listen({ port: PORT })
285
+
286
+ const input = {
287
+ a: {
288
+ '$ref': `${url}/nested/chunk1.json`,
289
+ },
290
+ }
291
+
292
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
293
+
294
+ expect(input).toEqual({
295
+ 'x-ext': {
296
+ [await getHash(`${url}/top-level`)]: {
297
+ c: 'c',
298
+ },
299
+ [await getHash(`${url}/nested/chunk1.json`)]: {
300
+ b: {
301
+ $ref: `#/x-ext/${await getHash(`${url}/top-level`)}`,
302
+ },
303
+ },
304
+ },
305
+ a: {
306
+ $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
307
+ },
308
+ })
309
+ })
310
+
311
+ it('bundles from a url input', async () => {
312
+ const url = `http://localhost:${PORT}`
313
+
314
+ server.get('/top-level', (_, reply) => {
315
+ reply.send({
316
+ c: 'c',
317
+ })
318
+ })
319
+
320
+ server.get('/nested/chunk1.json', (_, reply) => {
321
+ reply.send({
322
+ b: {
323
+ '$ref': `${url}/top-level#`,
324
+ },
325
+ })
326
+ })
327
+
328
+ server.get('/base/openapi.json', (_, reply) => {
329
+ reply.send({
330
+ a: {
331
+ $ref: '../nested/chunk1.json',
332
+ },
333
+ })
334
+ })
335
+
336
+ await server.listen({ port: PORT })
337
+
338
+ const output = await bundle(`${url}/base/openapi.json`, { plugins: [fetchUrls()], treeShake: false })
339
+
340
+ expect(output).toEqual({
341
+ 'x-ext': {
342
+ [await getHash(`${url}/top-level`)]: {
343
+ c: 'c',
344
+ },
345
+ [await getHash(`${url}/nested/chunk1.json`)]: {
346
+ b: {
347
+ $ref: `#/x-ext/${await getHash(`${url}/top-level`)}`,
348
+ },
349
+ },
350
+ },
351
+ a: {
352
+ $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
353
+ },
354
+ })
355
+ })
356
+
357
+ it('generated a map when we turn the urlMap on', async () => {
358
+ const url = `http://localhost:${PORT}`
359
+
360
+ server.get('/top-level', (_, reply) => {
361
+ reply.send({
362
+ c: 'c',
363
+ })
364
+ })
365
+
366
+ server.get('/nested/chunk1.json', (_, reply) => {
367
+ reply.send({
368
+ b: {
369
+ '$ref': `${url}/top-level#`,
370
+ },
371
+ })
372
+ })
373
+
374
+ server.get('/base/openapi.json', (_, reply) => {
375
+ reply.send({
376
+ a: {
377
+ $ref: '../nested/chunk1.json',
378
+ },
379
+ })
380
+ })
381
+
382
+ await server.listen({ port: PORT })
383
+
384
+ const output = await bundle(`${url}/base/openapi.json`, {
385
+ plugins: [fetchUrls()],
386
+ treeShake: false,
387
+ urlMap: true,
388
+ })
389
+
390
+ expect(output).toEqual({
391
+ 'x-ext': {
392
+ [await getHash(`${url}/top-level`)]: {
393
+ c: 'c',
394
+ },
395
+ [await getHash(`${url}/nested/chunk1.json`)]: {
396
+ b: {
397
+ $ref: `#/x-ext/${await getHash(`${url}/top-level`)}`,
398
+ },
399
+ },
400
+ },
401
+ 'x-ext-urls': {
402
+ [await getHash(`${url}/top-level`)]: `${url}/top-level`,
403
+ [await getHash(`${url}/nested/chunk1.json`)]: `${url}/nested/chunk1.json`,
404
+ },
405
+ a: {
406
+ $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
407
+ },
408
+ })
409
+ })
410
+
411
+ it('prefixes the refs only once', async () => {
412
+ const url = `http://localhost:${PORT}`
413
+
414
+ const chunk2 = {
415
+ a: 'a',
416
+ b: {
417
+ '$ref': `${url}/chunk1#`,
418
+ },
419
+ }
420
+ const chunk1 = {
421
+ a: {
422
+ hello: 'hello',
423
+ },
424
+ b: {
425
+ '$ref': `${url}/chunk2#`,
426
+ },
427
+ }
428
+
429
+ server.get('/chunk1', (_, reply) => {
430
+ reply.send(chunk1)
431
+ })
432
+ server.get('/chunk2', (_, reply) => {
433
+ reply.send(chunk2)
434
+ })
435
+
436
+ await server.listen({ port: PORT })
437
+
438
+ const input = {
439
+ a: {
440
+ b: {
441
+ c: {
442
+ '$ref': `${url}/chunk1#`,
443
+ },
444
+ d: {
445
+ e: {
446
+ f: {
447
+ g: {
448
+ '$ref': `${url}/chunk1#`,
449
+ },
450
+ },
451
+ },
452
+ },
453
+ },
454
+ },
455
+ }
456
+
457
+ await bundle(input, { plugins: [fetchUrls()], treeShake: false })
458
+
459
+ expect(input).toEqual({
460
+ a: {
461
+ b: {
462
+ c: {
463
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
464
+ },
465
+ d: {
466
+ e: {
467
+ f: {
468
+ g: {
469
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
470
+ },
471
+ },
472
+ },
473
+ },
474
+ },
475
+ },
476
+ 'x-ext': {
477
+ [await getHash(`${url}/chunk1`)]: {
478
+ a: {
479
+ hello: 'hello',
480
+ },
481
+ b: {
482
+ $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
483
+ },
484
+ },
485
+ [await getHash(`${url}/chunk2`)]: {
486
+ a: 'a',
487
+ b: {
488
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
489
+ },
490
+ },
491
+ },
492
+ })
493
+ })
494
+
495
+ it('bundles array references', async () => {
496
+ const url = `http://localhost:${PORT}`
497
+
498
+ const chunk1 = {
499
+ a: {
500
+ hello: 'hello',
501
+ },
502
+ }
503
+
504
+ server.get('/chunk1', (_, reply) => {
505
+ reply.send(chunk1)
506
+ })
507
+
508
+ await server.listen({ port: PORT })
509
+
510
+ const input = {
511
+ a: [
512
+ {
513
+ $ref: `${url}/chunk1#`,
514
+ },
515
+ ],
516
+ }
517
+ await bundle(input, {
518
+ plugins: [fetchUrls()],
519
+ treeShake: false,
520
+ })
521
+
522
+ expect(input).toEqual({
523
+ a: [
524
+ {
525
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
526
+ },
527
+ ],
528
+ 'x-ext': {
529
+ [await getHash(`${url}/chunk1`)]: {
530
+ a: {
531
+ hello: 'hello',
532
+ },
533
+ },
534
+ },
535
+ })
536
+ })
537
+
538
+ it('bundles subpart of the document', async () => {
539
+ const url = `http://localhost:${PORT}`
540
+
541
+ const chunk1 = {
542
+ a: {
543
+ hello: 'hello',
544
+ },
545
+ }
546
+
547
+ const fn = vi.fn()
548
+
549
+ server.get('/chunk1', (_, reply) => {
550
+ fn()
551
+ reply.send(chunk1)
552
+ })
553
+
554
+ await server.listen({ port: PORT })
555
+
556
+ const input = {
557
+ a: {
558
+ $ref: `${url}/chunk1#`,
559
+ },
560
+ b: {
561
+ $ref: `${url}/chunk1#`,
562
+ },
563
+ c: {
564
+ $ref: `${url}/chunk1#`,
565
+ },
566
+ }
567
+
568
+ const cache = new Map()
569
+
570
+ // Bundle only partial
571
+ await bundle(input.b, {
572
+ plugins: [fetchUrls()],
573
+ treeShake: false,
574
+ root: input,
575
+ cache,
576
+ })
577
+
578
+ expect(input).toEqual({
579
+ a: {
580
+ $ref: `${url}/chunk1#`,
581
+ },
582
+ b: {
583
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
584
+ },
585
+ c: {
586
+ $ref: `${url}/chunk1#`,
587
+ },
588
+ 'x-ext': {
589
+ [await getHash(`${url}/chunk1`)]: {
590
+ a: {
591
+ hello: 'hello',
592
+ },
593
+ },
594
+ },
595
+ 'x-ext-urls': {
596
+ [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
597
+ },
598
+ })
599
+
600
+ // Bundle only partial
601
+ await bundle(input.c, {
602
+ plugins: [fetchUrls()],
603
+ treeShake: false,
604
+ root: input,
605
+ cache,
606
+ })
607
+
608
+ expect(input).toEqual({
609
+ a: {
610
+ $ref: `${url}/chunk1#`,
611
+ },
612
+ b: {
613
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
614
+ },
615
+ c: {
616
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
617
+ },
618
+ 'x-ext': {
619
+ [await getHash(`${url}/chunk1`)]: {
620
+ a: {
621
+ hello: 'hello',
622
+ },
623
+ },
624
+ },
625
+ 'x-ext-urls': {
626
+ [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
627
+ },
628
+ })
629
+
630
+ expect(fn).toHaveBeenCalledTimes(1)
631
+ })
632
+
633
+ it('always emits the url mappings when doing partial bundle', async () => {
634
+ const url = `http://localhost:${PORT}`
635
+
636
+ const chunk1 = {
637
+ a: {
638
+ hello: 'hello',
639
+ },
640
+ }
641
+
642
+ server.get('/chunk1', (_, reply) => {
643
+ reply.send(chunk1)
644
+ })
645
+
646
+ await server.listen({ port: PORT })
647
+
648
+ const input = {
649
+ a: {
650
+ $ref: `${url}/chunk1#`,
651
+ },
652
+ b: {
653
+ $ref: `${url}/chunk1#`,
654
+ },
655
+ c: {
656
+ $ref: `${url}/chunk1#`,
657
+ },
658
+ }
659
+
660
+ const cache = new Map()
661
+
662
+ // Bundle only partial
663
+ await bundle(input.b, {
664
+ plugins: [fetchUrls()],
665
+ treeShake: false,
666
+ root: input,
667
+ cache,
668
+ urlMap: false, // Set the urlMapping to false
669
+ })
670
+
671
+ expect(input).toEqual({
672
+ a: {
673
+ $ref: `${url}/chunk1#`,
674
+ },
675
+ b: {
676
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
677
+ },
678
+ c: {
679
+ $ref: `${url}/chunk1#`,
680
+ },
681
+ 'x-ext': {
682
+ [await getHash(`${url}/chunk1`)]: {
683
+ a: {
684
+ hello: 'hello',
685
+ },
686
+ },
687
+ },
688
+ // It should still inject the mappings on the output document
689
+ 'x-ext-urls': {
690
+ [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
691
+ },
692
+ })
693
+ })
694
+
695
+ it('tree shakes the external documents correctly', async () => {
696
+ const url = `http://localhost:${PORT}`
697
+
698
+ const chunk1 = {
699
+ a: {
700
+ b: {
701
+ hello: 'hello',
702
+ g: {
703
+ $ref: '#/d/e',
704
+ },
705
+ },
706
+ c: 'c',
707
+ },
708
+ d: {
709
+ e: { message: 'I should be included' },
710
+ f: { message: 'I should be excluded on the final bundle' },
711
+ },
712
+ }
713
+
714
+ server.get('/chunk1', (_, reply) => {
715
+ reply.send(chunk1)
716
+ })
717
+
718
+ await server.listen({ port: PORT })
719
+
720
+ const input = {
721
+ a: {
722
+ $ref: `${url}/chunk1#/a/b`,
723
+ },
724
+ }
725
+
726
+ await bundle(input, { plugins: [fetchUrls()], treeShake: true })
727
+
728
+ expect(input).toEqual({
729
+ a: {
730
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a/b`,
731
+ },
732
+ 'x-ext': {
733
+ [await getHash(`${url}/chunk1`)]: {
734
+ a: {
735
+ b: {
736
+ g: {
737
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/d/e`,
738
+ },
739
+ hello: 'hello',
740
+ },
741
+ },
742
+ d: {
743
+ e: { message: 'I should be included' },
744
+ },
745
+ },
746
+ },
747
+ })
748
+ })
749
+
750
+ it('tree shakes correctly when working with nested external refs', async () => {
751
+ const url = `http://localhost:${PORT}`
752
+
753
+ const chunk2 = {
754
+ a: {
755
+ b: {
756
+ hello: 'hello',
757
+ },
758
+ hi: 'hi',
759
+ },
760
+ }
761
+
762
+ const chunk1 = {
763
+ a: {
764
+ b: {
765
+ hello: 'hello',
766
+ g: {
767
+ $ref: '#/d/e',
768
+ },
769
+ },
770
+ c: 'c',
771
+ external: {
772
+ $ref: './chunk2#/a/b',
773
+ },
774
+ },
775
+ d: {
776
+ e: { message: 'I should be included' },
777
+ f: { message: 'I should be excluded on the final bundle' },
778
+ },
779
+ }
780
+
781
+ server.get('/chunk1', (_, reply) => {
782
+ reply.send(chunk1)
783
+ })
784
+
785
+ server.get('/chunk2', (_, reply) => {
786
+ reply.send(chunk2)
787
+ })
788
+
789
+ await server.listen({ port: PORT })
790
+
791
+ const input = {
792
+ a: {
793
+ $ref: `${url}/chunk1#/a`,
794
+ },
795
+ }
796
+
797
+ await bundle(input, { plugins: [fetchUrls()], treeShake: true })
798
+
799
+ expect(input).toEqual({
800
+ a: {
801
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a`,
802
+ },
803
+ 'x-ext': {
804
+ [await getHash(`${url}/chunk1`)]: {
805
+ a: {
806
+ b: {
807
+ g: {
808
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/d/e`,
809
+ },
810
+ hello: 'hello',
811
+ },
812
+ c: 'c',
813
+ 'external': {
814
+ $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}/a/b`,
815
+ },
816
+ },
817
+ d: {
818
+ e: {
819
+ 'message': 'I should be included',
820
+ },
821
+ },
822
+ },
823
+ [await getHash(`${url}/chunk2`)]: {
824
+ a: {
825
+ b: {
826
+ hello: 'hello',
827
+ },
828
+ },
829
+ },
830
+ },
831
+ })
832
+ })
833
+
834
+ it('handles circular references when we treeshake', async () => {
835
+ const url = `http://localhost:${PORT}`
836
+
837
+ const chunk1 = {
838
+ a: {
839
+ b: {
840
+ hello: 'hello',
841
+ g: {
842
+ $ref: '#/a/external',
843
+ },
844
+ },
845
+ c: 'c',
846
+ external: {
847
+ $ref: '#/a/b',
848
+ },
849
+ },
850
+ }
851
+
852
+ server.get('/chunk1', (_, reply) => {
853
+ reply.send(chunk1)
854
+ })
855
+
856
+ await server.listen({ port: PORT })
857
+
858
+ const input = {
859
+ a: {
860
+ $ref: `${url}/chunk1#/a`,
861
+ },
862
+ }
863
+
864
+ await bundle(input, { plugins: [fetchUrls()], treeShake: true })
865
+
866
+ expect(input).toEqual({
867
+ a: {
868
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a`,
869
+ },
870
+ 'x-ext': {
871
+ [await getHash(`${url}/chunk1`)]: {
872
+ a: {
873
+ b: {
874
+ g: {
875
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a/external`,
876
+ },
877
+ hello: 'hello',
878
+ },
879
+ c: 'c',
880
+ external: {
881
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a/b`,
882
+ },
883
+ },
884
+ },
885
+ },
886
+ })
887
+ })
888
+
889
+ it('handles chunks', async () => {
890
+ const url = `http://localhost:${PORT}`
891
+
892
+ const chunk1 = {
893
+ description: 'Chunk 1',
894
+ someRef: {
895
+ $ref: '#/components/User',
896
+ },
897
+ }
898
+
899
+ const chunk2 = {
900
+ description: 'Chunk 2',
901
+ }
902
+
903
+ server.get('/chunk1', (_, reply) => {
904
+ reply.send(chunk1)
905
+ })
906
+ server.get('/chunk2', (_, reply) => {
907
+ reply.send(chunk2)
908
+ })
909
+
910
+ await server.listen({ port: PORT })
911
+
912
+ const input = {
913
+ a: {
914
+ $ref: `${url}/chunk1#`,
915
+ $global: true,
916
+ },
917
+ b: {
918
+ $ref: `${url}/chunk2#`,
919
+ $global: true,
920
+ },
921
+ c: {
922
+ $ref: `${url}/chunk1#`,
923
+ $global: true,
924
+ },
925
+ components: {
926
+ User: {
927
+ id: 'number',
928
+ name: {
929
+ $ref: '#/a',
930
+ },
931
+ another: {
932
+ $ref: '#/b',
933
+ },
934
+ },
935
+ },
936
+ }
937
+
938
+ // Bundle only partial
939
+ await bundle(input.a, {
940
+ plugins: [fetchUrls()],
941
+ treeShake: false,
942
+ root: input,
943
+ })
944
+
945
+ expect(input).toEqual({
946
+ a: {
947
+ $global: true,
948
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
949
+ },
950
+ b: {
951
+ $global: true,
952
+ $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
953
+ },
954
+ c: {
955
+ $global: true,
956
+ $ref: `${url}/chunk1#`,
957
+ },
958
+ components: {
959
+ User: {
960
+ another: {
961
+ $ref: '#/b',
962
+ },
963
+ id: 'number',
964
+ name: {
965
+ $ref: '#/a',
966
+ },
967
+ },
968
+ },
969
+ 'x-ext': {
970
+ [await getHash(`${url}/chunk1`)]: {
971
+ description: 'Chunk 1',
972
+ someRef: {
973
+ $ref: '#/components/User',
974
+ },
975
+ },
976
+ [await getHash(`${url}/chunk2`)]: {
977
+ description: 'Chunk 2',
978
+ },
979
+ },
980
+ 'x-ext-urls': {
981
+ [await getHash(`${url}/chunk2`)]: `${url}/chunk2`,
982
+ [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
983
+ },
984
+ })
985
+ })
986
+
987
+ it('when bundle partial document we ensure all the dependencies references are resolved', async () => {
988
+ const url = `http://localhost:${PORT}`
989
+
990
+ const chunk1 = {
991
+ a: {
992
+ hello: 'hello',
993
+ },
994
+ }
995
+ server.get('/chunk1', (_, reply) => {
996
+ reply.send(chunk1)
997
+ })
998
+
999
+ await server.listen({ port: PORT })
1000
+
1001
+ const input = {
1002
+ a: {
1003
+ $ref: `${url}/chunk1#`,
1004
+ },
1005
+ b: {
1006
+ a: 'a',
1007
+ someReference: {
1008
+ $ref: '#/a',
1009
+ },
1010
+ },
1011
+ c: {
1012
+ $ref: `${url}/chunk2#`,
1013
+ },
1014
+ }
1015
+
1016
+ const cache = new Map()
1017
+
1018
+ // Bundle only partial
1019
+ await bundle(input.b, {
1020
+ plugins: [fetchUrls()],
1021
+ treeShake: false,
1022
+ root: input,
1023
+ cache,
1024
+ })
1025
+
1026
+ expect(input).toEqual({
1027
+ a: {
1028
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
1029
+ },
1030
+ b: {
1031
+ a: 'a',
1032
+ someReference: {
1033
+ $ref: '#/a',
1034
+ },
1035
+ },
1036
+ c: {
1037
+ $ref: `${url}/chunk2#`,
1038
+ },
1039
+ 'x-ext': {
1040
+ [await getHash(`${url}/chunk1`)]: {
1041
+ a: {
1042
+ hello: 'hello',
1043
+ },
1044
+ },
1045
+ },
1046
+ 'x-ext-urls': {
1047
+ [await getHash(`${url}/chunk1`)]: 'http://localhost:7289/chunk1',
1048
+ },
1049
+ })
1050
+ })
1051
+
1052
+ it('should correctly handle nested chunk urls', async () => {
1053
+ const url = `http://localhost:${PORT}`
1054
+
1055
+ const chunk1 = {
1056
+ chunk1: 'chunk1',
1057
+ someRef: {
1058
+ $ref: '#/b',
1059
+ },
1060
+ }
1061
+
1062
+ const chunk2 = {
1063
+ chunk2: 'chunk2',
1064
+ someRef: {
1065
+ $ref: '#/c',
1066
+ },
1067
+ }
1068
+
1069
+ const chunk3 = {
1070
+ chunk3: 'chunk3',
1071
+ }
1072
+ const external = {
1073
+ external: 'external',
1074
+ someChunk: {
1075
+ $ref: '/chunk3',
1076
+ $global: true,
1077
+ },
1078
+ }
1079
+ server.get('/chunk1', (_, reply) => {
1080
+ reply.send(chunk1)
1081
+ })
1082
+ server.get('/chunk2', (_, reply) => {
1083
+ reply.send(chunk2)
1084
+ })
1085
+ server.get('/external/chunk3', (_, reply) => {
1086
+ reply.send(chunk3)
1087
+ })
1088
+ server.get('/chunk3', (_, reply) => {
1089
+ reply.send(chunk3)
1090
+ })
1091
+ server.get('/external/document.json', (_, reply) => {
1092
+ reply.send(external)
1093
+ })
1094
+
1095
+ await server.listen({ port: PORT })
1096
+
1097
+ const input = {
1098
+ c: {
1099
+ $ref: `${url}/external/document.json`,
1100
+ },
1101
+ b: {
1102
+ $ref: `${url}/chunk2#`,
1103
+ $global: true,
1104
+ },
1105
+ a: {
1106
+ $ref: `${url}/chunk1#`,
1107
+ $global: true,
1108
+ },
1109
+ entry: {
1110
+ $ref: '#/a',
1111
+ },
1112
+ nonBundle: {
1113
+ $ref: `${url}/chunk1#`,
1114
+ },
1115
+ }
1116
+
1117
+ const cache = new Map()
1118
+
1119
+ // Bundle only partial
1120
+ await bundle(input.entry, {
1121
+ plugins: [fetchUrls()],
1122
+ treeShake: false,
1123
+ root: input,
1124
+ cache,
1125
+ urlMap: true,
1126
+ })
1127
+
1128
+ expect(input).toEqual({
1129
+ a: {
1130
+ $global: true,
1131
+ $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
1132
+ },
1133
+ b: {
1134
+ $global: true,
1135
+ $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
1136
+ },
1137
+ c: {
1138
+ $ref: `#/x-ext/${await getHash(`${url}/external/document.json`)}`,
1139
+ },
1140
+
1141
+ entry: {
1142
+ $ref: '#/a',
1143
+ },
1144
+ nonBundle: {
1145
+ $ref: `http://localhost:${PORT}/chunk1#`,
1146
+ },
1147
+ 'x-ext': {
1148
+ [await getHash(`${url}/external/document.json`)]: {
1149
+ external: 'external',
1150
+ someChunk: {
1151
+ $ref: `#/x-ext/${await getHash(`${url}/external/chunk3`)}`,
1152
+ $global: true,
1153
+ },
1154
+ },
1155
+ [await getHash(`${url}/chunk1`)]: {
1156
+ chunk1: 'chunk1',
1157
+ someRef: {
1158
+ $ref: '#/b',
1159
+ },
1160
+ },
1161
+ [await getHash(`${url}/chunk2`)]: {
1162
+ chunk2: 'chunk2',
1163
+ someRef: {
1164
+ $ref: '#/c',
1165
+ },
1166
+ },
1167
+ [await getHash(`${url}/external/chunk3`)]: {
1168
+ chunk3: 'chunk3',
1169
+ },
1170
+ },
1171
+ 'x-ext-urls': {
1172
+ [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
1173
+ [await getHash(`${url}/chunk2`)]: `${url}/chunk2`,
1174
+ [await getHash(`${url}/external/chunk3`)]: `${url}/external/chunk3`,
1175
+ [await getHash(`${url}/external/document.json`)]: `${url}/external/document.json`,
1176
+ },
1177
+ })
1178
+ })
1179
+
1180
+ it('run success hook', async () => {
1181
+ const url = `http://localhost:${PORT}`
1182
+
1183
+ const chunk1 = {
1184
+ description: 'Chunk 1',
1185
+ }
1186
+
1187
+ server.get('/chunk1', (_, reply) => {
1188
+ reply.send(chunk1)
1189
+ })
1190
+
1191
+ await server.listen({ port: PORT })
1192
+
1193
+ const input = {
1194
+ a: {
1195
+ $ref: `${url}/chunk1#`,
1196
+ },
1197
+ }
1198
+
1199
+ const resolveStart = vi.fn()
1200
+ const resolveError = vi.fn()
1201
+ const resolveSuccess = vi.fn()
1202
+
1203
+ const refA = input.a
1204
+
1205
+ await bundle(input, {
1206
+ plugins: [fetchUrls()],
1207
+ treeShake: false,
1208
+ hooks: {
1209
+ onResolveStart(value) {
1210
+ resolveStart(value)
1211
+ },
1212
+ onResolveError(value) {
1213
+ resolveError(value)
1214
+ },
1215
+ onResolveSuccess(value) {
1216
+ resolveSuccess(value)
1217
+ },
1218
+ },
1219
+ })
1220
+
1221
+ expect(resolveStart).toHaveBeenCalledOnce()
1222
+ expect(resolveStart).toHaveBeenCalledWith(refA)
1223
+ expect(resolveSuccess).toHaveBeenCalledOnce()
1224
+ expect(resolveSuccess).toHaveBeenCalledWith(refA)
1225
+ expect(resolveError).not.toHaveBeenCalledOnce()
1226
+ })
1227
+
1228
+ it('run success hook', async () => {
1229
+ const url = `http://localhost:${PORT}`
1230
+
1231
+ server.get('/chunk1', (_, reply) => {
1232
+ reply.code(404).send()
1233
+ })
1234
+
1235
+ await server.listen({ port: PORT })
1236
+
1237
+ const input = {
1238
+ a: {
1239
+ $ref: `${url}/chunk1#`,
1240
+ },
1241
+ }
1242
+
1243
+ const resolveStart = vi.fn()
1244
+ const resolveError = vi.fn()
1245
+ const resolveSuccess = vi.fn()
1246
+
1247
+ const refA = input.a
1248
+
1249
+ await bundle(input, {
1250
+ plugins: [fetchUrls()],
1251
+ treeShake: false,
1252
+ hooks: {
1253
+ onResolveStart(value) {
1254
+ resolveStart(value)
1255
+ },
1256
+ onResolveError(value) {
1257
+ resolveError(value)
1258
+ },
1259
+ onResolveSuccess(value) {
1260
+ resolveSuccess(value)
1261
+ },
1262
+ },
1263
+ })
1264
+
1265
+ expect(resolveStart).toHaveBeenCalledOnce()
1266
+ expect(resolveStart).toHaveBeenCalledWith(refA)
1267
+ expect(resolveSuccess).not.toHaveBeenCalledOnce()
1268
+ expect(resolveError).toHaveBeenCalledOnce()
1269
+ expect(resolveError).toHaveBeenCalledWith(refA)
1270
+ })
1271
+ })
1272
+
1273
+ describe('local files', () => {
1274
+ it('resolves from local files', async () => {
1275
+ const chunk1 = { a: 'a', b: 'b' }
1276
+ const chunk1Path = randomUUID()
1277
+
1278
+ await fs.writeFile(chunk1Path, JSON.stringify(chunk1))
1279
+
1280
+ const input = {
1281
+ a: {
1282
+ '$ref': `./${chunk1Path}#/a`,
1283
+ },
1284
+ }
1285
+
1286
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1287
+
1288
+ await fs.rm(chunk1Path)
1289
+
1290
+ expect(input).toEqual({
1291
+ 'x-ext': {
1292
+ [await getHash(chunk1Path)]: {
1293
+ ...chunk1,
1294
+ },
1295
+ },
1296
+ a: {
1297
+ $ref: `#/x-ext/${await getHash(chunk1Path)}/a`,
1298
+ },
1299
+ })
1300
+ })
1301
+
1302
+ it('resolves external refs from resolved files', async () => {
1303
+ const chunk1 = { a: 'a', b: 'b' }
1304
+ const chunk1Path = randomUUID()
1305
+
1306
+ const chunk2 = { a: { '$ref': `./${chunk1Path}#` } }
1307
+ const chunk2Path = randomUUID()
1308
+
1309
+ await fs.writeFile(chunk1Path, JSON.stringify(chunk1))
1310
+ await fs.writeFile(chunk2Path, JSON.stringify(chunk2))
1311
+
1312
+ const input = {
1313
+ a: {
1314
+ '$ref': `./${chunk2Path}#`,
1315
+ },
1316
+ }
1317
+
1318
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1319
+
1320
+ await fs.rm(chunk1Path)
1321
+ await fs.rm(chunk2Path)
1322
+
1323
+ expect(input).toEqual({
1324
+ 'x-ext': {
1325
+ [await getHash(chunk1Path)]: {
1326
+ ...chunk1,
1327
+ },
1328
+ [await getHash(chunk2Path)]: {
1329
+ a: { $ref: `#/x-ext/${await getHash(chunk1Path)}` },
1330
+ },
1331
+ },
1332
+ a: {
1333
+ $ref: `#/x-ext/${await getHash(chunk2Path)}`,
1334
+ },
1335
+ })
1336
+ })
1337
+
1338
+ it('resolves nested refs correctly', async () => {
1339
+ const c = {
1340
+ c: 'c',
1341
+ }
1342
+ const cName = randomUUID()
1343
+
1344
+ const b = {
1345
+ b: {
1346
+ '$ref': `./${cName}`,
1347
+ },
1348
+ }
1349
+ const bName = randomUUID()
1350
+
1351
+ await fs.mkdir('./nested')
1352
+ await fs.writeFile(`./nested/${bName}`, JSON.stringify(b))
1353
+ await fs.writeFile(`./nested/${cName}`, JSON.stringify(c))
1354
+
1355
+ const input = {
1356
+ a: {
1357
+ '$ref': `./nested/${bName}`,
1358
+ },
1359
+ }
1360
+
1361
+ await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1362
+
1363
+ await fs.rm(`./nested/${bName}`)
1364
+ await fs.rm(`./nested/${cName}`)
1365
+ await fs.rmdir('nested')
1366
+
1367
+ expect(input).toEqual({
1368
+ 'x-ext': {
1369
+ [await getHash(`nested/${cName}`)]: {
1370
+ c: 'c',
1371
+ },
1372
+ [await getHash(`nested/${bName}`)]: {
1373
+ b: { $ref: `#/x-ext/${await getHash(`nested/${cName}`)}` },
1374
+ },
1375
+ },
1376
+ a: {
1377
+ $ref: `#/x-ext/${await getHash(`nested/${bName}`)}`,
1378
+ },
1379
+ })
1380
+ })
1381
+
1382
+ it('bundles from a file input', async () => {
1383
+ const c = {
1384
+ c: 'c',
1385
+ }
1386
+ const cName = randomUUID()
1387
+
1388
+ const b = {
1389
+ b: {
1390
+ '$ref': `./${cName}`,
1391
+ },
1392
+ }
1393
+ const bName = randomUUID()
1394
+
1395
+ await fs.mkdir('./nested')
1396
+ await fs.writeFile(`./nested/${bName}`, JSON.stringify(b))
1397
+ await fs.writeFile(`./nested/${cName}`, JSON.stringify(c))
1398
+
1399
+ const input = {
1400
+ a: {
1401
+ '$ref': `./${bName}`,
1402
+ },
1403
+ }
1404
+ const inputName = randomUUID()
1405
+ await fs.writeFile(`./nested/${inputName}`, JSON.stringify(input))
1406
+
1407
+ const result = await bundle(`./nested/${bName}`, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1408
+
1409
+ await fs.rm(`./nested/${bName}`)
1410
+ await fs.rm(`./nested/${cName}`)
1411
+ await fs.rm(`./nested/${inputName}`)
1412
+ await fs.rmdir('nested')
1413
+
1414
+ expect(result).toEqual({
1415
+ 'b': {
1416
+ '$ref': `#/x-ext/${await getHash(`nested/${cName}`)}`,
1417
+ },
1418
+ 'x-ext': {
1419
+ [await getHash(`nested/${cName}`)]: {
1420
+ 'c': 'c',
1421
+ },
1422
+ },
1423
+ })
1424
+ })
1425
+ })
1426
+
1427
+ describe('json inputs', () => {
1428
+ it('should process json inputs', async () => {
1429
+ const result = await bundle('{ "openapi": "3.1", "info": { "title": "Simple API", "version": "1.0" } }', {
1430
+ treeShake: false,
1431
+ plugins: [parseJson()],
1432
+ })
1433
+
1434
+ expect(result).toEqual({
1435
+ openapi: '3.1',
1436
+ info: {
1437
+ title: 'Simple API',
1438
+ version: '1.0',
1439
+ },
1440
+ })
1441
+ })
1442
+
1443
+ it('should correctly resolve refs for json inputs', async () => {
1444
+ const chunk1 = { a: 'a', b: 'b' }
1445
+ const chunk1Path = randomUUID()
1446
+
1447
+ await fs.writeFile(chunk1Path, JSON.stringify(chunk1))
1448
+
1449
+ const input = JSON.stringify({
1450
+ a: {
1451
+ '$ref': `./${chunk1Path}#/a`,
1452
+ },
1453
+ })
1454
+
1455
+ const result = await bundle(input, { plugins: [readFiles(), parseJson()], treeShake: false })
1456
+
1457
+ await fs.rm(chunk1Path)
1458
+
1459
+ expect(result).toEqual({
1460
+ 'x-ext': {
1461
+ [await getHash(chunk1Path)]: {
1462
+ ...chunk1,
1463
+ },
1464
+ },
1465
+ a: {
1466
+ $ref: `#/x-ext/${await getHash(chunk1Path)}/a`,
1467
+ },
1468
+ })
1469
+ })
1470
+ })
1471
+
1472
+ describe('yaml inputs', () => {
1473
+ let server: FastifyInstance
1474
+ const port = 7229
1475
+ const url = `http://localhost:${port}`
1476
+
1477
+ beforeEach(() => {
1478
+ server = fastify({ logger: false })
1479
+ })
1480
+
1481
+ afterEach(async () => {
1482
+ await server.close()
1483
+ await setTimeout(100)
1484
+ })
1485
+
1486
+ it('should process yaml inputs', async () => {
1487
+ const result = await bundle('openapi: "3.1"\ninfo:\n title: Simple API\n version: "1.0"\n', {
1488
+ treeShake: false,
1489
+ plugins: [parseYaml()],
1490
+ })
1491
+
1492
+ expect(result).toEqual({
1493
+ openapi: '3.1',
1494
+ info: {
1495
+ title: 'Simple API',
1496
+ version: '1.0',
1497
+ },
1498
+ })
1499
+ })
1500
+
1501
+ it('should correctly resolve refs for yaml inputs', async () => {
1502
+ const chunk1 = { a: 'a', b: 'b' }
1503
+ const chunk1Path = randomUUID()
1504
+
1505
+ await fs.writeFile(chunk1Path, YAML.stringify(chunk1))
1506
+
1507
+ const input = YAML.stringify({
1508
+ a: {
1509
+ '$ref': `./${chunk1Path}#/a`,
1510
+ },
1511
+ })
1512
+
1513
+ const result = await bundle(input, { plugins: [parseYaml(), readFiles()], treeShake: false })
1514
+
1515
+ await fs.rm(chunk1Path)
1516
+
1517
+ expect(result).toEqual({
1518
+ 'x-ext': {
1519
+ [await getHash(chunk1Path)]: {
1520
+ ...chunk1,
1521
+ },
1522
+ },
1523
+ a: {
1524
+ $ref: `#/x-ext/${await getHash(chunk1Path)}/a`,
1525
+ },
1526
+ })
1527
+ })
1528
+
1529
+ it('should correctly load the document from an url even when yaml plugin is provided and it has high priority on the list', async () => {
1530
+ server.get('/', () => ({
1531
+ openapi: '3.1.1',
1532
+ info: {
1533
+ title: 'My API',
1534
+ },
1535
+ }))
1536
+ await server.listen({ port })
1537
+ const result = await bundle(url, {
1538
+ treeShake: false,
1539
+ plugins: [parseYaml(), fetchUrls()],
1540
+ })
1541
+
1542
+ expect(result).toEqual({
1543
+ 'info': {
1544
+ 'title': 'My API',
1545
+ },
1546
+ 'openapi': '3.1.1',
1547
+ })
1548
+ })
1549
+ })
1550
+
1551
+ describe('bundle with a certain depth', () => {
1552
+ let server: FastifyInstance
1553
+ const PORT = 7299
1554
+ const url = `http://localhost:${PORT}`
1555
+
1556
+ beforeEach(() => {
1557
+ server = fastify({ logger: false })
1558
+ })
1559
+
1560
+ afterEach(async () => {
1561
+ await server.close()
1562
+ await setTimeout(100)
1563
+ })
1564
+
1565
+ it('bundles external urls', async () => {
1566
+ const external = {
1567
+ prop: 'I am external json prop',
1568
+ }
1569
+ server.get('/', (_, reply) => {
1570
+ reply.send(external)
1571
+ })
1572
+
1573
+ await server.listen({ port: PORT })
1574
+
1575
+ const input = {
1576
+ a: {
1577
+ b: {
1578
+ c: {
1579
+ d: {
1580
+ e: {
1581
+ // Deep ref
1582
+ '$ref': `http://localhost:${PORT}#/prop`,
1583
+ },
1584
+ },
1585
+ },
1586
+ },
1587
+ },
1588
+ d: {
1589
+ e: {
1590
+ '$ref': `http://localhost:${PORT}#/prop`,
1591
+ },
1592
+ },
1593
+ }
1594
+
1595
+ await bundle(input, {
1596
+ plugins: [fetchUrls()],
1597
+ treeShake: false,
1598
+ depth: 2,
1599
+ })
1600
+
1601
+ expect(input).toEqual({
1602
+ 'x-ext': {
1603
+ [await getHash(url)]: {
1604
+ ...external,
1605
+ },
1606
+ },
1607
+ 'x-ext-urls': {
1608
+ [await getHash(url)]: url,
1609
+ },
1610
+ a: {
1611
+ b: {
1612
+ c: {
1613
+ d: {
1614
+ e: {
1615
+ $ref: `${url}#/prop`,
1616
+ },
1617
+ },
1618
+ },
1619
+ },
1620
+ },
1621
+ d: {
1622
+ e: {
1623
+ $ref: `#/x-ext/${await getHash(url)}/prop`,
1624
+ },
1625
+ },
1626
+ })
1627
+ })
1628
+
1629
+ it('will not do full bundle if we do specify a depth and reuse the same hash set', async () => {
1630
+ const external = {
1631
+ prop: 'I am external json prop',
1632
+ }
1633
+ server.get('/', (_, reply) => {
1634
+ reply.send(external)
1635
+ })
1636
+
1637
+ await server.listen({ port: PORT })
1638
+
1639
+ const input = {
1640
+ a: {
1641
+ b: {
1642
+ c: {
1643
+ d: {
1644
+ e: {
1645
+ // Deep ref
1646
+ '$ref': `http://localhost:${PORT}#/prop`,
1647
+ },
1648
+ },
1649
+ },
1650
+ },
1651
+ },
1652
+ d: {
1653
+ e: {
1654
+ '$ref': `http://localhost:${PORT}#/prop`,
1655
+ },
1656
+ },
1657
+ }
1658
+
1659
+ const visitedNodes = new Set()
1660
+
1661
+ await bundle(input, {
1662
+ plugins: [fetchUrls()],
1663
+ treeShake: false,
1664
+ depth: 2,
1665
+ visitedNodes: visitedNodes,
1666
+ })
1667
+
1668
+ expect(input).toEqual({
1669
+ 'x-ext': {
1670
+ [await getHash(url)]: {
1671
+ ...external,
1672
+ },
1673
+ },
1674
+ 'x-ext-urls': {
1675
+ [await getHash(url)]: url,
1676
+ },
1677
+ a: {
1678
+ b: {
1679
+ c: {
1680
+ d: {
1681
+ e: {
1682
+ $ref: `${url}#/prop`,
1683
+ },
1684
+ },
1685
+ },
1686
+ },
1687
+ },
1688
+ d: {
1689
+ e: {
1690
+ $ref: `#/x-ext/${await getHash(url)}/prop`,
1691
+ },
1692
+ },
1693
+ })
1694
+
1695
+ // We run a full bundle on the root of the document without a depth
1696
+ await bundle(input, {
1697
+ plugins: [fetchUrls()],
1698
+ treeShake: false,
1699
+ visitedNodes: visitedNodes,
1700
+ urlMap: true,
1701
+ })
1702
+
1703
+ // Expect the input to be the same as before
1704
+ // because we are reusing the same hash set
1705
+ expect(input).toEqual({
1706
+ 'x-ext': {
1707
+ [await getHash(url)]: {
1708
+ ...external,
1709
+ },
1710
+ },
1711
+ 'x-ext-urls': {
1712
+ [await getHash(url)]: url,
1713
+ },
1714
+ a: {
1715
+ b: {
1716
+ c: {
1717
+ d: {
1718
+ e: {
1719
+ $ref: `${url}#/prop`,
1720
+ },
1721
+ },
1722
+ },
1723
+ },
1724
+ },
1725
+ d: {
1726
+ e: {
1727
+ $ref: `#/x-ext/${await getHash(url)}/prop`,
1728
+ },
1729
+ },
1730
+ })
1731
+
1732
+ // When we run a full bundle again without the same hash set we expect a full bundle
1733
+ await bundle(input, {
1734
+ plugins: [fetchUrls()],
1735
+ treeShake: false,
1736
+ urlMap: true,
1737
+ })
1738
+
1739
+ expect(input).toEqual({
1740
+ 'x-ext': {
1741
+ [await getHash(url)]: {
1742
+ ...external,
1743
+ },
1744
+ },
1745
+ 'x-ext-urls': {
1746
+ [await getHash(url)]: url,
1747
+ },
1748
+ a: {
1749
+ b: {
1750
+ c: {
1751
+ d: {
1752
+ e: {
1753
+ $ref: `#/x-ext/${await getHash(url)}/prop`,
1754
+ },
1755
+ },
1756
+ },
1757
+ },
1758
+ },
1759
+ d: {
1760
+ e: {
1761
+ $ref: `#/x-ext/${await getHash(url)}/prop`,
1762
+ },
1763
+ },
1764
+ })
1765
+ })
1766
+ })
1767
+ })
1768
+
1769
+ describe('isRemoteUrl', () => {
1770
+ it.each([
1771
+ ['https://example.com/schema.json', true],
1772
+ ['http://api.example.com/schemas/user.json', true],
1773
+ ['file://some/path', false],
1774
+ ['random-string', false],
1775
+ ['#/components/schemas/User', false],
1776
+ ['./local-schema.json', false],
1777
+ ])('detects remote urls', (a, b) => {
1778
+ expect(isRemoteUrl(a)).toBe(b)
1779
+ })
1780
+ })
1781
+
1782
+ describe('isLocalRef', () => {
1783
+ it.each([
1784
+ ['#/components/schemas/User', true],
1785
+ ['https://example.com/schema.json', false],
1786
+ ['./local-schema.json', false],
1787
+ ])('detects local refs', (a, b) => {
1788
+ expect(isLocalRef(a)).toBe(b)
1789
+ })
1790
+ })
1791
+
1792
+ describe('getNestedValue', () => {
1793
+ it.each([
1794
+ [{ a: { b: { c: 'hello' } } }, ['a', 'b', 'c'], 'hello'],
1795
+ [{ a: { b: { c: 'hello' } } }, [], { a: { b: { c: 'hello' } } }],
1796
+ [{ foo: { bar: { baz: 42 } } }, ['foo', 'bar', 'baz'], 42],
1797
+ [{ foo: { bar: { baz: 42 } } }, ['foo', 'non-existing', 'baz'], undefined],
1798
+ ])('gets nested value', (a, b, c) => {
1799
+ expect(getNestedValue(a, b)).toEqual(c)
1800
+ })
1801
+ })
1802
+
1803
+ describe('prefixInternalRef', () => {
1804
+ it.each([
1805
+ ['#/hello', ['prefix'], '#/prefix/hello'],
1806
+ ['#/a/b/c', ['prefixA', 'prefixB'], '#/prefixA/prefixB/a/b/c'],
1807
+ ])('correctly prefix the internal refs', (a, b, c) => {
1808
+ expect(prefixInternalRef(a, b)).toEqual(c)
1809
+ })
1810
+
1811
+ it('throws when the ref is not internal', () => {
1812
+ expect(() => prefixInternalRef('http://example.com#/prefix', ['a', 'b'])).toThrowError()
1813
+ })
1814
+ })
1815
+
1816
+ describe('prefixInternalRefRecursive', () => {
1817
+ it.each([
1818
+ [
1819
+ { a: { $ref: '#/a/b' }, b: { $ref: '#' } },
1820
+ ['d', 'e', 'f'],
1821
+ { a: { $ref: '#/d/e/f/a/b' }, b: { $ref: '#/d/e/f' } },
1822
+ ],
1823
+ [
1824
+ { a: { $ref: '#/a/b' }, b: { $ref: 'http://example.com#/external' } },
1825
+ ['d', 'e', 'f'],
1826
+ { a: { $ref: '#/d/e/f/a/b' }, b: { $ref: 'http://example.com#/external' } },
1827
+ ],
1828
+ ])('recursively prefixes any internal ref with the correct values', (a, b, c) => {
1829
+ prefixInternalRefRecursive(a, b)
1830
+ expect(a).toEqual(c)
1831
+ })
1832
+ })
1833
+
1834
+ describe('setValueAtPath', () => {
1835
+ it.each([
1836
+ [{}, '/a/b/c', { hello: 'hi' }, { a: { b: { c: { hello: 'hi' } } } }],
1837
+ [{ a: { b: 'b' } }, '/a/c', { hello: 'hi' }, { a: { b: 'b', c: { hello: 'hi' } } }],
1838
+ ])('correctly sets a value at the specified path by creating new objects if necessary', (a, b, c, d) => {
1839
+ setValueAtPath(a, b, c)
1840
+
1841
+ expect(a).toEqual(d)
1842
+ })
1843
+ })