@scalar/json-magic 0.1.0 → 0.3.1

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 (57) hide show
  1. package/.turbo/turbo-build.log +4 -3
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +21 -3
  4. package/dist/bundle/bundle.d.ts +84 -14
  5. package/dist/bundle/bundle.d.ts.map +1 -1
  6. package/dist/bundle/bundle.js +60 -15
  7. package/dist/bundle/bundle.js.map +3 -3
  8. package/dist/bundle/index.d.ts +2 -1
  9. package/dist/bundle/index.d.ts.map +1 -1
  10. package/dist/bundle/index.js.map +2 -2
  11. package/dist/bundle/plugins/fetch-urls/index.d.ts +2 -2
  12. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
  13. package/dist/bundle/plugins/fetch-urls/index.js +1 -0
  14. package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
  15. package/dist/bundle/plugins/parse-json/index.d.ts +2 -2
  16. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
  17. package/dist/bundle/plugins/parse-json/index.js +1 -0
  18. package/dist/bundle/plugins/parse-json/index.js.map +2 -2
  19. package/dist/bundle/plugins/parse-yaml/index.d.ts +2 -2
  20. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
  21. package/dist/bundle/plugins/parse-yaml/index.js +1 -0
  22. package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
  23. package/dist/bundle/plugins/read-files/index.d.ts +2 -2
  24. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
  25. package/dist/bundle/plugins/read-files/index.js +1 -0
  26. package/dist/bundle/plugins/read-files/index.js.map +2 -2
  27. package/dist/diff/apply.d.ts +1 -1
  28. package/dist/diff/apply.d.ts.map +1 -1
  29. package/dist/diff/apply.js.map +2 -2
  30. package/dist/diff/diff.d.ts +2 -2
  31. package/dist/diff/diff.d.ts.map +1 -1
  32. package/dist/diff/diff.js.map +2 -2
  33. package/dist/diff/merge.d.ts +3 -3
  34. package/dist/diff/merge.d.ts.map +1 -1
  35. package/dist/diff/merge.js.map +2 -2
  36. package/dist/magic-proxy/proxy.d.ts +23 -42
  37. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  38. package/dist/magic-proxy/proxy.js +103 -80
  39. package/dist/magic-proxy/proxy.js.map +3 -3
  40. package/dist/utils/is-object.d.ts +1 -1
  41. package/dist/utils/is-object.d.ts.map +1 -1
  42. package/dist/utils/is-object.js.map +2 -2
  43. package/package.json +11 -10
  44. package/src/bundle/bundle.test.ts +591 -47
  45. package/src/bundle/bundle.ts +173 -32
  46. package/src/bundle/index.ts +2 -1
  47. package/src/bundle/plugins/fetch-urls/index.ts +3 -2
  48. package/src/bundle/plugins/parse-json/index.ts +3 -2
  49. package/src/bundle/plugins/parse-yaml/index.ts +3 -2
  50. package/src/bundle/plugins/read-files/index.ts +4 -2
  51. package/src/dereference/dereference.test.ts +26 -18
  52. package/src/diff/apply.ts +8 -3
  53. package/src/diff/diff.ts +3 -3
  54. package/src/diff/merge.ts +6 -6
  55. package/src/magic-proxy/proxy.test.ts +1095 -100
  56. package/src/magic-proxy/proxy.ts +150 -171
  57. package/src/utils/is-object.ts +1 -1
@@ -10,6 +10,7 @@ import {
10
10
  prefixInternalRef,
11
11
  prefixInternalRefRecursive,
12
12
  setValueAtPath,
13
+ type LoaderPlugin,
13
14
  } from './bundle'
14
15
  import { fetchUrls } from './plugins/fetch-urls'
15
16
  import { readFiles } from './plugins/read-files'
@@ -18,11 +19,13 @@ import { parseJson } from '@/bundle/plugins/parse-json'
18
19
  import { parseYaml } from '@/bundle/plugins/parse-yaml'
19
20
  import YAML from 'yaml'
20
21
  import { getHash } from '@/bundle/value-generator'
22
+ import { consoleWarnSpy, resetConsoleSpies } from '@scalar/helpers/testing/console-spies'
21
23
 
