@scalar/json-magic 0.8.1 → 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 (94) hide show
  1. package/CHANGELOG.md +22 -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/bundle/value-generator.d.ts +2 -2
  10. package/dist/bundle/value-generator.d.ts.map +1 -1
  11. package/dist/bundle/value-generator.js +3 -3
  12. package/dist/bundle/value-generator.js.map +2 -2
  13. package/dist/dereference/index.d.ts.map +1 -1
  14. package/dist/dereference/index.js.map +2 -2
  15. package/dist/diff/index.d.ts +1 -1
  16. package/dist/diff/index.d.ts.map +1 -1
  17. package/dist/diff/index.js +1 -1
  18. package/dist/diff/index.js.map +2 -2
  19. package/dist/helpers/escape-json-pointer.d.ts +1 -1
  20. package/dist/helpers/escape-json-pointer.js.map +1 -1
  21. package/dist/magic-proxy/index.d.ts.map +1 -1
  22. package/dist/magic-proxy/index.js.map +2 -2
  23. package/dist/magic-proxy/proxy.d.ts +0 -1
  24. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  25. package/dist/magic-proxy/proxy.js +1 -2
  26. package/dist/magic-proxy/proxy.js.map +2 -2
  27. package/package.json +12 -14
  28. package/.turbo/turbo-build.log +0 -10
  29. package/dist/helpers/generate-hash.d.ts +0 -11
  30. package/dist/helpers/generate-hash.d.ts.map +0 -1
  31. package/dist/helpers/generate-hash.js +0 -16
  32. package/dist/helpers/generate-hash.js.map +0 -7
  33. package/esbuild.ts +0 -15
  34. package/src/bundle/bundle.test.ts +0 -2921
  35. package/src/bundle/bundle.ts +0 -916
  36. package/src/bundle/create-limiter.test.ts +0 -28
  37. package/src/bundle/create-limiter.ts +0 -52
  38. package/src/bundle/index.ts +0 -3
  39. package/src/bundle/plugins/browser.ts +0 -4
  40. package/src/bundle/plugins/fetch-urls/index.test.ts +0 -144
  41. package/src/bundle/plugins/fetch-urls/index.ts +0 -105
  42. package/src/bundle/plugins/node.ts +0 -5
  43. package/src/bundle/plugins/parse-json/index.test.ts +0 -24
  44. package/src/bundle/plugins/parse-json/index.ts +0 -32
  45. package/src/bundle/plugins/parse-yaml/index.test.ts +0 -26
  46. package/src/bundle/plugins/parse-yaml/index.ts +0 -34
  47. package/src/bundle/plugins/read-files/index.test.ts +0 -36
  48. package/src/bundle/plugins/read-files/index.ts +0 -58
  49. package/src/bundle/value-generator.test.ts +0 -165
  50. package/src/bundle/value-generator.ts +0 -143
  51. package/src/dereference/dereference.test.ts +0 -145
  52. package/src/dereference/dereference.ts +0 -84
  53. package/src/dereference/index.ts +0 -2
  54. package/src/diff/apply.test.ts +0 -262
  55. package/src/diff/apply.ts +0 -83
  56. package/src/diff/diff.test.ts +0 -328
  57. package/src/diff/diff.ts +0 -93
  58. package/src/diff/index.test.ts +0 -150
  59. package/src/diff/index.ts +0 -5
  60. package/src/diff/merge.test.ts +0 -1109
  61. package/src/diff/merge.ts +0 -136
  62. package/src/diff/trie.test.ts +0 -30
  63. package/src/diff/trie.ts +0 -113
  64. package/src/diff/utils.test.ts +0 -169
  65. package/src/diff/utils.ts +0 -111
  66. package/src/helpers/convert-to-local-ref.test.ts +0 -211
  67. package/src/helpers/convert-to-local-ref.ts +0 -43
  68. package/src/helpers/escape-json-pointer.test.ts +0 -13
  69. package/src/helpers/escape-json-pointer.ts +0 -8
  70. package/src/helpers/generate-hash.test.ts +0 -74
  71. package/src/helpers/generate-hash.ts +0 -29
  72. package/src/helpers/get-schemas.test.ts +0 -356
  73. package/src/helpers/get-schemas.ts +0 -80
  74. package/src/helpers/get-segments-from-path.test.ts +0 -17
  75. package/src/helpers/get-segments-from-path.ts +0 -17
  76. package/src/helpers/get-value-by-path.test.ts +0 -338
  77. package/src/helpers/get-value-by-path.ts +0 -44
  78. package/src/helpers/is-json-object.ts +0 -31
  79. package/src/helpers/is-object.test.ts +0 -27
  80. package/src/helpers/is-object.ts +0 -4
  81. package/src/helpers/is-yaml.ts +0 -18
  82. package/src/helpers/json-path-utils.test.ts +0 -57
  83. package/src/helpers/json-path-utils.ts +0 -50
  84. package/src/helpers/normalize.test.ts +0 -92
  85. package/src/helpers/normalize.ts +0 -35
  86. package/src/helpers/unescape-json-pointer.test.ts +0 -23
  87. package/src/helpers/unescape-json-pointer.ts +0 -9
  88. package/src/magic-proxy/index.ts +0 -2
  89. package/src/magic-proxy/proxy.test.ts +0 -1987
  90. package/src/magic-proxy/proxy.ts +0 -323
  91. package/src/types.ts +0 -1
  92. package/tsconfig.build.json +0 -12
  93. package/tsconfig.json +0 -16
  94. package/vite.config.ts +0 -8
