@scalar/json-magic 0.8.2 → 0.8.4

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