22
24
  describe('bundle', () => {
23
25
  describe('external urls', () => {
24
26
  let server: FastifyInstance
25
- const PORT = 7289
27
+ const port = 7289
28
+ const url = `http://localhost:${port}`
26
29
 
27
30
  beforeEach(() => {
28
31
  server = fastify({ logger: false })
@@ -34,7 +37,7 @@ describe('bundle', () => {
34
37
  })
35
38
 
36
39
  it('bundles external urls', async () => {
37
- const url = `http://localhost:${PORT}`
40
+ const url = `http://localhost:${port}`
38
41
 
39
42
  const external = {
40
43
  prop: 'I am external json prop',
@@ -43,7 +46,7 @@ describe('bundle', () => {
43
46
  reply.send(external)
44
47
  })
45
48
 
46
- await server.listen({ port: PORT })
49
+ await server.listen({ port: port })
47
50
 
48
51
  const input = {
49
52
  a: {
@@ -52,7 +55,7 @@ describe('bundle', () => {
52
55
  },
53
56
  },
54
57
  d: {
55
- '$ref': `http://localhost:${PORT}#/prop`,
58
+ '$ref': `http://localhost:${port}#/prop`,
56
59
  },
57
60
  }
58
61
 
@@ -79,7 +82,7 @@ describe('bundle', () => {
79
82
  })
80
83
 
81
84
  it('bundles external urls from resolved external piece', async () => {
82
- const url = `http://localhost:${PORT}`
85
+ const url = `http://localhost:${port}`
83
86
  const chunk2 = {
84
87
  hey: 'hey',
85
88
  nested: {
@@ -104,7 +107,7 @@ describe('bundle', () => {
104
107
  reply.send(chunk2)
105
108
  })
106
109
 
107
- await server.listen({ port: PORT })
110
+ await server.listen({ port: port })
108
111
 
109
112
  const input = {
110
113
  a: {
@@ -142,7 +145,7 @@ describe('bundle', () => {
142
145
  })
143
146
 
144
147
  it('should correctly handle only urls without a pointer', async () => {
145
- const url = `http://localhost:${PORT}`
148
+ const url = `http://localhost:${port}`
146
149
 
147
150
  server.get('/', (_, reply) => {
148
151
  reply.send({
@@ -150,7 +153,7 @@ describe('bundle', () => {
150
153
  })
151
154
  })
152
155
 
153
- await server.listen({ port: PORT })
156
+ await server.listen({ port: port })
154
157
 
155
158
  const input = {
156
159
  a: {
@@ -178,7 +181,7 @@ describe('bundle', () => {
178
181
 
179
182
  it('caches results for same resource', async () => {
180
183
  const fn = vi.fn()
181
- const url = `http://localhost:${PORT}`
184
+ const url = `http://localhost:${port}`
182
185
 
183
186
  server.get('/', (_, reply) => {
184
187
  fn()
@@ -188,7 +191,7 @@ describe('bundle', () => {
188
191
  })
189
192
  })
190
193
 
191
- await server.listen({ port: PORT })
194
+ await server.listen({ port: port })
192
195
 
193
196
  const input = {
194
197
  a: {
@@ -221,7 +224,7 @@ describe('bundle', () => {
221
224
  })
222
225
 
223
226
  it('handles correctly external nested refs', async () => {
224
- const url = `http://localhost:${PORT}`
227
+ const url = `http://localhost:${port}`
225
228
 
226
229
  server.get('/nested/another-file.json', (_, reply) => {
227
230
  reply.send({
@@ -237,7 +240,7 @@ describe('bundle', () => {
237
240
  })
238
241
  })
239
242
 
240
- await server.listen({ port: PORT })
243
+ await server.listen({ port: port })
241
244
 
242
245
  const input = {
243
246
  a: {
@@ -265,7 +268,7 @@ describe('bundle', () => {
265
268
  })
266
269
 
267
270
  it('does not merge paths when we use absolute urls', async () => {
268
- const url = `http://localhost:${PORT}`
271
+ const url = `http://localhost:${port}`
269
272
 
270
273
  server.get('/top-level', (_, reply) => {
271
274
  reply.send({
@@ -281,7 +284,7 @@ describe('bundle', () => {
281
284
  })
282
285
  })
283
286
 
284
- await server.listen({ port: PORT })
287
+ await server.listen({ port: port })
285
288
 
286
289
  const input = {
287
290
  a: {
@@ -309,7 +312,7 @@ describe('bundle', () => {
309
312
  })
310
313
 
311
314
  it('bundles from a url input', async () => {
312
- const url = `http://localhost:${PORT}`
315
+ const url = `http://localhost:${port}`
313
316
 
314
317
  server.get('/top-level', (_, reply) => {
315
318
  reply.send({
@@ -333,7 +336,7 @@ describe('bundle', () => {
333
336
  })
334
337
  })
335
338
 
336
- await server.listen({ port: PORT })
339
+ await server.listen({ port: port })
337
340
 
338
341
  const output = await bundle(`${url}/base/openapi.json`, { plugins: [fetchUrls()], treeShake: false })
339
342
 
@@ -355,7 +358,7 @@ describe('bundle', () => {
355
358
  })
356
359
 
357
360
  it('generated a map when we turn the urlMap on', async () => {
358
- const url = `http://localhost:${PORT}`
361
+ const url = `http://localhost:${port}`
359
362
 
360
363
  server.get('/top-level', (_, reply) => {
361
364
  reply.send({
@@ -379,7 +382,7 @@ describe('bundle', () => {
379
382
  })
380
383
  })
381
384
 
382
- await server.listen({ port: PORT })
385
+ await server.listen({ port: port })
383
386
 
384
387
  const output = await bundle(`${url}/base/openapi.json`, {
385
388
  plugins: [fetchUrls()],
@@ -409,7 +412,7 @@ describe('bundle', () => {
409
412
  })
410
413
 
411
414
  it('prefixes the refs only once', async () => {
412
- const url = `http://localhost:${PORT}`
415
+ const url = `http://localhost:${port}`
413
416
 
414
417
  const chunk2 = {
415
418
  a: 'a',
@@ -433,7 +436,7 @@ describe('bundle', () => {
433
436
  reply.send(chunk2)
434
437
  })
435
438
 
436
- await server.listen({ port: PORT })
439
+ await server.listen({ port: port })
437
440
 
438
441
  const input = {
439
442
  a: {
@@ -493,7 +496,7 @@ describe('bundle', () => {
493
496
  })
494
497
 
495
498
  it('bundles array references', async () => {
496
- const url = `http://localhost:${PORT}`
499
+ const url = `http://localhost:${port}`
497
500
 
498
501
  const chunk1 = {
499
502
  a: {
@@ -505,7 +508,7 @@ describe('bundle', () => {
505
508
  reply.send(chunk1)
506
509
  })
507
510
 
508
- await server.listen({ port: PORT })
511
+ await server.listen({ port: port })
509
512
 
510
513
  const input = {
511
514
  a: [
@@ -536,7 +539,7 @@ describe('bundle', () => {
536
539
  })
537
540
 
538
541
  it('bundles subpart of the document', async () => {
539
- const url = `http://localhost:${PORT}`
542
+ const url = `http://localhost:${port}`
540
543
 
541
544
  const chunk1 = {
542
545
  a: {
@@ -551,7 +554,7 @@ describe('bundle', () => {
551
554
  reply.send(chunk1)
552
555
  })
553
556
 
554
- await server.listen({ port: PORT })
557
+ await server.listen({ port: port })
555
558
 
556
559
  const input = {
557
560
  a: {
@@ -631,7 +634,7 @@ describe('bundle', () => {
631
634
  })
632
635
 
633
636
  it('always emits the url mappings when doing partial bundle', async () => {
634
- const url = `http://localhost:${PORT}`
637
+ const url = `http://localhost:${port}`
635
638
 
636
639
  const chunk1 = {
637
640
  a: {
@@ -643,7 +646,7 @@ describe('bundle', () => {
643
646
  reply.send(chunk1)
644
647
  })
645
648
 
646
- await server.listen({ port: PORT })
649
+ await server.listen({ port: port })
647
650
 
648
651
  const input = {
649
652
  a: {
@@ -693,7 +696,7 @@ describe('bundle', () => {
693
696
  })
694
697
 
695
698
  it('tree shakes the external documents correctly', async () => {
696
- const url = `http://localhost:${PORT}`
699
+ const url = `http://localhost:${port}`
697
700
 
698
701
  const chunk1 = {
699
702
  a: {
@@ -715,7 +718,7 @@ describe('bundle', () => {
715
718
  reply.send(chunk1)
716
719
  })
717
720
 
718
- await server.listen({ port: PORT })
721
+ await server.listen({ port: port })
719
722
 
720
723
  const input = {
721
724
  a: {
@@ -748,7 +751,7 @@ describe('bundle', () => {
748
751
  })
749
752
 
750
753
  it('tree shakes correctly when working with nested external refs', async () => {
751
- const url = `http://localhost:${PORT}`
754
+ const url = `http://localhost:${port}`
752
755
 
753
756
  const chunk2 = {
754
757
  a: {
@@ -786,7 +789,7 @@ describe('bundle', () => {
786
789
  reply.send(chunk2)
787
790
  })
788
791
 
789
- await server.listen({ port: PORT })
792
+ await server.listen({ port: port })
790
793
 
791
794
  const input = {
792
795
  a: {
@@ -832,7 +835,7 @@ describe('bundle', () => {
832
835
  })
833
836
 
834
837
  it('handles circular references when we treeshake', async () => {
835
- const url = `http://localhost:${PORT}`
838
+ const url = `http://localhost:${port}`
836
839
 
837
840
  const chunk1 = {
838
841
  a: {
@@ -853,7 +856,7 @@ describe('bundle', () => {
853
856
  reply.send(chunk1)
854
857
  })
855
858
 
856
- await server.listen({ port: PORT })
859
+ await server.listen({ port: port })
857
860
 
858
861
  const input = {
859
862
  a: {
@@ -887,7 +890,7 @@ describe('bundle', () => {
887
890
  })
888
891
 
889
892
  it('handles chunks', async () => {
890
- const url = `http://localhost:${PORT}`
893
+ const url = `http://localhost:${port}`
891
894
 
892
895
  const chunk1 = {
893
896
  description: 'Chunk 1',
@@ -907,7 +910,7 @@ describe('bundle', () => {
907
910
  reply.send(chunk2)
908
911
  })
909
912
 
910
- await server.listen({ port: PORT })
913
+ await server.listen({ port: port })
911
914
 
912
915
  const input = {
913
916
  a: {
@@ -985,7 +988,7 @@ describe('bundle', () => {
985
988
  })
986
989
 
987
990
  it('when bundle partial document we ensure all the dependencies references are resolved', async () => {
988
- const url = `http://localhost:${PORT}`
991
+ const url = `http://localhost:${port}`
989
992
 
990
993
  const chunk1 = {
991
994
  a: {
@@ -996,7 +999,7 @@ describe('bundle', () => {
996
999
  reply.send(chunk1)
997
1000
  })
998
1001
 
999
- await server.listen({ port: PORT })
1002
+ await server.listen({ port: port })
1000
1003
 
1001
1004
  const input = {
1002
1005
  a: {
@@ -1050,7 +1053,7 @@ describe('bundle', () => {
1050
1053
  })
1051
1054
 
1052
1055
  it('should correctly handle nested chunk urls', async () => {
1053
- const url = `http://localhost:${PORT}`
1056
+ const url = `http://localhost:${port}`
1054
1057
 
1055
1058
  const chunk1 = {
1056
1059
  chunk1: 'chunk1',
@@ -1092,7 +1095,7 @@ describe('bundle', () => {
1092
1095
  reply.send(external)
1093
1096
  })
1094
1097
 
1095
- await server.listen({ port: PORT })
1098
+ await server.listen({ port: port })
1096
1099
 
1097
1100
  const input = {
1098
1101
  c: {
@@ -1142,13 +1145,13 @@ describe('bundle', () => {
1142
1145
  $ref: '#/a',
1143
1146
  },
1144
1147
  nonBundle: {
1145
- $ref: `http://localhost:${PORT}/chunk1#`,
1148
+ $ref: `http://localhost:${port}/chunk1#`,
1146
1149
  },
1147
1150
  'x-ext': {
1148
1151
  [await getHash(`${url}/external/document.json`)]: {
1149
1152
  external: 'external',
1150
1153
  someChunk: {
1151
- $ref: `#/x-ext/${await getHash(`${url}/external/chunk3`)}`,
1154
+ $ref: `#/x-ext/${await getHash(`${url}/chunk3`)}`,
1152
1155
  $global: true,
1153
1156
  },
1154
1157
  },
@@ -1164,21 +1167,21 @@ describe('bundle', () => {
1164
1167
  $ref: '#/c',
1165
1168
  },
1166
1169
  },
1167
- [await getHash(`${url}/external/chunk3`)]: {
1170
+ [await getHash(`${url}/chunk3`)]: {
1168
1171
  chunk3: 'chunk3',
1169
1172
  },
1170
1173
  },
1171
1174
  'x-ext-urls': {
1172
1175
  [await getHash(`${url}/chunk1`)]: `${url}/chunk1`,
1173
1176
  [await getHash(`${url}/chunk2`)]: `${url}/chunk2`,
1174
- [await getHash(`${url}/external/chunk3`)]: `${url}/external/chunk3`,
1177
+ [await getHash(`${url}/chunk3`)]: `${url}/chunk3`,
1175
1178
  [await getHash(`${url}/external/document.json`)]: `${url}/external/document.json`,
1176
1179
  },
1177
1180
  })
1178
1181
  })
1179
1182
 
1180
1183
  it('run success hook', async () => {
1181
- const url = `http://localhost:${PORT}`
1184
+ const url = `http://localhost:${port}`
1182
1185
 
1183
1186
  const chunk1 = {
1184
1187
  description: 'Chunk 1',
@@ -1188,7 +1191,7 @@ describe('bundle', () => {
1188
1191
  reply.send(chunk1)
1189
1192
  })
1190
1193
 
1191
- await server.listen({ port: PORT })
1194
+ await server.listen({ port: port })
1192
1195
 
1193
1196
  const input = {
1194
1197
  a: {
@@ -1225,14 +1228,14 @@ describe('bundle', () => {
1225
1228
  expect(resolveError).not.toHaveBeenCalledOnce()
1226
1229
  })
1227
1230
 
1228
- it('run success hook', async () => {
1229
- const url = `http://localhost:${PORT}`
1231
+ it('run error hook', async () => {
1232
+ const url = `http://localhost:${port}`
1230
1233
 
1231
1234
  server.get('/chunk1', (_, reply) => {
1232
1235
  reply.code(404).send()
1233
1236
  })
1234
1237
 
1235
- await server.listen({ port: PORT })
1238
+ await server.listen({ port: port })
1236
1239
 
1237
1240
  const input = {
1238
1241
  a: {
@@ -1268,6 +1271,60 @@ describe('bundle', () => {
1268
1271
  expect(resolveError).toHaveBeenCalledOnce()
1269
1272
  expect(resolveError).toHaveBeenCalledWith(refA)
1270
1273
  })
1274
+
1275
+ it('uses the provided origin for object inputs', async () => {
1276
+ server.get('/', () => ({
1277
+ message: 'some resolved external reference',
1278
+ }))
1279
+ await server.listen({ port })
1280
+
1281
+ const result = await bundle(
1282
+ {
1283
+ a: {
1284
+ $ref: '/#',
1285
+ },
1286
+ },
1287
+ {
1288
+ treeShake: false,
1289
+ plugins: [fetchUrls()],
1290
+ origin: url,
1291
+ },
1292
+ )
1293
+
1294
+ expect(result).toEqual({
1295
+ 'a': {
1296
+ '$ref': '#/x-ext/3664f29',
1297
+ },
1298
+ 'x-ext': {
1299
+ '3664f29': {
1300
+ 'message': 'some resolved external reference',
1301
+ },
1302
+ },
1303
+ })
1304
+ })
1305
+
1306
+ it('prioritizes configuration origin rather than document input url', async () => {
1307
+ server.get('/', () => ({
1308
+ a: {
1309
+ $ref: '/d#',
1310
+ },
1311
+ }))
1312
+ server.get('/d', () => ({
1313
+ message: 'some resolved external reference',
1314
+ }))
1315
+ await server.listen({ port: port })
1316
+
1317
+ const result = await bundle(url, {
1318
+ plugins: [fetchUrls()],
1319
+ treeShake: false,
1320
+ origin: `${url}/a/b/c`,
1321
+ })
1322
+
1323
+ expect(result).toEqual({
1324
+ a: { '$ref': '#/x-ext/e53b62c' },
1325
+ 'x-ext': { e53b62c: { message: 'some resolved external reference' } },
1326
+ })
1327
+ })
1271
1328
  })
1272
1329
 
1273
1330
  describe('local files', () => {
@@ -1764,6 +1821,493 @@ describe('bundle', () => {
1764
1821
  })
1765
1822
  })
1766
1823
  })
1824
+
1825
+ describe('hooks', () => {
1826
+ describe('onBeforeNodeProcess', () => {
1827
+ it('should correctly call the `onBeforeNodeProcess` correctly on all the nodes', async () => {
1828
+ const fn = vi.fn()
1829
+
1830
+ const input = {
1831
+ someKey: 'someValue',
1832
+ anotherKey: {
1833
+ innerKey: 'nestedValue',
1834
+ },
1835
+ }
1836
+
1837
+ await bundle(input, {
1838
+ plugins: [],
1839
+ treeShake: false,
1840
+ hooks: {
1841
+ onBeforeNodeProcess(node) {
1842
+ fn(node)
1843
+ },
1844
+ },
1845
+ })
1846
+
1847
+ expect(fn).toHaveBeenCalled()
1848
+ expect(fn).toBeCalledTimes(2)
1849
+
1850
+ expect(fn.mock.calls[0][0]).toEqual(input)
1851
+ expect(fn.mock.calls[1][0]).toEqual(input.anotherKey)
1852
+ })
1853
+
1854
+ it('should run bundle on the mutated object properties', async () => {
1855
+ const fn = vi.fn()
1856
+
1857
+ const input = {
1858
+ a: {
1859
+ b: {
1860
+ c: 'c',
1861
+ d: 'd',
1862
+ },
1863
+ e: 'e',
1864
+ },
1865
+ }
1866
+
1867
+ await bundle(input, {
1868
+ plugins: [],
1869
+ treeShake: false,
1870
+ hooks: {
1871
+ onBeforeNodeProcess(node) {
1872
+ if ('e' in node) {
1873
+ node['processedKey'] = { 'message': 'Processed node' }
1874
+ }
1875
+ fn(node)
1876
+ },
1877
+ },
1878
+ })
1879
+
1880
+ expect(fn).toHaveBeenCalled()
1881
+ expect(fn).toBeCalledTimes(4)
1882
+
1883
+ expect(fn.mock.calls[3][0]).toEqual({ 'message': 'Processed node' })
1884
+ })
1885
+ })
1886
+
1887
+ describe('onAfterNodeProcess', () => {
1888
+ it('should call `onAfterNodeProcess` hook on the nodes', async () => {
1889
+ const fn = vi.fn()
1890
+
1891
+ const input = {
1892
+ a: {
1893
+ b: {
1894
+ c: 'c',
1895
+ d: 'd',
1896
+ },
1897
+ e: 'e',
1898
+ },
1899
+ }
1900
+
1901
+ await bundle(input, {
1902
+ plugins: [],
1903
+ treeShake: false,
1904
+ hooks: {
1905
+ onAfterNodeProcess(node) {
1906
+ if ('e' in node) {
1907
+ node['processedKey'] = { 'message': 'Processed node' }
1908
+ }
1909
+ fn(node)
1910
+ },
1911
+ },
1912
+ })
1913
+
1914
+ expect(fn).toHaveBeenCalled()
1915
+ expect(fn).toHaveBeenCalledTimes(3)
1916
+ })
1917
+ })
1918
+ })
1919
+
1920
+ describe('plugins', () => {
1921
+ it('use load plugins to load the documents', async () => {
1922
+ const validate = vi.fn()
1923
+ const exec = vi.fn()
1924
+
1925
+ const resolver = (): LoaderPlugin => {
1926
+ return {
1927
+ type: 'loader',
1928
+ validate(value) {
1929
+ validate(value)
1930
+ return true
1931
+ },
1932
+ async exec(value) {
1933
+ exec(value)
1934
+ return {
1935
+ ok: true,
1936
+ data: { message: 'Resolved document' },
1937
+ }
1938
+ },
1939
+ }
1940
+ }
1941
+
1942
+ const result = await bundle('hello', { treeShake: false, plugins: [resolver()] })
1943
+
1944
+ expect(validate).toHaveBeenCalledOnce()
1945
+ expect(exec).toHaveBeenCalledOnce()
1946
+
1947
+ expect(validate.mock.calls[0][0]).toBe('hello')
1948
+ expect(exec.mock.calls[0][0]).toBe('hello')
1949
+
1950
+ expect(result).toEqual({ message: 'Resolved document' })
1951
+ })
1952
+
1953
+ it('throws if we can not process the input with any of the provided loaders', async () => {
1954
+ const validate = vi.fn()
1955
+ const exec = vi.fn()
1956
+
1957
+ const resolver = (): LoaderPlugin => {
1958
+ return {
1959
+ type: 'loader',
1960
+ validate(value) {
1961
+ validate(value)
1962
+ return false
1963
+ },
1964
+ async exec(value) {
1965
+ exec(value)
1966
+ return {
1967
+ ok: true,
1968
+ data: { message: 'Resolved document' },
1969
+ }
1970
+ },
1971
+ }
1972
+ }
1973
+
1974
+ await expect(bundle('hello', { treeShake: false, plugins: [resolver()] })).rejects.toThrow()
1975
+
1976
+ expect(validate).toHaveBeenCalledOnce()
1977
+ expect(validate.mock.calls[0][0]).toBe('hello')
1978
+
1979
+ expect(exec).not.toHaveBeenCalled()
1980
+ })
1981
+
1982
+ it('use load plugin to resolve external refs', async () => {
1983
+ const validate = vi.fn()
1984
+ const exec = vi.fn()
1985
+
1986
+ const resolver = (): LoaderPlugin => {
1987
+ return {
1988
+ type: 'loader',
1989
+ validate(value) {
1990
+ validate(value)
1991
+ return true
1992
+ },
1993
+ async exec(value) {
1994
+ exec(value)
1995
+ return {
1996
+ ok: true,
1997
+ data: { message: 'Resolved document' },
1998
+ }
1999
+ },
2000
+ }
2001
+ }
2002
+
2003
+ const result = await bundle({ $ref: 'hello' }, { treeShake: false, plugins: [resolver()] })
2004
+
2005
+ expect(validate).toHaveBeenCalledOnce()
2006
+ expect(exec).toHaveBeenCalledOnce()
2007
+
2008
+ expect(validate.mock.calls[0][0]).toBe('hello')
2009
+ expect(exec.mock.calls[0][0]).toBe('hello')
2010
+
2011
+ expect(result).toEqual({
2012
+ $ref: '#/x-ext/aaf4c61',
2013
+ 'x-ext': {
2014
+ 'aaf4c61': {
2015
+ message: 'Resolved document',
2016
+ },
2017
+ },
2018
+ })
2019
+ })
2020
+
2021
+ it('emits warning when there is no loader to resolve the external ref', async () => {
2022
+ resetConsoleSpies()
2023
+ const validate = vi.fn()
2024
+ const exec = vi.fn()
2025
+
2026
+ const resolver = (): LoaderPlugin => {
2027
+ return {
2028
+ type: 'loader',
2029
+ validate(value) {
2030
+ validate(value)
2031
+ return false
2032
+ },
2033
+ async exec(value) {
2034
+ exec(value)
2035
+ return {
2036
+ ok: true,
2037
+ data: { message: 'Resolved document' },
2038
+ }
2039
+ },
2040
+ }
2041
+ }
2042
+
2043
+ const result = await bundle({ $ref: 'hello' }, { treeShake: false, plugins: [resolver()] })
2044
+
2045
+ expect(validate).toHaveBeenCalledOnce()
2046
+ expect(validate.mock.calls[0][0]).toBe('hello')
2047
+
2048
+ expect(exec).not.toHaveBeenCalled()
2049
+
2050
+ expect(result).toEqual({ $ref: 'hello' })
2051
+
2052
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
2053
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
2054
+ 'Failed to resolve external reference "hello". The reference may be invalid, inaccessible, or missing a loader for this type of reference.',
2055
+ )
2056
+ })
2057
+
2058
+ it('lets plugins hook into nodes lifecycle #1', async () => {
2059
+ const onBeforeNodeProcessCallback = vi.fn()
2060
+ const onAfterNodeProcessCallback = vi.fn()
2061
+
2062
+ await bundle(
2063
+ {
2064
+ prop: {
2065
+ innerProp: 'string',
2066
+ },
2067
+ },
2068
+ {
2069
+ treeShake: false,
2070
+ plugins: [
2071
+ {
2072
+ type: 'lifecycle',
2073
+ onBeforeNodeProcess: onBeforeNodeProcessCallback,
2074
+ onAfterNodeProcess: onAfterNodeProcessCallback,
2075
+ },
2076
+ ],
2077
+ },
2078
+ )
2079
+
2080
+ expect(onBeforeNodeProcessCallback).toHaveBeenCalledTimes(2)
2081
+ expect(onBeforeNodeProcessCallback.mock.calls[0][0]).toEqual({
2082
+ prop: {
2083
+ innerProp: 'string',
2084
+ },
2085
+ })
2086
+ expect(onBeforeNodeProcessCallback.mock.calls[0][1]).toEqual({
2087
+ path: [],
2088
+ resolutionCache: new Map(),
2089
+ parentNode: null,
2090
+ rootNode: {
2091
+ prop: {
2092
+ innerProp: 'string',
2093
+ },
2094
+ },
2095
+ loaders: [],
2096
+ })
2097
+ expect(onBeforeNodeProcessCallback.mock.calls[1][0]).toEqual({
2098
+ innerProp: 'string',
2099
+ })
2100
+ expect(onBeforeNodeProcessCallback.mock.calls[1][1]).toEqual({
2101
+ path: ['prop'],
2102
+ resolutionCache: new Map(),
2103
+ parentNode: {
2104
+ prop: {
2105
+ innerProp: 'string',
2106
+ },
2107
+ },
2108
+ rootNode: {
2109
+ prop: {
2110
+ innerProp: 'string',
2111
+ },
2112
+ },
2113
+ loaders: [],
2114
+ })
2115
+ expect(onAfterNodeProcessCallback).toHaveBeenCalledTimes(2)
2116
+ expect(onAfterNodeProcessCallback.mock.calls[0][0]).toEqual({
2117
+ innerProp: 'string',
2118
+ })
2119
+ expect(onAfterNodeProcessCallback.mock.calls[0][1]).toEqual({
2120
+ path: ['prop'],
2121
+ resolutionCache: new Map(),
2122
+ parentNode: {
2123
+ prop: {
2124
+ innerProp: 'string',
2125
+ },
2126
+ },
2127
+ rootNode: {
2128
+ prop: {
2129
+ innerProp: 'string',
2130
+ },
2131
+ },
2132
+ loaders: [],
2133
+ })
2134
+ expect(onAfterNodeProcessCallback.mock.calls[1][0]).toEqual({
2135
+ prop: {
2136
+ innerProp: 'string',
2137
+ },
2138
+ })
2139
+ expect(onAfterNodeProcessCallback.mock.calls[1][1]).toEqual({
2140
+ path: [],
2141
+ resolutionCache: new Map(),
2142
+ parentNode: null,
2143
+ rootNode: {
2144
+ prop: {
2145
+ innerProp: 'string',
2146
+ },
2147
+ },
2148
+ loaders: [],
2149
+ })
2150
+ })
2151
+
2152
+ it('lets plugins hook into nodes lifecycle #2', async () => {
2153
+ const validate = vi.fn()
2154
+ const exec = vi.fn()
2155
+ const onResolveStart = vi.fn()
2156
+ const onResolveError = vi.fn()
2157
+ const onResolveSuccess = vi.fn()
2158
+ await bundle(
2159
+ {
2160
+ hello: {
2161
+ $ref: 'some-value',
2162
+ },
2163
+ hi: {
2164
+ $ref: 'resolve',
2165
+ },
2166
+ },
2167
+ {
2168
+ treeShake: false,
2169
+ plugins: [
2170
+ {
2171
+ type: 'loader',
2172
+ validate(value) {
2173
+ validate()
2174
+ if (value === 'resolve') {
2175
+ return true
2176
+ }
2177
+ return false
2178
+ },
2179
+ async exec(value) {
2180
+ exec()
2181
+ return {
2182
+ ok: true,
2183
+ data: {
2184
+ message: 'Resolved value',
2185
+ 'x-original-value': value,
2186
+ },
2187
+ }
2188
+ },
2189
+ },
2190
+ {
2191
+ type: 'lifecycle',
2192
+ onResolveStart,
2193
+ onResolveError,
2194
+ onResolveSuccess,
2195
+ },
2196
+ ],
2197
+ },
2198
+ )
2199
+
2200
+ expect(validate).toHaveBeenCalledTimes(2)
2201
+ expect(exec).toHaveBeenCalledOnce()
2202
+
2203
+ expect(onResolveStart).toHaveBeenCalledTimes(2)
2204
+ expect(onResolveStart.mock.calls[0][0]).toEqual({
2205
+ $ref: 'some-value',
2206
+ })
2207
+ expect(onResolveStart.mock.calls[1][0]).toEqual({
2208
+ $ref: '#/x-ext/4e7a208',
2209
+ })
2210
+ expect(onResolveError).toHaveBeenCalledTimes(1)
2211
+ expect(onResolveError.mock.calls[0][0]).toEqual({
2212
+ $ref: 'some-value',
2213
+ })
2214
+ expect(onResolveSuccess).toHaveBeenCalledTimes(1)
2215
+ expect(onResolveSuccess.mock.calls[0][0]).toEqual({
2216
+ $ref: '#/x-ext/4e7a208',
2217
+ })
2218
+ })
2219
+
2220
+ it('correctly provides the parent node in different levels', async () => {
2221
+ const onBeforeNodeProcess = vi.fn()
2222
+ const input = {
2223
+ a: {
2224
+ b: {
2225
+ c: {
2226
+ someNode: 'hello world',
2227
+ },
2228
+ },
2229
+ },
2230
+ d: {
2231
+ $ref: '#/a',
2232
+ },
2233
+ e: {
2234
+ f: {
2235
+ $ref: '#/a/b/c',
2236
+ },
2237
+ },
2238
+ }
2239
+
2240
+ await bundle(input, {
2241
+ plugins: [
2242
+ {
2243
+ type: 'lifecycle',
2244
+ onBeforeNodeProcess,
2245
+ },
2246
+ ],
2247
+ treeShake: false,
2248
+ })
2249
+
2250
+ expect(onBeforeNodeProcess).toHaveBeenCalled()
2251
+
2252
+ // First call should be the root with a null parent
2253
+ expect(onBeforeNodeProcess.mock.calls[0][0]).toEqual(input)
2254
+ expect(onBeforeNodeProcess.mock.calls[0][1].parentNode).toEqual(null)
2255
+
2256
+ expect(onBeforeNodeProcess.mock.calls[1][0]).toEqual(input.a)
2257
+ expect(onBeforeNodeProcess.mock.calls[1][1].parentNode).toEqual(input)
2258
+
2259
+ expect(onBeforeNodeProcess.mock.calls[2][0]).toEqual(input.d)
2260
+ expect(onBeforeNodeProcess.mock.calls[2][1].parentNode).toEqual(input)
2261
+
2262
+ expect(onBeforeNodeProcess.mock.calls[3][0]).toEqual(input.e)
2263
+ expect(onBeforeNodeProcess.mock.calls[3][1].parentNode).toEqual(input)
2264
+
2265
+ expect(onBeforeNodeProcess.mock.calls[4][0]).toEqual(input.a.b)
2266
+ expect(onBeforeNodeProcess.mock.calls[4][1].parentNode).toEqual(input.a)
2267
+
2268
+ expect(onBeforeNodeProcess.mock.calls[5][0]).toEqual(input.e.f)
2269
+ expect(onBeforeNodeProcess.mock.calls[5][1].parentNode).toEqual(input.e)
2270
+
2271
+ expect(onBeforeNodeProcess.mock.calls[6][0]).toEqual(input.a.b.c)
2272
+ expect(onBeforeNodeProcess.mock.calls[6][1].parentNode).toEqual(input.a.b)
2273
+ })
2274
+
2275
+ it('correctly provides the parent node on partial bundle for referenced nodes', async () => {
2276
+ const onBeforeNodeProcess = vi.fn()
2277
+
2278
+ const input = {
2279
+ a: {
2280
+ b: 'some-prop',
2281
+ },
2282
+ b: {
2283
+ c: {
2284
+ $ref: '#/a',
2285
+ },
2286
+ },
2287
+ }
2288
+
2289
+ await bundle(input.b, {
2290
+ treeShake: false,
2291
+ root: input,
2292
+ plugins: [
2293
+ {
2294
+ type: 'lifecycle',
2295
+ onBeforeNodeProcess,
2296
+ },
2297
+ ],
2298
+ })
2299
+
2300
+ expect(onBeforeNodeProcess).toHaveBeenCalled()
2301
+ expect(onBeforeNodeProcess.mock.calls[0][0]).toEqual(input.b)
2302
+ expect(onBeforeNodeProcess.mock.calls[0][1].parentNode).toEqual(null)
2303
+
2304
+ expect(onBeforeNodeProcess.mock.calls[1][0]).toEqual(input.b.c)
2305
+ expect(onBeforeNodeProcess.mock.calls[1][1].parentNode).toEqual(input.b)
2306
+
2307
+ expect(onBeforeNodeProcess.mock.calls[2][0]).toEqual(input.a)
2308
+ expect(onBeforeNodeProcess.mock.calls[2][1].parentNode).toEqual(input)
2309
+ })
2310
+ })
1767
2311
  })
1768
2312
 
1769
2313
  describe('isRemoteUrl', () => {