@@ -1,2921 +0,0 @@
1
- import { randomUUID } from 'node:crypto'
2
- import fs from 'node:fs/promises'
3
- import { setTimeout } from 'node:timers/promises'
4
-
5
- import { consoleWarnSpy, resetConsoleSpies } from '@scalar/helpers/testing/console-spies'
6
- import fastify, { type FastifyInstance } from 'fastify'
7
- import { beforeEach, describe, expect, it, vi } from 'vitest'
8
- import YAML from 'yaml'
9
-
10
- import { parseJson } from '@/bundle/plugins/parse-json'
11
- import { parseYaml } from '@/bundle/plugins/parse-yaml'
12
- import { getHash } from '@/bundle/value-generator'
13
-
14
- import {
15
- type LoaderPlugin,
16
- bundle,
17
- isLocalRef,
18
- isRemoteUrl,
19
- prefixInternalRef,
20
- prefixInternalRefRecursive,
21
- resolveAndCopyReferences,
22
- setValueAtPath,
23
- } from './bundle'
24
- import { fetchUrls } from './plugins/fetch-urls'
25
- import { readFiles } from './plugins/read-files'
26
-
27
- describe('bundle', () => {
28
- describe('external urls', () => {
29
- let server: FastifyInstance
30
- const port = 7289
31
- const url = `http://localhost:${port}`
32
-
33
- beforeEach(() => {
34
- server = fastify({ logger: false })
35
-
36
- return async () => {
37
- await server.close()
38
- await setTimeout(100)
39
- }
40
- })
41
-
42
- it('bundles external urls', async () => {
43
- const url = `http://localhost:${port}`
44
-
45
- const external = {
46
- prop: 'I am external json prop',
47
- }
48
- server.get('/', (_, reply) => {
49
- reply.send(external)
50
- })
51
-
52
- await server.listen({ port: port })
53
-
54
- const input = {
55
- a: {
56
- b: {
57
- c: 'hello',
58
- },
59
- },
60
- d: {
61
- '$ref': `http://localhost:${port}#/prop`,
62
- },
63
- }
64
-
65
- await bundle(input, {
66
- plugins: [fetchUrls(), readFiles()],
67
- treeShake: false,
68
- })
69
-
70
- expect(input).toEqual({
71
- 'x-ext': {
72
- [await getHash(url)]: {
73
- ...external,
74
- },
75
- },
76
- a: {
77
- b: {
78
- c: 'hello',
79
- },
80
- },
81
- d: {
82
- $ref: `#/x-ext/${await getHash(url)}/prop`,
83
- },
84
- })
85
- })
86
-
87
- it('bundles external urls from resolved external piece', async () => {
88
- const url = `http://localhost:${port}`
89
- const chunk2 = {
90
- hey: 'hey',
91
- nested: {
92
- key: 'value',
93
- },
94
- internal: '#/nested/key',
95
- }
96
-
97
- const chunk1 = {
98
- a: {
99
- hello: 'hello',
100
- },
101
- b: {
102
- '$ref': `${url}/chunk2#`,
103
- },
104
- }
105
-
106
- server.get('/chunk1', (_, reply) => {
107
- reply.send(chunk1)
108
- })
109
- server.get('/chunk2', (_, reply) => {
110
- reply.send(chunk2)
111
- })
112
-
113
- await server.listen({ port: port })
114
-
115
- const input = {
116
- a: {
117
- b: {
118
- c: {
119
- '$ref': `${url}/chunk1#`,
120
- },
121
- },
122
- },
123
- }
124
-
125
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
126
-
127
- expect(input).toEqual({
128
- 'x-ext': {
129
- [await getHash(`${url}/chunk1`)]: {
130
- ...chunk1,
131
- b: {
132
- $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
133
- },
134
- },
135
- [await getHash(`${url}/chunk2`)]: {
136
- ...chunk2,
137
- internal: '#/nested/key',
138
- },
139
- },
140
- a: {
141
- b: {
142
- c: {
143
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
144
- },
145
- },
146
- },
147
- })
148
- })
149
-
150
- it('should correctly handle only urls without a pointer', async () => {
151
- const url = `http://localhost:${port}`
152
-
153
- server.get('/', (_, reply) => {
154
- reply.send({
155
- a: 'a',
156
- })
157
- })
158
-
159
- await server.listen({ port: port })
160
-
161
- const input = {
162
- a: {
163
- b: {
164
- '$ref': `${url}`,
165
- },
166
- },
167
- }
168
-
169
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
170
-
171
- expect(input).toEqual({
172
- 'x-ext': {
173
- [await getHash(url)]: {
174
- a: 'a',
175
- },
176
- },
177
- a: {
178
- b: {
179
- $ref: `#/x-ext/${await getHash(url)}`,
180
- },
181
- },
182
- })
183
- })
184
-
185
- it('caches results for same resource', async () => {
186
- const fn = vi.fn()
187
- const url = `http://localhost:${port}`
188
-
189
- server.get('/', (_, reply) => {
190
- fn()
191
- reply.send({
192
- a: 'a',
193
- b: 'b',
194
- })
195
- })
196
-
197
- await server.listen({ port: port })
198
-
199
- const input = {
200
- a: {
201
- '$ref': `${url}#/a`,
202
- },
203
- b: {
204
- '$ref': `${url}#/b`,
205
- },
206
- }
207
-
208
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
209
-
210
- expect(input).toEqual({
211
- 'x-ext': {
212
- [await getHash(url)]: {
213
- a: 'a',
214
- b: 'b',
215
- },
216
- },
217
- a: {
218
- $ref: `#/x-ext/${await getHash(url)}/a`,
219
- },
220
- b: {
221
- $ref: `#/x-ext/${await getHash(url)}/b`,
222
- },
223
- })
224
-
225
- // We expect the bundler to cache the result for the same url
226
- expect(fn.mock.calls.length).toBe(1)
227
- })
228
-
229
- it('handles correctly external nested refs', async () => {
230
- const url = `http://localhost:${port}`
231
-
232
- server.get('/nested/another-file.json', (_, reply) => {
233
- reply.send({
234
- c: 'c',
235
- })
236
- })
237
-
238
- server.get('/nested/chunk1.json', (_, reply) => {
239
- reply.send({
240
- b: {
241
- '$ref': './another-file.json#',
242
- },
243
- })
244
- })
245
-
246
- await server.listen({ port: port })
247
-
248
- const input = {
249
- a: {
250
- '$ref': `${url}/nested/chunk1.json#`,
251
- },
252
- }
253
-
254
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
255
-
256
- expect(input).toEqual({
257
- 'x-ext': {
258
- [await getHash(`${url}/nested/another-file.json`)]: {
259
- c: 'c',
260
- },
261
- [await getHash(`${url}/nested/chunk1.json`)]: {
262
- b: {
263
- $ref: `#/x-ext/${await getHash(`${url}/nested/another-file.json`)}`,
264
- },
265
- },
266
- },
267
- a: {
268
- $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
269
- },
270
- })
271
- })
272
-
273
- it('does not merge paths when we use absolute urls', async () => {
274
- const url = `http://localhost:${port}`
275
-
276
- server.get('/top-level', (_, reply) => {
277
- reply.send({
278
- c: 'c',
279
- })
280
- })
281
-
282
- server.get('/nested/chunk1.json', (_, reply) => {
283
- reply.send({
284
- b: {
285
- '$ref': `${url}/top-level#`,
286
- },
287
- })
288
- })
289
-
290
- await server.listen({ port: port })
291
-
292
- const input = {
293
- a: {
294
- '$ref': `${url}/nested/chunk1.json`,
295
- },
296
- }
297
-
298
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
299
-
300
- expect(input).toEqual({
301
- 'x-ext': {
302
- [await getHash(`${url}/top-level`)]: {
303
- c: 'c',
304
- },
305
- [await getHash(`${url}/nested/chunk1.json`)]: {
306
- b: {
307
- $ref: `#/x-ext/${await getHash(`${url}/top-level`)}`,
308
- },
309
- },
310
- },
311
- a: {
312
- $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
313
- },
314
- })
315
- })
316
-
317
- it('bundles from a url input', async () => {
318
- const url = `http://localhost:${port}`
319
-
320
- server.get('/top-level', (_, reply) => {
321
- reply.send({
322
- c: 'c',
323
- })
324
- })
325
-
326
- server.get('/nested/chunk1.json', (_, reply) => {
327
- reply.send({
328
- b: {
329
- '$ref': `${url}/top-level#`,
330
- },
331
- })
332
- })
333
-
334
- server.get('/base/openapi.json', (_, reply) => {
335
- reply.send({
336
- a: {
337
- $ref: '../nested/chunk1.json',
338
- },
339
- })
340
- })
341
-
342
- await server.listen({ port: port })
343
-
344
- const output = await bundle(`${url}/base/openapi.json`, { plugins: [fetchUrls()], treeShake: false })
345
-
346
- expect(output).toEqual({
347
- 'x-ext': {
348
- [await getHash(`${url}/top-level`)]: {
349
- c: 'c',
350
- },
351
- [await getHash(`${url}/nested/chunk1.json`)]: {
352
- b: {
353
- $ref: `#/x-ext/${await getHash(`${url}/top-level`)}`,
354
- },
355
- },
356
- },
357
- a: {
358
- $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
359
- },
360
- })
361
- })
362
-
363
- it('generated a map when we turn the urlMap on', async () => {
364
- const url = `http://localhost:${port}`
365
-
366
- server.get('/top-level', (_, reply) => {
367
- reply.send({
368
- c: 'c',
369
- })
370
- })
371
-
372
- server.get('/nested/chunk1.json', (_, reply) => {
373
- reply.send({
374
- b: {
375
- '$ref': `${url}/top-level#`,
376
- },
377
- })
378
- })
379
-
380
- server.get('/base/openapi.json', (_, reply) => {
381
- reply.send({
382
- a: {
383
- $ref: '../nested/chunk1.json',
384
- },
385
- })
386
- })
387
-
388
- await server.listen({ port: port })
389
-
390
- const output = await bundle(`${url}/base/openapi.json`, {
391
- plugins: [fetchUrls()],
392
- treeShake: false,
393
- urlMap: true,
394
- })
395
-
396
- expect(output).toEqual({
397
- 'x-ext': {
398
- [await getHash(`${url}/top-level`)]: {
399
- c: 'c',
400
- },
401
- [await getHash(`${url}/nested/chunk1.json`)]: {
402
- b: {
403
- $ref: `#/x-ext/${await getHash(`${url}/top-level`)}`,
404
- },
405
- },
406
- },
407
- 'x-ext-urls': {
408
- [await getHash(`${url}/top-level`)]: `${url}/top-level`,
409
- [await getHash(`${url}/nested/chunk1.json`)]: `${url}/nested/chunk1.json`,
410
- },
411
- a: {
412
- $ref: `#/x-ext/${await getHash(`${url}/nested/chunk1.json`)}`,
413
- },
414
- })
415
- })
416
-
417
- it('prefixes the refs only once', async () => {
418
- const url = `http://localhost:${port}`
419
-
420
- const chunk2 = {
421
- a: 'a',
422
- b: {
423
- '$ref': `${url}/chunk1#`,
424
- },
425
- }
426
- const chunk1 = {
427
- a: {
428
- hello: 'hello',
429
- },
430
- b: {
431
- '$ref': `${url}/chunk2#`,
432
- },
433
- }
434
-
435
- server.get('/chunk1', (_, reply) => {
436
- reply.send(chunk1)
437
- })
438
- server.get('/chunk2', (_, reply) => {
439
- reply.send(chunk2)
440
- })
441
-
442
- await server.listen({ port: port })
443
-
444
- const input = {
445
- a: {
446
- b: {
447
- c: {
448
- '$ref': `${url}/chunk1#`,
449
- },
450
- d: {
451
- e: {
452
- f: {
453
- g: {
454
- '$ref': `${url}/chunk1#`,
455
- },
456
- },
457
- },
458
- },
459
- },
460
- },
461
- }
462
-
463
- await bundle(input, { plugins: [fetchUrls()], treeShake: false })
464
-
465
- expect(input).toEqual({
466
- a: {
467
- b: {
468
- c: {
469
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
470
- },
471
- d: {
472
- e: {
473
- f: {
474
- g: {
475
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
476
- },
477
- },
478
- },
479
- },
480
- },
481
- },
482
- 'x-ext': {
483
- [await getHash(`${url}/chunk1`)]: {
484
- a: {
485
- hello: 'hello',
486
- },
487
- b: {
488
- $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
489
- },
490
- },
491
- [await getHash(`${url}/chunk2`)]: {
492
- a: 'a',
493
- b: {
494
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
495
- },
496
- },
497
- },
498
- })
499
- })
500
-
501
- it('bundles array references', async () => {
502
- const url = `http://localhost:${port}`
503
-
504
- const chunk1 = {
505
- a: {
506
- hello: 'hello',
507
- },
508
- }
509
-
510
- server.get('/chunk1', (_, reply) => {
511
- reply.send(chunk1)
512
- })
513
-
514
- await server.listen({ port: port })
515
-
516
- const input = {
517
- a: [
518
- {
519
- $ref: `${url}/chunk1#`,
520
- },
521
- ],
522
- }
523
- await bundle(input, {
524
- plugins: [fetchUrls()],
525
- treeShake: false,
526
- })
527
-
528
- expect(input).toEqual({
529
- a: [
530
- {
531
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
532
- },
533
- ],
534
- 'x-ext': {
535
- [await getHash(`${url}/chunk1`)]: {
536
- a: {
537
- hello: 'hello',
538
- },
539
- },
540
- },
541
- })
542
- })
543
-
544
- it('bundles subpart of the document', async () => {
545
- const url = `http://localhost:${port}`
546
-
547
- const chunk1 = {
548
- a: {
549
- hello: 'hello',
550
- },
551
- }
552
-
553
- const fn = vi.fn()
554
-
555
- server.get('/chunk1', (_, reply) => {
556
- fn()
557
- reply.send(chunk1)
558
- })
559
-
560
- await server.listen({ port: port })
561
-
562
- const input = {
563
- a: {
564
- $ref: `${url}/chunk1#`,
565
- },
566
- b: {
567
- $ref: `${url}/chunk1#`,
568
- },
569
- c: {
570
- $ref: `${url}/chunk1#`,
571
- },
572
- }
573
-
574
- const cache = new Map()
575
-
576
- // Bundle only partial
577
- await bundle(input.b, {
578
- plugins: [fetchUrls()],
579
- treeShake: false,
580
- root: input,
581
- cache,
582
- })
583
-
584
- expect(input).toEqual({
585
- a: {
586
- $ref: `${url}/chunk1#`,
587
- },
588
- b: {
589
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
590
- },
591
- c: {
592
- $ref: `${url}/chunk1#`,
593
- },
594
- 'x-ext': {
595
- [await getHash(`${url}/chunk1`)]: {
596
- a: {
597
- hello: 'hello',
598
- },
599
- },
600
- },
601
- 'x-ext-urls': {
602
- [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
603
- },
604
- })
605
-
606
- // Bundle only partial
607
- await bundle(input.c, {
608
- plugins: [fetchUrls()],
609
- treeShake: false,
610
- root: input,
611
- cache,
612
- })
613
-
614
- expect(input).toEqual({
615
- a: {
616
- $ref: `${url}/chunk1#`,
617
- },
618
- b: {
619
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
620
- },
621
- c: {
622
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
623
- },
624
- 'x-ext': {
625
- [await getHash(`${url}/chunk1`)]: {
626
- a: {
627
- hello: 'hello',
628
- },
629
- },
630
- },
631
- 'x-ext-urls': {
632
- [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
633
- },
634
- })
635
-
636
- expect(fn).toHaveBeenCalledTimes(1)
637
- })
638
-
639
- it('always emits the url mappings when doing partial bundle', async () => {
640
- const url = `http://localhost:${port}`
641
-
642
- const chunk1 = {
643
- a: {
644
- hello: 'hello',
645
- },
646
- }
647
-
648
- server.get('/chunk1', (_, reply) => {
649
- reply.send(chunk1)
650
- })
651
-
652
- await server.listen({ port: port })
653
-
654
- const input = {
655
- a: {
656
- $ref: `${url}/chunk1#`,
657
- },
658
- b: {
659
- $ref: `${url}/chunk1#`,
660
- },
661
- c: {
662
- $ref: `${url}/chunk1#`,
663
- },
664
- }
665
-
666
- const cache = new Map()
667
-
668
- // Bundle only partial
669
- await bundle(input.b, {
670
- plugins: [fetchUrls()],
671
- treeShake: false,
672
- root: input,
673
- cache,
674
- urlMap: false, // Set the urlMapping to false
675
- })
676
-
677
- expect(input).toEqual({
678
- a: {
679
- $ref: `${url}/chunk1#`,
680
- },
681
- b: {
682
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
683
- },
684
- c: {
685
- $ref: `${url}/chunk1#`,
686
- },
687
- 'x-ext': {
688
- [await getHash(`${url}/chunk1`)]: {
689
- a: {
690
- hello: 'hello',
691
- },
692
- },
693
- },
694
- // It should still inject the mappings on the output document
695
- 'x-ext-urls': {
696
- [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
697
- },
698
- })
699
- })
700
-
701
- it('tree shakes the external documents correctly', async () => {
702
- const url = `http://localhost:${port}`
703
-
704
- const chunk1 = {
705
- a: {
706
- b: {
707
- hello: 'hello',
708
- g: {
709
- $ref: '#/d/e',
710
- },
711
- },
712
- c: 'c',
713
- },
714
- d: {
715
- e: { message: 'I should be included' },
716
- f: { message: 'I should be excluded on the final bundle' },
717
- },
718
- }
719
-
720
- server.get('/chunk1', (_, reply) => {
721
- reply.send(chunk1)
722
- })
723
-
724
- await server.listen({ port: port })
725
-
726
- const input = {
727
- a: {
728
- $ref: `${url}/chunk1#/a/b`,
729
- },
730
- }
731
-
732
- await bundle(input, { plugins: [fetchUrls()], treeShake: true })
733
-
734
- expect(input).toEqual({
735
- a: {
736
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a/b`,
737
- },
738
- 'x-ext': {
739
- [await getHash(`${url}/chunk1`)]: {
740
- a: {
741
- b: {
742
- g: {
743
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/d/e`,
744
- },
745
- hello: 'hello',
746
- },
747
- },
748
- d: {
749
- e: { message: 'I should be included' },
750
- },
751
- },
752
- },
753
- })
754
- })
755
-
756
- it('tree shakes correctly when working with nested external refs', async () => {
757
- const url = `http://localhost:${port}`
758
-
759
- const chunk2 = {
760
- a: {
761
- b: {
762
- hello: 'hello',
763
- },
764
- hi: 'hi',
765
- },
766
- }
767
-
768
- const chunk1 = {
769
- a: {
770
- b: {
771
- hello: 'hello',
772
- g: {
773
- $ref: '#/d/e',
774
- },
775
- },
776
- c: 'c',
777
- external: {
778
- $ref: './chunk2#/a/b',
779
- },
780
- },
781
- d: {
782
- e: { message: 'I should be included' },
783
- f: { message: 'I should be excluded on the final bundle' },
784
- },
785
- }
786
-
787
- server.get('/chunk1', (_, reply) => {
788
- reply.send(chunk1)
789
- })
790
-
791
- server.get('/chunk2', (_, reply) => {
792
- reply.send(chunk2)
793
- })
794
-
795
- await server.listen({ port: port })
796
-
797
- const input = {
798
- a: {
799
- $ref: `${url}/chunk1#/a`,
800
- },
801
- }
802
-
803
- await bundle(input, { plugins: [fetchUrls()], treeShake: true })
804
-
805
- expect(input).toEqual({
806
- a: {
807
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a`,
808
- },
809
- 'x-ext': {
810
- [await getHash(`${url}/chunk1`)]: {
811
- a: {
812
- b: {
813
- g: {
814
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/d/e`,
815
- },
816
- hello: 'hello',
817
- },
818
- c: 'c',
819
- 'external': {
820
- $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}/a/b`,
821
- },
822
- },
823
- d: {
824
- e: {
825
- 'message': 'I should be included',
826
- },
827
- },
828
- },
829
- [await getHash(`${url}/chunk2`)]: {
830
- a: {
831
- b: {
832
- hello: 'hello',
833
- },
834
- },
835
- },
836
- },
837
- })
838
- })
839
-
840
- it('handles circular references when we treeshake', async () => {
841
- const url = `http://localhost:${port}`
842
-
843
- const chunk1 = {
844
- a: {
845
- b: {
846
- hello: 'hello',
847
- g: {
848
- $ref: '#/a/external',
849
- },
850
- },
851
- c: 'c',
852
- external: {
853
- $ref: '#/a/b',
854
- },
855
- },
856
- }
857
-
858
- server.get('/chunk1', (_, reply) => {
859
- reply.send(chunk1)
860
- })
861
-
862
- await server.listen({ port: port })
863
-
864
- const input = {
865
- a: {
866
- $ref: `${url}/chunk1#/a`,
867
- },
868
- }
869
-
870
- await bundle(input, { plugins: [fetchUrls()], treeShake: true })
871
-
872
- expect(input).toEqual({
873
- a: {
874
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a`,
875
- },
876
- 'x-ext': {
877
- [await getHash(`${url}/chunk1`)]: {
878
- a: {
879
- b: {
880
- g: {
881
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a/external`,
882
- },
883
- hello: 'hello',
884
- },
885
- c: 'c',
886
- external: {
887
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}/a/b`,
888
- },
889
- },
890
- },
891
- },
892
- })
893
- })
894
-
895
- it('handles chunks', async () => {
896
- const url = `http://localhost:${port}`
897
-
898
- const chunk1 = {
899
- description: 'Chunk 1',
900
- someRef: {
901
- $ref: '#/components/User',
902
- },
903
- }
904
-
905
- const chunk2 = {
906
- description: 'Chunk 2',
907
- }
908
-
909
- server.get('/chunk1', (_, reply) => {
910
- reply.send(chunk1)
911
- })
912
- server.get('/chunk2', (_, reply) => {
913
- reply.send(chunk2)
914
- })
915
-
916
- await server.listen({ port: port })
917
-
918
- const input = {
919
- a: {
920
- $ref: `${url}/chunk1#`,
921
- $global: true,
922
- },
923
- b: {
924
- $ref: `${url}/chunk2#`,
925
- $global: true,
926
- },
927
- c: {
928
- $ref: `${url}/chunk1#`,
929
- $global: true,
930
- },
931
- components: {
932
- User: {
933
- id: 'number',
934
- name: {
935
- $ref: '#/a',
936
- },
937
- another: {
938
- $ref: '#/b',
939
- },
940
- },
941
- },
942
- }
943
-
944
- // Bundle only partial
945
- await bundle(input.a, {
946
- plugins: [fetchUrls()],
947
- treeShake: false,
948
- root: input,
949
- })
950
-
951
- expect(input).toEqual({
952
- a: {
953
- $global: true,
954
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
955
- },
956
- b: {
957
- $global: true,
958
- $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
959
- },
960
- c: {
961
- $global: true,
962
- $ref: `${url}/chunk1#`,
963
- },
964
- components: {
965
- User: {
966
- another: {
967
- $ref: '#/b',
968
- },
969
- id: 'number',
970
- name: {
971
- $ref: '#/a',
972
- },
973
- },
974
- },
975
- 'x-ext': {
976
- [await getHash(`${url}/chunk1`)]: {
977
- description: 'Chunk 1',
978
- someRef: {
979
- $ref: '#/components/User',
980
- },
981
- },
982
- [await getHash(`${url}/chunk2`)]: {
983
- description: 'Chunk 2',
984
- },
985
- },
986
- 'x-ext-urls': {
987
- [await getHash(`${url}/chunk2`)]: `${url}/chunk2`,
988
- [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
989
- },
990
- })
991
- })
992
-
993
- it('when bundle partial document we ensure all the dependencies references are resolved', async () => {
994
- const url = `http://localhost:${port}`
995
-
996
- const chunk1 = {
997
- a: {
998
- hello: 'hello',
999
- },
1000
- }
1001
- server.get('/chunk1', (_, reply) => {
1002
- reply.send(chunk1)
1003
- })
1004
-
1005
- await server.listen({ port: port })
1006
-
1007
- const input = {
1008
- a: {
1009
- $ref: `${url}/chunk1#`,
1010
- },
1011
- b: {
1012
- a: 'a',
1013
- someReference: {
1014
- $ref: '#/a',
1015
- },
1016
- },
1017
- c: {
1018
- $ref: `${url}/chunk2#`,
1019
- },
1020
- }
1021
-
1022
- const cache = new Map()
1023
-
1024
- // Bundle only partial
1025
- await bundle(input.b, {
1026
- plugins: [fetchUrls()],
1027
- treeShake: false,
1028
- root: input,
1029
- cache,
1030
- })
1031
-
1032
- expect(input).toEqual({
1033
- a: {
1034
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
1035
- },
1036
- b: {
1037
- a: 'a',
1038
- someReference: {
1039
- $ref: '#/a',
1040
- },
1041
- },
1042
- c: {
1043
- $ref: `${url}/chunk2#`,
1044
- },
1045
- 'x-ext': {
1046
- [await getHash(`${url}/chunk1`)]: {
1047
- a: {
1048
- hello: 'hello',
1049
- },
1050
- },
1051
- },
1052
- 'x-ext-urls': {
1053
- [await getHash(`${url}/chunk1`)]: 'http://localhost:7289/chunk1',
1054
- },
1055
- })
1056
- })
1057
-
1058
- it('should correctly handle nested chunk urls', async () => {
1059
- const url = `http://localhost:${port}`
1060
-
1061
- const chunk1 = {
1062
- chunk1: 'chunk1',
1063
- someRef: {
1064
- $ref: '#/b',
1065
- },
1066
- }
1067
-
1068
- const chunk2 = {
1069
- chunk2: 'chunk2',
1070
- someRef: {
1071
- $ref: '#/c',
1072
- },
1073
- }
1074
-
1075
- const chunk3 = {
1076
- chunk3: 'chunk3',
1077
- }
1078
- const external = {
1079
- external: 'external',
1080
- someChunk: {
1081
- $ref: '/chunk3',
1082
- $global: true,
1083
- },
1084
- }
1085
- server.get('/chunk1', (_, reply) => {
1086
- reply.send(chunk1)
1087
- })
1088
- server.get('/chunk2', (_, reply) => {
1089
- reply.send(chunk2)
1090
- })
1091
- server.get('/external/chunk3', (_, reply) => {
1092
- reply.send(chunk3)
1093
- })
1094
- server.get('/chunk3', (_, reply) => {
1095
- reply.send(chunk3)
1096
- })
1097
- server.get('/external/document.json', (_, reply) => {
1098
- reply.send(external)
1099
- })
1100
-
1101
- await server.listen({ port: port })
1102
-
1103
- const input = {
1104
- c: {
1105
- $ref: `${url}/external/document.json`,
1106
- },
1107
- b: {
1108
- $ref: `${url}/chunk2`,
1109
- $global: true,
1110
- },
1111
- a: {
1112
- $ref: `${url}/chunk1`,
1113
- $global: true,
1114
- },
1115
- entry: {
1116
- $ref: '#/a',
1117
- },
1118
- nonBundle: {
1119
- $ref: `${url}/chunk1`,
1120
- },
1121
- }
1122
-
1123
- const cache = new Map()
1124
-
1125
- // Bundle only partial
1126
- await bundle(input.entry, {
1127
- plugins: [fetchUrls()],
1128
- treeShake: false,
1129
- root: input,
1130
- cache,
1131
- urlMap: true,
1132
- })
1133
-
1134
- expect(input).toEqual({
1135
- a: {
1136
- $global: true,
1137
- $ref: `#/x-ext/${await getHash(`${url}/chunk1`)}`,
1138
- },
1139
- b: {
1140
- $global: true,
1141
- $ref: `#/x-ext/${await getHash(`${url}/chunk2`)}`,
1142
- },
1143
- c: {
1144
- $ref: `#/x-ext/${await getHash(`${url}/external/document.json`)}`,
1145
- },
1146
-
1147
- entry: {
1148
- $ref: '#/a',
1149
- },
1150
- nonBundle: {
1151
- $ref: `http://localhost:${port}/chunk1`,
1152
- },
1153
- 'x-ext': {
1154
- [await getHash(`${url}/external/document.json`)]: {
1155
- external: 'external',
1156
- someChunk: {
1157
- $ref: `#/x-ext/${await getHash(`${url}/chunk3`)}`,
1158
- $global: true,
1159
- },
1160
- },
1161
- [await getHash(`${url}/chunk1`)]: {
1162
- chunk1: 'chunk1',
1163
- someRef: {
1164
- $ref: '#/b',
1165
- },
1166
- },
1167
- [await getHash(`${url}/chunk2`)]: {
1168
- chunk2: 'chunk2',
1169
- someRef: {
1170
- $ref: '#/c',
1171
- },
1172
- },
1173
- [await getHash(`${url}/chunk3`)]: {
1174
- chunk3: 'chunk3',
1175
- },
1176
- },
1177
- 'x-ext-urls': {
1178
- [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
1179
- [await getHash(`${url}/chunk2`)]: `${url}/chunk2`,
1180
- [await getHash(`${url}/chunk3`)]: `${url}/chunk3`,
1181
- [await getHash(`${url}/external/document.json`)]: `${url}/external/document.json`,
1182
- },
1183
- })
1184
- })
1185
-
1186
- it('run success hook', async () => {
1187
- const url = `http://localhost:${port}`
1188
-
1189
- const chunk1 = {
1190
- description: 'Chunk 1',
1191
- }
1192
-
1193
- server.get('/chunk1', (_, reply) => {
1194
- reply.send(chunk1)
1195
- })
1196
-
1197
- await server.listen({ port: port })
1198
-
1199
- const input = {
1200
- a: {
1201
- $ref: `${url}/chunk1#`,
1202
- },
1203
- }
1204
-
1205
- const resolveStart = vi.fn()
1206
- const resolveError = vi.fn()
1207
- const resolveSuccess = vi.fn()
1208
-
1209
- const refA = input.a
1210
-
1211
- await bundle(input, {
1212
- plugins: [fetchUrls()],
1213
- treeShake: false,
1214
- hooks: {
1215
- onResolveStart(value) {
1216
- resolveStart(value)
1217
- },
1218
- onResolveError(value) {
1219
- resolveError(value)
1220
- },
1221
- onResolveSuccess(value) {
1222
- resolveSuccess(value)
1223
- },
1224
- },
1225
- })
1226
-
1227
- expect(resolveStart).toHaveBeenCalledOnce()
1228
- expect(resolveStart).toHaveBeenCalledWith(refA)
1229
- expect(resolveSuccess).toHaveBeenCalledOnce()
1230
- expect(resolveSuccess).toHaveBeenCalledWith(refA)
1231
- expect(resolveError).not.toHaveBeenCalledOnce()
1232
- })
1233
-
1234
- it('run error hook', async () => {
1235
- const url = `http://localhost:${port}`
1236
-
1237
- server.get('/chunk1', (_, reply) => {
1238
- reply.code(404).send()
1239
- })
1240
-
1241
- await server.listen({ port: port })
1242
-
1243
- const input = {
1244
- a: {
1245
- $ref: `${url}/chunk1#`,
1246
- },
1247
- }
1248
-
1249
- const resolveStart = vi.fn()
1250
- const resolveError = vi.fn()
1251
- const resolveSuccess = vi.fn()
1252
-
1253
- const refA = input.a
1254
-
1255
- await bundle(input, {
1256
- plugins: [fetchUrls()],
1257
- treeShake: false,
1258
- hooks: {
1259
- onResolveStart(value) {
1260
- resolveStart(value)
1261
- },
1262
- onResolveError(value) {
1263
- resolveError(value)
1264
- },
1265
- onResolveSuccess(value) {
1266
- resolveSuccess(value)
1267
- },
1268
- },
1269
- })
1270
-
1271
- expect(resolveStart).toHaveBeenCalledOnce()
1272
- expect(resolveStart).toHaveBeenCalledWith(refA)
1273
- expect(resolveSuccess).not.toHaveBeenCalledOnce()
1274
- expect(resolveError).toHaveBeenCalledOnce()
1275
- expect(resolveError).toHaveBeenCalledWith(refA)
1276
- })
1277
-
1278
- it('uses the provided origin for object inputs', async () => {
1279
- server.get('/', () => ({
1280
- message: 'some resolved external reference',
1281
- }))
1282
- await server.listen({ port })
1283
-
1284
- const result = await bundle(
1285
- {
1286
- a: {
1287
- $ref: '/#',
1288
- },
1289
- },
1290
- {
1291
- treeShake: false,
1292
- plugins: [fetchUrls()],
1293
- origin: url,
1294
- },
1295
- )
1296
-
1297
- expect(result).toEqual({
1298
- 'a': {
1299
- '$ref': '#/x-ext/8706c8f',
1300
- },
1301
- 'x-ext': {
1302
- '8706c8f': {
1303
- 'message': 'some resolved external reference',
1304
- },
1305
- },
1306
- })
1307
- })
1308
-
1309
- it('prioritizes configuration origin rather than document input url', async () => {
1310
- server.get('/', () => ({
1311
- a: {
1312
- $ref: '/d#',
1313
- },
1314
- }))
1315
- server.get('/d', () => ({
1316
- message: 'some resolved external reference',
1317
- }))
1318
- await server.listen({ port: port })
1319
-
1320
- const result = await bundle(url, {
1321
- plugins: [fetchUrls()],
1322
- treeShake: false,
1323
- origin: `${url}/a/b/c`,
1324
- })
1325
-
1326
- expect(result).toEqual({
1327
- a: { '$ref': '#/x-ext/6b07652' },
1328
- 'x-ext': { '6b07652': { message: 'some resolved external reference' } },
1329
- })
1330
- })
1331
-
1332
- it('does not modify external URLs when already defined by $id property', async () => {
1333
- const url = `http://localhost:${port}`
1334
-
1335
- const input = {
1336
- $id: 'https://example.com/root',
1337
- components: {
1338
- schemas: {
1339
- User: {
1340
- $id: `${url}/schema`,
1341
- type: 'object',
1342
- properties: {
1343
- name: { type: 'string' },
1344
- },
1345
- },
1346
- },
1347
- },
1348
- paths: {
1349
- '/users': {
1350
- get: {
1351
- responses: {
1352
- '200': {
1353
- description: 'Success',
1354
- content: {
1355
- 'application/json': {
1356
- schema: {
1357
- $ref: `${url}/schema`,
1358
- },
1359
- },
1360
- },
1361
- },
1362
- },
1363
- },
1364
- },
1365
- },
1366
- }
1367
-
1368
- await bundle(input, {
1369
- plugins: [fetchUrls(), readFiles()],
1370
- treeShake: false,
1371
- })
1372
-
1373
- // The $ref should remain unchanged because the schema is already defined locally with $id
1374
- expect(input.paths['/users'].get.responses['200'].content['application/json'].schema.$ref).toBe(`${url}/schema`)
1375
-
1376
- // The external schema should not be bundled into x-ext
1377
- expect(input['x-ext']).toBeUndefined()
1378
-
1379
- // The local schema with $id should remain unchanged
1380
- expect(input.components.schemas.User).toEqual({
1381
- $id: `${url}/schema`,
1382
- type: 'object',
1383
- properties: {
1384
- name: { type: 'string' },
1385
- },
1386
- })
1387
- })
1388
-
1389
- it('does not modify external URLs when already defined by $anchor property', async () => {
1390
- const input = {
1391
- $id: 'https://example.com/root',
1392
- components: {
1393
- schemas: {
1394
- User: {
1395
- $anchor: 'user-schema',
1396
- type: 'object',
1397
- properties: {
1398
- name: { type: 'string' },
1399
- },
1400
- },
1401
- },
1402
- },
1403
- paths: {
1404
- '/users': {
1405
- get: {
1406
- responses: {
1407
- '200': {
1408
- description: 'Success',
1409
- content: {
1410
- 'application/json': {
1411
- schema: {
1412
- $ref: 'https://example.com/root#user-schema',
1413
- },
1414
- },
1415
- },
1416
- },
1417
- },
1418
- },
1419
- },
1420
- },
1421
- }
1422
-
1423
- await bundle(input, {
1424
- plugins: [
1425
- fetchUrls({
1426
- fetch: () => Promise.resolve(Response.json({ message: 'should not be called' })),
1427
- }),
1428
- readFiles(),
1429
- ],
1430
- treeShake: false,
1431
- })
1432
-
1433
- // The $ref should remain unchanged because the schema is already defined locally with $anchor
1434
- expect(input.paths['/users'].get.responses['200'].content['application/json'].schema.$ref).toBe(
1435
- 'https://example.com/root#user-schema',
1436
- )
1437
-
1438
- // The external schema should not be bundled into x-ext
1439
- expect(input['x-ext']).toBeUndefined()
1440
-
1441
- // The local schema with $anchor should remain unchanged
1442
- expect(input.components.schemas.User).toEqual({
1443
- $anchor: 'user-schema',
1444
- type: 'object',
1445
- properties: {
1446
- name: { type: 'string' },
1447
- },
1448
- })
1449
- })
1450
-
1451
- it('does not modify external URLs when prefix is already defined by $id', async () => {
1452
- const url = `http://localhost:${port}`
1453
-
1454
- const input = {
1455
- $id: `${url}/schema`,
1456
- components: {
1457
- schemas: {
1458
- User: {
1459
- type: 'object',
1460
- properties: {
1461
- name: { type: 'string' },
1462
- },
1463
- },
1464
- },
1465
- },
1466
- paths: {
1467
- '/users': {
1468
- get: {
1469
- responses: {
1470
- '200': {
1471
- description: 'Success',
1472
- content: {
1473
- 'application/json': {
1474
- schema: {
1475
- $ref: `${url}/schema#/components/schemas/User`,
1476
- },
1477
- },
1478
- },
1479
- },
1480
- },
1481
- },
1482
- },
1483
- },
1484
- }
1485
-
1486
- await bundle(input, {
1487
- plugins: [fetchUrls(), readFiles()],
1488
- treeShake: false,
1489
- })
1490
-
1491
- // The $ref should remain unchanged because the prefix is already defined locally with $id
1492
- expect(input.paths['/users'].get.responses['200'].content['application/json'].schema.$ref).toBe(
1493
- `${url}/schema#/components/schemas/User`,
1494
- )
1495
-
1496
- // The external schema should not be bundled into x-ext
1497
- expect(input['x-ext']).toBeUndefined()
1498
-
1499
- // The local schema should remain unchanged
1500
- expect(input.components.schemas.User).toEqual({
1501
- type: 'object',
1502
- properties: {
1503
- name: { type: 'string' },
1504
- },
1505
- })
1506
- })
1507
-
1508
- it('prioritizes $id when resolving refs', async () => {
1509
- const input = {
1510
- $id: 'https://example.com/root',
1511
- a: {
1512
- b: {
1513
- c: {
1514
- $ref: '/b',
1515
- },
1516
- },
1517
- },
1518
- }
1519
-
1520
- const exec = vi.fn<LoaderPlugin['exec']>().mockResolvedValue({
1521
- ok: true,
1522
- data: {
1523
- message: 'resolved value',
1524
- },
1525
- raw: JSON.stringify({
1526
- message: 'resolved value',
1527
- }),
1528
- })
1529
-
1530
- await bundle(input, {
1531
- treeShake: false,
1532
- plugins: [
1533
- {
1534
- type: 'loader',
1535
- validate: () => true,
1536
- exec,
1537
- },
1538
- ],
1539
- })
1540
-
1541
- expect(input).toEqual({
1542
- '$id': 'https://example.com/root',
1543
- 'a': {
1544
- 'b': {
1545
- 'c': {
1546
- '$ref': '#/x-ext/1c26e20',
1547
- },
1548
- },
1549
- },
1550
- 'x-ext': {
1551
- '1c26e20': {
1552
- 'message': 'resolved value',
1553
- },
1554
- },
1555
- })
1556
-
1557
- expect(exec).toHaveBeenCalled()
1558
- expect(exec).toHaveBeenCalledWith('https://example.com/b')
1559
- })
1560
-
1561
- it('prioritizes $id when resolving refs with origin #1', async () => {
1562
- const url = `http://localhost:${port}`
1563
-
1564
- const input = {
1565
- $id: '/root',
1566
- a: {
1567
- b: {
1568
- c: {
1569
- $ref: '/b',
1570
- },
1571
- },
1572
- },
1573
- }
1574
-
1575
- const exec = vi.fn<LoaderPlugin['exec']>().mockResolvedValue({
1576
- ok: true,
1577
- data: {
1578
- message: 'resolved value',
1579
- },
1580
- raw: JSON.stringify({
1581
- message: 'resolved value',
1582
- }),
1583
- })
1584
-
1585
- await bundle(input, {
1586
- treeShake: false,
1587
- origin: url,
1588
- plugins: [
1589
- {
1590
- type: 'loader',
1591
- validate: () => true,
1592
- exec,
1593
- },
1594
- ],
1595
- })
1596
-
1597
- expect(input).toEqual({
1598
- '$id': '/root',
1599
- 'a': {
1600
- 'b': {
1601
- 'c': {
1602
- '$ref': '#/x-ext/19ac1c5',
1603
- },
1604
- },
1605
- },
1606
- 'x-ext': {
1607
- '19ac1c5': {
1608
- 'message': 'resolved value',
1609
- },
1610
- },
1611
- })
1612
-
1613
- expect(exec).toHaveBeenCalled()
1614
- expect(exec).toHaveBeenCalledWith('/b')
1615
- })
1616
-
1617
- it('prioritizes $id when resolving refs with origin #2', async () => {
1618
- const url = `http://localhost:${port}`
1619
-
1620
- const input = {
1621
- $id: 'http://example.com/root',
1622
- a: {
1623
- b: {
1624
- c: {
1625
- $ref: '/b',
1626
- },
1627
- },
1628
- },
1629
- }
1630
-
1631
- const exec = vi.fn<LoaderPlugin['exec']>((value) => {
1632
- if (value === url) {
1633
- return Promise.resolve({
1634
- ok: true,
1635
- data: input,
1636
- raw: JSON.stringify(input),
1637
- })
1638
- }
1639
-
1640
- return Promise.resolve({
1641
- ok: true,
1642
- data: {
1643
- message: 'resolved value',
1644
- },
1645
- raw: JSON.stringify({
1646
- message: 'resolved value',
1647
- }),
1648
- })
1649
- })
1650
-
1651
- await bundle(url, {
1652
- treeShake: false,
1653
- origin: url,
1654
- plugins: [
1655
- {
1656
- type: 'loader',
1657
- validate: () => true,
1658
- exec,
1659
- },
1660
- ],
1661
- })
1662
-
1663
- expect(input).toEqual({
1664
- '$id': 'http://example.com/root',
1665
- 'a': {
1666
- 'b': {
1667
- 'c': {
1668
- '$ref': '#/x-ext/8f7df4f',
1669
- },
1670
- },
1671
- },
1672
- 'x-ext': {
1673
- '8f7df4f': {
1674
- 'message': 'resolved value',
1675
- },
1676
- },
1677
- })
1678
-
1679
- expect(exec).toHaveBeenCalledTimes(2)
1680
- expect(exec).toHaveBeenNthCalledWith(1, url)
1681
- expect(exec).toHaveBeenNthCalledWith(2, 'http://example.com/b')
1682
- })
1683
-
1684
- it('correctly bundles when doing a partial bundle with $anchor on a different context', async () => {
1685
- const input = {
1686
- a: {
1687
- b: {
1688
- c: {
1689
- $ref: '#/e/f',
1690
- },
1691
- },
1692
- },
1693
- e: {
1694
- $id: 'https://example.com/e',
1695
- $anchor: 'my-anchor',
1696
- f: {
1697
- $ref: '#my-anchor',
1698
- },
1699
- g: {
1700
- $ref: 'http://example.com',
1701
- },
1702
- },
1703
- }
1704
-
1705
- await bundle(input.a, {
1706
- treeShake: false,
1707
- plugins: [
1708
- {
1709
- type: 'loader',
1710
- validate: () => true,
1711
- exec: (value) => {
1712
- if (value === 'http://example.com') {
1713
- return Promise.resolve({
1714
- ok: true,
1715
- data: {
1716
- message: 'resolved value',
1717
- },
1718
- raw: JSON.stringify({
1719
- message: 'resolved value',
1720
- }),
1721
- })
1722
- }
1723
- return Promise.resolve({ ok: false })
1724
- },
1725
- },
1726
- ],
1727
- root: input,
1728
- urlMap: true,
1729
- cache: new Map(),
1730
- })
1731
-
1732
- expect(input).toEqual({
1733
- 'a': {
1734
- 'b': {
1735
- 'c': {
1736
- '$ref': '#/e/f',
1737
- },
1738
- },
1739
- },
1740
- 'e': {
1741
- '$anchor': 'my-anchor',
1742
- '$id': 'https://example.com/e',
1743
- 'f': {
1744
- '$ref': '#my-anchor',
1745
- },
1746
- 'g': {
1747
- '$ref': '#/x-ext/0a9e8c9',
1748
- },
1749
- },
1750
- 'x-ext': {
1751
- '0a9e8c9': {
1752
- 'message': 'resolved value',
1753
- },
1754
- },
1755
- 'x-ext-urls': {
1756
- '0a9e8c9': 'http://example.com',
1757
- },
1758
- })
1759
- })
1760
-
1761
- it('treats internal root pointers as internal references', async () => {
1762
- resetConsoleSpies()
1763
-
1764
- const input = {
1765
- a: {
1766
- $ref: '#/',
1767
- },
1768
- }
1769
-
1770
- await bundle(input, { plugins: [fetchUrls()], treeShake: false })
1771
-
1772
- expect(input).toEqual({
1773
- a: {
1774
- $ref: '#/',
1775
- },
1776
- })
1777
-
1778
- expect(consoleWarnSpy).toHaveBeenCalledTimes(0)
1779
- })
1780
- })
1781
-
1782
- describe('local files', () => {
1783
- it('resolves from local files', async () => {
1784
- const chunk1 = { a: 'a', b: 'b' }
1785
- const chunk1Path = randomUUID()
1786
-
1787
- await fs.writeFile(chunk1Path, JSON.stringify(chunk1))
1788
-
1789
- const input = {
1790
- a: {
1791
- '$ref': `./${chunk1Path}#/a`,
1792
- },
1793
- }
1794
-
1795
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1796
-
1797
- await fs.rm(chunk1Path)
1798
-
1799
- expect(input).toEqual({
1800
- 'x-ext': {
1801
- [await getHash(chunk1Path)]: {
1802
- ...chunk1,
1803
- },
1804
- },
1805
- a: {
1806
- $ref: `#/x-ext/${await getHash(chunk1Path)}/a`,
1807
- },
1808
- })
1809
- })
1810
-
1811
- it('resolves external refs from resolved files', async () => {
1812
- const chunk1 = { a: 'a', b: 'b' }
1813
- const chunk1Path = randomUUID()
1814
-
1815
- const chunk2 = { a: { '$ref': `./${chunk1Path}#` } }
1816
- const chunk2Path = randomUUID()
1817
-
1818
- await fs.writeFile(chunk1Path, JSON.stringify(chunk1))
1819
- await fs.writeFile(chunk2Path, JSON.stringify(chunk2))
1820
-
1821
- const input = {
1822
- a: {
1823
- '$ref': `./${chunk2Path}#`,
1824
- },
1825
- }
1826
-
1827
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1828
-
1829
- await fs.rm(chunk1Path)
1830
- await fs.rm(chunk2Path)
1831
-
1832
- expect(input).toEqual({
1833
- 'x-ext': {
1834
- [await getHash(chunk1Path)]: {
1835
- ...chunk1,
1836
- },
1837
- [await getHash(chunk2Path)]: {
1838
- a: { $ref: `#/x-ext/${await getHash(chunk1Path)}` },
1839
- },
1840
- },
1841
- a: {
1842
- $ref: `#/x-ext/${await getHash(chunk2Path)}`,
1843
- },
1844
- })
1845
- })
1846
-
1847
- it('resolves nested refs correctly', async () => {
1848
- const c = {
1849
- c: 'c',
1850
- }
1851
- const cName = randomUUID()
1852
-
1853
- const b = {
1854
- b: {
1855
- '$ref': `./${cName}`,
1856
- },
1857
- }
1858
- const bName = randomUUID()
1859
-
1860
- await fs.mkdir('./nested').catch(() => {
1861
- return
1862
- })
1863
- await fs.writeFile(`./nested/${bName}`, JSON.stringify(b))
1864
- await fs.writeFile(`./nested/${cName}`, JSON.stringify(c))
1865
-
1866
- const input = {
1867
- a: {
1868
- '$ref': `./nested/${bName}`,
1869
- },
1870
- }
1871
-
1872
- await bundle(input, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1873
-
1874
- await fs.rm(`./nested/${bName}`)
1875
- await fs.rm(`./nested/${cName}`)
1876
- await fs.rmdir('nested')
1877
-
1878
- expect(input).toEqual({
1879
- 'x-ext': {
1880
- [await getHash(`nested/${cName}`)]: {
1881
- c: 'c',
1882
- },
1883
- [await getHash(`nested/${bName}`)]: {
1884
- b: { $ref: `#/x-ext/${await getHash(`nested/${cName}`)}` },
1885
- },
1886
- },
1887
- a: {
1888
- $ref: `#/x-ext/${await getHash(`nested/${bName}`)}`,
1889
- },
1890
- })
1891
- })
1892
-
1893
- it('bundles from a file input', async () => {
1894
- const c = {
1895
- c: 'c',
1896
- }
1897
- const cName = randomUUID()
1898
-
1899
- const b = {
1900
- b: {
1901
- '$ref': `./${cName}`,
1902
- },
1903
- }
1904
- const bName = randomUUID()
1905
-
1906
- await fs.mkdir('./nested').catch(() => {
1907
- return
1908
- })
1909
- await fs.writeFile(`./nested/${bName}`, JSON.stringify(b))
1910
- await fs.writeFile(`./nested/${cName}`, JSON.stringify(c))
1911
-
1912
- const input = {
1913
- a: {
1914
- '$ref': `./${bName}`,
1915
- },
1916
- }
1917
- const inputName = randomUUID()
1918
- await fs.writeFile(`./nested/${inputName}`, JSON.stringify(input))
1919
-
1920
- const result = await bundle(`./nested/${bName}`, { plugins: [fetchUrls(), readFiles()], treeShake: false })
1921
-
1922
- await fs.rm(`./nested/${bName}`)
1923
- await fs.rm(`./nested/${cName}`)
1924
- await fs.rm(`./nested/${inputName}`)
1925
- await fs.rmdir('nested')
1926
-
1927
- expect(result).toEqual({
1928
- 'b': {
1929
- '$ref': `#/x-ext/${await getHash(`nested/${cName}`)}`,
1930
- },
1931
- 'x-ext': {
1932
- [await getHash(`nested/${cName}`)]: {
1933
- 'c': 'c',
1934
- },
1935
- },
1936
- })
1937
- })
1938
- })
1939
-
1940
- describe('json inputs', () => {
1941
- it('should process json inputs', async () => {
1942
- const result = await bundle('{ "openapi": "3.1", "info": { "title": "Simple API", "version": "1.0" } }', {
1943
- treeShake: false,
1944
- plugins: [parseJson()],
1945
- })
1946
-
1947
- expect(result).toEqual({
1948
- openapi: '3.1',
1949
- info: {
1950
- title: 'Simple API',
1951
- version: '1.0',
1952
- },
1953
- })
1954
- })
1955
-
1956
- it('should correctly resolve refs for json inputs', async () => {
1957
- const chunk1 = { a: 'a', b: 'b' }
1958
- const chunk1Path = randomUUID()
1959
-
1960
- await fs.writeFile(chunk1Path, JSON.stringify(chunk1))
1961
-
1962
- const input = JSON.stringify({
1963
- a: {
1964
- '$ref': `./${chunk1Path}#/a`,
1965
- },
1966
- })
1967
-
1968
- const result = await bundle(input, { plugins: [readFiles(), parseJson()], treeShake: false })
1969
-
1970
- await fs.rm(chunk1Path)
1971
-
1972
- expect(result).toEqual({
1973
- 'x-ext': {
1974
- [await getHash(chunk1Path)]: {
1975
- ...chunk1,
1976
- },
1977
- },
1978
- a: {
1979
- $ref: `#/x-ext/${await getHash(chunk1Path)}/a`,
1980
- },
1981
- })
1982
- })
1983
- })
1984
-
1985
- describe('yaml inputs', () => {
1986
- let server: FastifyInstance
1987
- const port = 7229
1988
- const url = `http://localhost:${port}`
1989
-
1990
- beforeEach(() => {
1991
- server = fastify({ logger: false })
1992
-
1993
- return async () => {
1994
- await server.close()
1995
- await setTimeout(100)
1996
- }
1997
- })
1998
-
1999
- it('should process yaml inputs', async () => {
2000
- const result = await bundle('openapi: "3.1"\ninfo:\n title: Simple API\n version: "1.0"\n', {
2001
- treeShake: false,
2002
- plugins: [parseYaml()],
2003
- })
2004
-
2005
- expect(result).toEqual({
2006
- openapi: '3.1',
2007
- info: {
2008
- title: 'Simple API',
2009
- version: '1.0',
2010
- },
2011
- })
2012
- })
2013
-
2014
- it('should correctly resolve refs for yaml inputs', async () => {
2015
- const chunk1 = { a: 'a', b: 'b' }
2016
- const chunk1Path = randomUUID()
2017
-
2018
- await fs.writeFile(chunk1Path, YAML.stringify(chunk1))
2019
-
2020
- const input = YAML.stringify({
2021
- a: {
2022
- '$ref': `./${chunk1Path}#/a`,
2023
- },
2024
- })
2025
-
2026
- const result = await bundle(input, { plugins: [parseYaml(), readFiles()], treeShake: false })
2027
-
2028
- await fs.rm(chunk1Path)
2029
-
2030
- expect(result).toEqual({
2031
- 'x-ext': {
2032
- [await getHash(chunk1Path)]: {
2033
- ...chunk1,
2034
- },
2035
- },
2036
- a: {
2037
- $ref: `#/x-ext/${await getHash(chunk1Path)}/a`,
2038
- },
2039
- })
2040
- })
2041
-
2042
- it('should correctly load the document from an url even when yaml plugin is provided and it has high priority on the list', async () => {
2043
- server.get('/', () => ({
2044
- openapi: '3.1.1',
2045
- info: {
2046
- title: 'My API',
2047
- },
2048
- }))
2049
- await server.listen({ port })
2050
- const result = await bundle(url, {
2051
- treeShake: false,
2052
- plugins: [parseYaml(), fetchUrls()],
2053
- })
2054
-
2055
- expect(result).toEqual({
2056
- 'info': {
2057
- 'title': 'My API',
2058
- },
2059
- 'openapi': '3.1.1',
2060
- })
2061
- })
2062
- })
2063
-
2064
- describe('bundle with a certain depth', () => {
2065
- let server: FastifyInstance
2066
- const PORT = 7298
2067
- const url = `http://localhost:${PORT}`
2068
-
2069
- beforeEach(() => {
2070
- server = fastify({ logger: false })
2071
-
2072
- return async () => {
2073
- await server.close()
2074
- await setTimeout(100)
2075
- }
2076
- })
2077
-
2078
- it('bundles external urls', async () => {
2079
- const external = {
2080
- prop: 'I am external json prop',
2081
- }
2082
- server.get('/', (_, reply) => {
2083
- reply.send(external)
2084
- })
2085
-
2086
- await server.listen({ port: PORT })
2087
-
2088
- const input = {
2089
- a: {
2090
- b: {
2091
- c: {
2092
- d: {
2093
- e: {
2094
- // Deep ref
2095
- '$ref': `http://localhost:${PORT}#/prop`,
2096
- },
2097
- },
2098
- },
2099
- },
2100
- },
2101
- d: {
2102
- e: {
2103
- '$ref': `http://localhost:${PORT}#/prop`,
2104
- },
2105
- },
2106
- }
2107
-
2108
- await bundle(input, {
2109
- plugins: [fetchUrls()],
2110
- treeShake: false,
2111
- depth: 2,
2112
- })
2113
-
2114
- expect(input).toEqual({
2115
- 'x-ext': {
2116
- [await getHash(url)]: {
2117
- ...external,
2118
- },
2119
- },
2120
- 'x-ext-urls': {
2121
- [await getHash(url)]: url,
2122
- },
2123
- a: {
2124
- b: {
2125
- c: {
2126
- d: {
2127
- e: {
2128
- $ref: `${url}#/prop`,
2129
- },
2130
- },
2131
- },
2132
- },
2133
- },
2134
- d: {
2135
- e: {
2136
- $ref: `#/x-ext/${await getHash(url)}/prop`,
2137
- },
2138
- },
2139
- })
2140
- })
2141
-
2142
- it('will not do full bundle if we do specify a depth and reuse the same hash set', async () => {
2143
- const external = {
2144
- prop: 'I am external json prop',
2145
- }
2146
- server.get('/', (_, reply) => {
2147
- reply.send(external)
2148
- })
2149
-
2150
- await server.listen({ port: PORT })
2151
-
2152
- const input = {
2153
- a: {
2154
- b: {
2155
- c: {
2156
- d: {
2157
- e: {
2158
- // Deep ref
2159
- '$ref': `http://localhost:${PORT}#/prop`,
2160
- },
2161
- },
2162
- },
2163
- },
2164
- },
2165
- d: {
2166
- e: {
2167
- '$ref': `http://localhost:${PORT}#/prop`,
2168
- },
2169
- },
2170
- }
2171
-
2172
- const visitedNodes = new Set()
2173
-
2174
- await bundle(input, {
2175
- plugins: [fetchUrls()],
2176
- treeShake: false,
2177
- depth: 2,
2178
- visitedNodes: visitedNodes,
2179
- })
2180
-
2181
- expect(input).toEqual({
2182
- 'x-ext': {
2183
- [await getHash(url)]: {
2184
- ...external,
2185
- },
2186
- },
2187
- 'x-ext-urls': {
2188
- [await getHash(url)]: url,
2189
- },
2190
- a: {
2191
- b: {
2192
- c: {
2193
- d: {
2194
- e: {
2195
- $ref: `${url}#/prop`,
2196
- },
2197
- },
2198
- },
2199
- },
2200
- },
2201
- d: {
2202
- e: {
2203
- $ref: `#/x-ext/${await getHash(url)}/prop`,
2204
- },
2205
- },
2206
- })
2207
-
2208
- // We run a full bundle on the root of the document without a depth
2209
- await bundle(input, {
2210
- plugins: [fetchUrls()],
2211
- treeShake: false,
2212
- visitedNodes: visitedNodes,
2213
- urlMap: true,
2214
- })
2215
-
2216
- // Expect the input to be the same as before
2217
- // because we are reusing the same hash set
2218
- expect(input).toEqual({
2219
- 'x-ext': {
2220
- [await getHash(url)]: {
2221
- ...external,
2222
- },
2223
- },
2224
- 'x-ext-urls': {
2225
- [await getHash(url)]: url,
2226
- },
2227
- a: {
2228
- b: {
2229
- c: {
2230
- d: {
2231
- e: {
2232
- $ref: `${url}#/prop`,
2233
- },
2234
- },
2235
- },
2236
- },
2237
- },
2238
- d: {
2239
- e: {
2240
- $ref: `#/x-ext/${await getHash(url)}/prop`,
2241
- },
2242
- },
2243
- })
2244
-
2245
- // When we run a full bundle again without the same hash set we expect a full bundle
2246
- await bundle(input, {
2247
- plugins: [fetchUrls()],
2248
- treeShake: false,
2249
- urlMap: true,
2250
- })
2251
-
2252
- expect(input).toEqual({
2253
- 'x-ext': {
2254
- [await getHash(url)]: {
2255
- ...external,
2256
- },
2257
- },
2258
- 'x-ext-urls': {
2259
- [await getHash(url)]: url,
2260
- },
2261
- a: {
2262
- b: {
2263
- c: {
2264
- d: {
2265
- e: {
2266
- $ref: `#/x-ext/${await getHash(url)}/prop`,
2267
- },
2268
- },
2269
- },
2270
- },
2271
- },
2272
- d: {
2273
- e: {
2274
- $ref: `#/x-ext/${await getHash(url)}/prop`,
2275
- },
2276
- },
2277
- })
2278
- })
2279
- })
2280
-
2281
- describe('hooks', () => {
2282
- describe('onBeforeNodeProcess', () => {
2283
- it('should correctly call the `onBeforeNodeProcess` correctly on all the nodes', async () => {
2284
- const onBeforeNodeProcess = vi.fn()
2285
-
2286
- const input = {
2287
- someKey: 'someValue',
2288
- anotherKey: {
2289
- innerKey: 'nestedValue',
2290
- },
2291
- }
2292
-
2293
- await bundle(input, {
2294
- plugins: [],
2295
- treeShake: false,
2296
- hooks: {
2297
- onBeforeNodeProcess,
2298
- },
2299
- })
2300
-
2301
- expect(onBeforeNodeProcess).toBeCalledTimes(2)
2302
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(1, input, expect.any(Object))
2303
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(2, input.anotherKey, expect.any(Object))
2304
- })
2305
-
2306
- it('should run bundle on the mutated object properties', async () => {
2307
- const onBeforeNodeProcessSpy = vi.fn()
2308
-
2309
- const input = {
2310
- a: {
2311
- b: {
2312
- c: 'c',
2313
- d: 'd',
2314
- },
2315
- e: 'e',
2316
- },
2317
- }
2318
-
2319
- await bundle(input, {
2320
- plugins: [],
2321
- treeShake: false,
2322
- hooks: {
2323
- onBeforeNodeProcess(node) {
2324
- if ('e' in node) {
2325
- node['processedKey'] = { 'message': 'Processed node' }
2326
- }
2327
- onBeforeNodeProcessSpy(node)
2328
- },
2329
- },
2330
- })
2331
-
2332
- expect(onBeforeNodeProcessSpy).toBeCalledTimes(4)
2333
- expect(onBeforeNodeProcessSpy).toHaveBeenNthCalledWith(4, { 'message': 'Processed node' })
2334
- })
2335
- })
2336
-
2337
- describe('onAfterNodeProcess', () => {
2338
- it('should call `onAfterNodeProcess` hook on the nodes', async () => {
2339
- const onAfterNodeProcessSpy = vi.fn()
2340
-
2341
- const input = {
2342
- a: {
2343
- b: {
2344
- c: 'c',
2345
- d: 'd',
2346
- },
2347
- e: 'e',
2348
- },
2349
- }
2350
-
2351
- await bundle(input, {
2352
- plugins: [],
2353
- treeShake: false,
2354
- hooks: {
2355
- onAfterNodeProcess(node) {
2356
- if ('e' in node) {
2357
- node['processedKey'] = { 'message': 'Processed node' }
2358
- }
2359
- onAfterNodeProcessSpy(node)
2360
- },
2361
- },
2362
- })
2363
-
2364
- expect(onAfterNodeProcessSpy).toHaveBeenCalledTimes(3)
2365
- })
2366
- })
2367
- })
2368
-
2369
- describe('plugins', () => {
2370
- it('use load plugins to load the documents', async () => {
2371
- const validate = vi.fn<LoaderPlugin['validate']>().mockReturnValue(true)
2372
- const exec = vi.fn<LoaderPlugin['exec']>().mockResolvedValue({
2373
- ok: true,
2374
- data: { message: 'Resolved document' },
2375
- raw: JSON.stringify({ message: 'Resolved document' }),
2376
- })
2377
-
2378
- const resolver = (): LoaderPlugin => {
2379
- return {
2380
- type: 'loader',
2381
- validate,
2382
- exec,
2383
- }
2384
- }
2385
-
2386
- const result = await bundle('hello', { treeShake: false, plugins: [resolver()] })
2387
-
2388
- expect(validate).toHaveBeenCalledOnce()
2389
- expect(validate).toHaveBeenLastCalledWith('hello')
2390
-
2391
- expect(exec).toHaveBeenCalledOnce()
2392
- expect(exec).toHaveBeenLastCalledWith('hello')
2393
-
2394
- expect(result).toEqual({ message: 'Resolved document' })
2395
- })
2396
-
2397
- it('throws if we can not process the input with any of the provided loaders', async () => {
2398
- const validate = vi.fn<LoaderPlugin['validate']>().mockReturnValue(false)
2399
- const exec = vi.fn<LoaderPlugin['exec']>()
2400
-
2401
- const resolver = (): LoaderPlugin => {
2402
- return {
2403
- type: 'loader',
2404
- validate,
2405
- exec,
2406
- }
2407
- }
2408
-
2409
- await expect(bundle('hello', { treeShake: false, plugins: [resolver()] })).rejects.toThrow()
2410
-
2411
- expect(validate).toHaveBeenCalledOnce()
2412
- expect(validate).toHaveBeenLastCalledWith('hello')
2413
-
2414
- expect(exec).not.toHaveBeenCalled()
2415
- })
2416
-
2417
- it('use load plugin to resolve external refs', async () => {
2418
- const validate = vi.fn<LoaderPlugin['validate']>().mockReturnValue(true)
2419
- const exec = vi.fn<LoaderPlugin['exec']>().mockResolvedValue({
2420
- ok: true,
2421
- data: { message: 'Resolved document' },
2422
- raw: JSON.stringify({ message: 'Resolved document' }),
2423
- })
2424
-
2425
- const resolver = (): LoaderPlugin => {
2426
- return {
2427
- type: 'loader',
2428
- validate,
2429
- exec,
2430
- }
2431
- }
2432
-
2433
- const result = await bundle({ $ref: 'hello' }, { treeShake: false, plugins: [resolver()] })
2434
-
2435
- expect(validate).toHaveBeenCalledOnce()
2436
- expect(validate).toHaveBeenLastCalledWith('hello')
2437
-
2438
- expect(exec).toHaveBeenCalledOnce()
2439
- expect(exec).toHaveBeenLastCalledWith('hello')
2440
-
2441
- expect(result).toEqual({
2442
- $ref: '#/x-ext/26c7827',
2443
- 'x-ext': {
2444
- '26c7827': {
2445
- message: 'Resolved document',
2446
- },
2447
- },
2448
- })
2449
- })
2450
-
2451
- it('emits warning when there is no loader to resolve the external ref', async () => {
2452
- resetConsoleSpies()
2453
- const validate = vi.fn<LoaderPlugin['validate']>().mockReturnValue(false)
2454
- const exec = vi.fn<LoaderPlugin['exec']>()
2455
-
2456
- const resolver = (): LoaderPlugin => {
2457
- return {
2458
- type: 'loader',
2459
- validate,
2460
- exec,
2461
- }
2462
- }
2463
-
2464
- const result = await bundle({ $ref: 'hello' }, { treeShake: false, plugins: [resolver()] })
2465
-
2466
- expect(validate).toHaveBeenCalledOnce()
2467
- expect(validate).toHaveBeenLastCalledWith('hello')
2468
-
2469
- expect(exec).not.toHaveBeenCalled()
2470
-
2471
- expect(result).toEqual({ $ref: 'hello' })
2472
-
2473
- expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
2474
- expect(consoleWarnSpy).toHaveBeenCalledWith(
2475
- 'Failed to resolve external reference "hello". The reference may be invalid, inaccessible, or missing a loader for this type of reference.',
2476
- )
2477
- })
2478
-
2479
- it('lets plugins hook into nodes lifecycle #1', async () => {
2480
- const onBeforeNodeProcess = vi.fn()
2481
- const onAfterNodeProcess = vi.fn()
2482
-
2483
- await bundle(
2484
- {
2485
- prop: {
2486
- innerProp: 'string',
2487
- },
2488
- },
2489
- {
2490
- treeShake: false,
2491
- plugins: [
2492
- {
2493
- type: 'lifecycle',
2494
- onBeforeNodeProcess,
2495
- onAfterNodeProcess,
2496
- },
2497
- ],
2498
- },
2499
- )
2500
-
2501
- expect(onBeforeNodeProcess).toHaveBeenCalledTimes(2)
2502
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2503
- 1,
2504
- {
2505
- prop: {
2506
- innerProp: 'string',
2507
- },
2508
- },
2509
- {
2510
- path: [],
2511
- resolutionCache: new Map(),
2512
- parentNode: null,
2513
- rootNode: {
2514
- prop: {
2515
- innerProp: 'string',
2516
- },
2517
- },
2518
- loaders: [],
2519
- },
2520
- )
2521
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2522
- 2,
2523
- {
2524
- innerProp: 'string',
2525
- },
2526
- {
2527
- path: ['prop'],
2528
- resolutionCache: new Map(),
2529
- parentNode: {
2530
- prop: {
2531
- innerProp: 'string',
2532
- },
2533
- },
2534
- rootNode: {
2535
- prop: {
2536
- innerProp: 'string',
2537
- },
2538
- },
2539
- loaders: [],
2540
- },
2541
- )
2542
-
2543
- expect(onAfterNodeProcess).toHaveBeenCalledTimes(2)
2544
- expect(onAfterNodeProcess).toHaveBeenNthCalledWith(
2545
- 1,
2546
- {
2547
- innerProp: 'string',
2548
- },
2549
- {
2550
- path: ['prop'],
2551
- resolutionCache: new Map(),
2552
- parentNode: {
2553
- prop: {
2554
- innerProp: 'string',
2555
- },
2556
- },
2557
- rootNode: {
2558
- prop: {
2559
- innerProp: 'string',
2560
- },
2561
- },
2562
- loaders: [],
2563
- },
2564
- )
2565
- expect(onAfterNodeProcess).toHaveBeenNthCalledWith(
2566
- 2,
2567
- {
2568
- prop: {
2569
- innerProp: 'string',
2570
- },
2571
- },
2572
- {
2573
- path: [],
2574
- resolutionCache: new Map(),
2575
- parentNode: null,
2576
- rootNode: {
2577
- prop: {
2578
- innerProp: 'string',
2579
- },
2580
- },
2581
- loaders: [],
2582
- },
2583
- )
2584
- })
2585
-
2586
- it('lets plugins hook into nodes lifecycle #2', async () => {
2587
- const validate = vi.fn<LoaderPlugin['validate']>((value) => {
2588
- if (value === 'resolve') {
2589
- return true
2590
- }
2591
- return false
2592
- })
2593
- const exec = vi.fn<LoaderPlugin['exec']>((value) =>
2594
- Promise.resolve({
2595
- ok: true,
2596
- data: {
2597
- message: 'Resolved value',
2598
- 'x-original-value': value,
2599
- },
2600
- raw: JSON.stringify({
2601
- message: 'Resolved value',
2602
- 'x-original-value': value,
2603
- }),
2604
- }),
2605
- )
2606
- const onResolveStart = vi.fn()
2607
- const onResolveError = vi.fn()
2608
- const onResolveSuccess = vi.fn()
2609
- await bundle(
2610
- {
2611
- hello: {
2612
- $ref: 'some-value',
2613
- },
2614
- hi: {
2615
- $ref: 'resolve',
2616
- },
2617
- },
2618
- {
2619
- treeShake: false,
2620
- plugins: [
2621
- {
2622
- type: 'loader',
2623
- validate,
2624
- exec,
2625
- },
2626
- {
2627
- type: 'lifecycle',
2628
- onResolveStart,
2629
- onResolveError,
2630
- onResolveSuccess,
2631
- },
2632
- ],
2633
- },
2634
- )
2635
-
2636
- expect(validate).toHaveBeenCalledTimes(2)
2637
- expect(exec).toHaveBeenCalledOnce()
2638
-
2639
- expect(onResolveStart).toHaveBeenCalledTimes(2)
2640
- expect(onResolveStart).nthCalledWith(1, { $ref: 'some-value' })
2641
- expect(onResolveStart).nthCalledWith(2, { $ref: '#/x-ext/2a57507' })
2642
-
2643
- expect(onResolveError).toHaveBeenCalledTimes(1)
2644
- expect(onResolveError).lastCalledWith({ $ref: 'some-value' })
2645
-
2646
- expect(onResolveSuccess).toHaveBeenCalledTimes(1)
2647
- expect(onResolveSuccess).lastCalledWith({ $ref: '#/x-ext/2a57507' })
2648
- })
2649
-
2650
- it('correctly provides the parent node in different levels', async () => {
2651
- const onBeforeNodeProcess = vi.fn()
2652
- const input = {
2653
- a: {
2654
- b: {
2655
- c: {
2656
- someNode: 'hello world',
2657
- },
2658
- },
2659
- },
2660
- d: {
2661
- $ref: '#/a',
2662
- },
2663
- e: {
2664
- f: {
2665
- $ref: '#/a/b/c',
2666
- },
2667
- },
2668
- }
2669
-
2670
- await bundle(input, {
2671
- plugins: [
2672
- {
2673
- type: 'lifecycle',
2674
- onBeforeNodeProcess,
2675
- },
2676
- ],
2677
- treeShake: false,
2678
- })
2679
-
2680
- expect(onBeforeNodeProcess).toHaveBeenCalledTimes(7)
2681
-
2682
- // First call should be the root with a null parent
2683
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2684
- 1,
2685
- input,
2686
- expect.objectContaining({
2687
- parentNode: null,
2688
- }),
2689
- )
2690
-
2691
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2692
- 2,
2693
- input.a,
2694
- expect.objectContaining({
2695
- parentNode: input,
2696
- }),
2697
- )
2698
-
2699
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2700
- 3,
2701
- input.d,
2702
- expect.objectContaining({
2703
- parentNode: input,
2704
- }),
2705
- )
2706
-
2707
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2708
- 4,
2709
- input.e,
2710
- expect.objectContaining({
2711
- parentNode: input,
2712
- }),
2713
- )
2714
-
2715
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2716
- 5,
2717
- input.a.b,
2718
- expect.objectContaining({ parentNode: input.a }),
2719
- )
2720
-
2721
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2722
- 6,
2723
- input.e.f,
2724
- expect.objectContaining({ parentNode: input.e }),
2725
- )
2726
-
2727
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2728
- 7,
2729
- input.a.b.c,
2730
- expect.objectContaining({ parentNode: input.a.b }),
2731
- )
2732
- })
2733
-
2734
- it('correctly provides the parent node on partial bundle for referenced nodes', async () => {
2735
- const onBeforeNodeProcess = vi.fn()
2736
-
2737
- const input = {
2738
- a: {
2739
- b: 'some-prop',
2740
- },
2741
- b: {
2742
- c: {
2743
- $ref: '#/a',
2744
- },
2745
- },
2746
- }
2747
-
2748
- await bundle(input.b, {
2749
- treeShake: false,
2750
- root: input,
2751
- plugins: [
2752
- {
2753
- type: 'lifecycle',
2754
- onBeforeNodeProcess,
2755
- },
2756
- ],
2757
- })
2758
-
2759
- expect(onBeforeNodeProcess).toHaveBeenCalledTimes(3)
2760
-
2761
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2762
- 1,
2763
- input.b,
2764
- expect.objectContaining({
2765
- parentNode: null,
2766
- }),
2767
- )
2768
-
2769
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2770
- 2,
2771
- input.b.c,
2772
- expect.objectContaining({
2773
- parentNode: input.b,
2774
- }),
2775
- )
2776
-
2777
- expect(onBeforeNodeProcess).toHaveBeenNthCalledWith(
2778
- 3,
2779
- input.a,
2780
- expect.objectContaining({
2781
- parentNode: input,
2782
- }),
2783
- )
2784
- })
2785
- })
2786
- })
2787
-
2788
- describe('isRemoteUrl', () => {
2789
- it.each([
2790
- ['https://example.com/schema.json', true],
2791
- ['http://api.example.com/schemas/user.json', true],
2792
- ['file://some/path', false],
2793
- ['random-string', false],
2794
- ['#/components/schemas/User', false],
2795
- ['./local-schema.json', false],
2796
- ])('detects remote urls', (a, b) => {
2797
- expect(isRemoteUrl(a)).toBe(b)
2798
- })
2799
- })
2800
-
2801
- describe('isLocalRef', () => {
2802
- it.each([
2803
- ['#/components/schemas/User', true],
2804
- ['https://example.com/schema.json', false],
2805
- ['./local-schema.json', false],
2806
- ])('detects local refs', (a, b) => {
2807
- expect(isLocalRef(a)).toBe(b)
2808
- })
2809
- })
2810
-
2811
- describe('prefixInternalRef', () => {
2812
- it.each([
2813
- ['#/hello', ['prefix'], '#/prefix/hello'],
2814
- ['#/a/b/c', ['prefixA', 'prefixB'], '#/prefixA/prefixB/a/b/c'],
2815
- ])('correctly prefix the internal refs', (a, b, c) => {
2816
- expect(prefixInternalRef(a, b)).toEqual(c)
2817
- })
2818
-
2819
- it('throws when the ref is not internal', () => {
2820
- expect(() => prefixInternalRef('http://example.com#/prefix', ['a', 'b'])).toThrowError()
2821
- })
2822
- })
2823
-
2824
- describe('prefixInternalRefRecursive', () => {
2825
- it.each([
2826
- [
2827
- { a: { $ref: '#/a/b' }, b: { $ref: '#' } },
2828
- ['d', 'e', 'f'],
2829
- { a: { $ref: '#/d/e/f/a/b' }, b: { $ref: '#/d/e/f' } },
2830
- ],
2831
- [
2832
- { a: { $ref: '#/a/b' }, b: { $ref: 'http://example.com#/external' } },
2833
- ['d', 'e', 'f'],
2834
- { a: { $ref: '#/d/e/f/a/b' }, b: { $ref: 'http://example.com#/external' } },
2835
- ],
2836
- ])('recursively prefixes any internal ref with the correct values', (a, b, c) => {
2837
- prefixInternalRefRecursive(a, b)
2838
- expect(a).toEqual(c)
2839
- })
2840
- })
2841
-
2842
- describe('setValueAtPath', () => {
2843
- it.each([
2844
- [{}, '/a/b/c', { hello: 'hi' }, { a: { b: { c: { hello: 'hi' } } } }],
2845
- [{ a: { b: 'b' } }, '/a/c', { hello: 'hi' }, { a: { b: 'b', c: { hello: 'hi' } } }],
2846
- ])('correctly sets a value at the specified path by creating new objects if necessary', (a, b, c, d) => {
2847
- setValueAtPath(a, b, c)
2848
-
2849
- expect(a).toEqual(d)
2850
- })
2851
- })
2852
-
2853
- describe('resolveAndCopyReferences', () => {
2854
- const source = {
2855
- openapi: '3.1.1',
2856
- info: {
2857
- title: 'Example API',
2858
- version: '1.0.0',
2859
- },
2860
- paths: {
2861
- '/': {
2862
- get: {
2863
- responses: {
2864
- '200': {
2865
- content: {
2866
- 'application/json': {
2867
- schema: {
2868
- $ref: '#/components/schemas/User',
2869
- },
2870
- },
2871
- },
2872
- },
2873
- },
2874
- },
2875
- },
2876
- },
2877
- components: {
2878
- schemas: {
2879
- User: {
2880
- type: 'object',
2881
- properties: {
2882
- name: { type: 'string' },
2883
- },
2884
- },
2885
- Person: {
2886
- type: 'object',
2887
- properties: {
2888
- name: { type: 'string', default: 'John Doe' },
2889
- },
2890
- },
2891
- },
2892
- },
2893
- }
2894
-
2895
- it('correctly resolves and copies local references, and leaves out the rest', () => {
2896
- const target = {}
2897
-
2898
- resolveAndCopyReferences(target, source, '/paths/~1', '', '', true)
2899
-
2900
- expect(target).toEqual({
2901
- paths: {
2902
- '/': {
2903
- get: {
2904
- responses: {
2905
- '200': {
2906
- content: {
2907
- 'application/json': { schema: { '$ref': '#/components/schemas/User' } },
2908
- },
2909
- },
2910
- },
2911
- },
2912
- },
2913
- },
2914
- components: {
2915
- schemas: {
2916
- User: { type: 'object', properties: { name: { type: 'string' } } },
2917
- },
2918
- },
2919
- })
2920
- })
2921
- })