@scalar/json-magic 0.12.20 → 0.13.2
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.
- package/CHANGELOG.md +53 -0
- package/README.md +392 -173
- package/dist/bundle/index.d.ts +1 -1
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/diff/apply.d.ts +18 -2
- package/dist/diff/apply.d.ts.map +1 -1
- package/dist/diff/apply.js +37 -2
- package/dist/diff/diff.d.ts +10 -0
- package/dist/diff/diff.d.ts.map +1 -1
- package/dist/diff/diff.js +36 -2
- package/dist/diff/merge.d.ts +8 -1
- package/dist/diff/merge.d.ts.map +1 -1
- package/dist/diff/merge.js +14 -3
- package/dist/diff/trie.d.ts +7 -1
- package/dist/diff/trie.d.ts.map +1 -1
- package/dist/diff/trie.js +10 -5
- package/dist/diff/utils.d.ts +11 -2
- package/dist/diff/utils.d.ts.map +1 -1
- package/dist/diff/utils.js +32 -2
- package/package.json +23 -3
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
# json-magic
|
|
1
|
+
# @scalar/json-magic
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@scalar/json-magic)
|
|
4
4
|
[](https://www.npmjs.com/package/@scalar/json-magic)
|
|
5
5
|
[](https://www.npmjs.com/package/@scalar/json-magic)
|
|
6
6
|
[](https://discord.gg/scalar)
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
A collection of utilities for working with JSON objects, including diffing, conflict resolution, bundling and more.
|
|
10
9
|
|
|
11
10
|
---
|
|
@@ -21,9 +20,41 @@ Scalar is an open-source API platform for teams who want beautiful developer int
|
|
|
21
20
|
|
|
22
21
|
---
|
|
23
22
|
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm add @scalar/json-magic
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Entry points
|
|
30
|
+
|
|
31
|
+
There is no root export. Every module is imported from its own entry point, so you only pay for what you use.
|
|
32
|
+
|
|
33
|
+
| Entry point | Exports |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `@scalar/json-magic/bundle` | `bundle`, `isLocalRef`, `prefixInternalRef`, `prefixInternalRefRecursive`, `resolveAndCopyReferences`, plus the `Plugin`, `LoaderPlugin`, `LifecyclePlugin` and `ResolveResult` types |
|
|
36
|
+
| `@scalar/json-magic/bundle/plugins/browser` | `fetchUrls`, `parseJson`, `parseYaml` |
|
|
37
|
+
| `@scalar/json-magic/bundle/plugins/node` | `fetchUrls`, `parseJson`, `parseYaml`, `readFiles` |
|
|
38
|
+
| `@scalar/json-magic/bundle/value-generator` | `getHash`, `generateUniqueValue`, `uniqueValueGeneratorFactory` |
|
|
39
|
+
| `@scalar/json-magic/dereference` | `dereference` |
|
|
40
|
+
| `@scalar/json-magic/diff` | `diff`, `merge`, `apply`, the `Difference` type |
|
|
41
|
+
| `@scalar/json-magic/magic-proxy` | `createMagicProxy`, `getRaw` |
|
|
42
|
+
| `@scalar/json-magic/helpers/*` | Small standalone helpers, see [Helpers](#helpers) |
|
|
43
|
+
|
|
44
|
+
## Which module do I need?
|
|
45
|
+
|
|
46
|
+
| I want to … | Use |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| Pull external `$ref` documents into a single self-contained document | [`bundle`](#bundle) |
|
|
49
|
+
| Read through `$ref` pointers without rewriting the document | [`magic-proxy`](#magic-proxy) |
|
|
50
|
+
| Resolve every `$ref`, internal and external, in one call | [`dereference`](#dereference) |
|
|
51
|
+
| Compare two documents and merge concurrent edits | [`diff`](#diff) |
|
|
52
|
+
|
|
24
53
|
## bundle
|
|
25
54
|
|
|
26
|
-
|
|
55
|
+
`bundle` walks a JSON object, resolves every external `$ref` (URLs, local files, or anything a custom loader plugin can handle) and embeds the result into the document itself. The original `$ref` values are rewritten to point at the embedded copies, so the output is a single self-contained document.
|
|
56
|
+
|
|
57
|
+
External documents are stored under the `x-ext` key, and the mapping between the generated keys and their original URLs is stored under `x-ext-urls`. Both keys are configurable, see [Options](#options).
|
|
27
58
|
|
|
28
59
|
### Quick start
|
|
29
60
|
|
|
@@ -31,23 +62,36 @@ Bundle external references in a json object
|
|
|
31
62
|
import { bundle } from '@scalar/json-magic/bundle'
|
|
32
63
|
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
33
64
|
|
|
34
|
-
const result = await bundle(
|
|
35
|
-
$ref: 'http://example.com/document.json'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
65
|
+
const result = await bundle(
|
|
66
|
+
{ $ref: 'http://example.com/document.json' },
|
|
67
|
+
{
|
|
68
|
+
plugins: [fetchUrls()],
|
|
69
|
+
treeShake: false,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
40
72
|
|
|
41
|
-
//
|
|
73
|
+
// The bundled document
|
|
42
74
|
console.log(result)
|
|
43
75
|
```
|
|
44
76
|
|
|
45
|
-
|
|
77
|
+
When the input is an object it is modified in place, and the same object is returned. When the input is a string it is resolved with the given plugins first.
|
|
78
|
+
|
|
79
|
+
### Loaders
|
|
80
|
+
|
|
81
|
+
A loader plugin teaches the bundler how to read one kind of reference. Import loaders from `@scalar/json-magic/bundle/plugins/browser` in the browser, or from `@scalar/json-magic/bundle/plugins/node` in Node.js. The Node entry point is a superset: it adds `readFiles`, which needs filesystem access.
|
|
82
|
+
|
|
83
|
+
| Loader | Handles | Available in |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `fetchUrls` | `http://` and `https://` references | browser, node |
|
|
86
|
+
| `readFiles` | Local file paths | node |
|
|
87
|
+
| `parseJson` | A raw JSON string passed as the input | browser, node |
|
|
88
|
+
| `parseYaml` | A raw YAML string passed as the input | browser, node |
|
|
46
89
|
|
|
47
|
-
|
|
90
|
+
You can combine as many loaders as you need. The first one whose `validate` returns `true` wins.
|
|
48
91
|
|
|
49
|
-
|
|
50
|
-
|
|
92
|
+
#### fetchUrls
|
|
93
|
+
|
|
94
|
+
Resolves remote documents over HTTP. It works in both Node.js and the browser.
|
|
51
95
|
|
|
52
96
|
```ts
|
|
53
97
|
import { bundle } from '@scalar/json-magic/bundle'
|
|
@@ -59,102 +103,104 @@ const document = {
|
|
|
59
103
|
paths: {},
|
|
60
104
|
components: {
|
|
61
105
|
schemas: {
|
|
62
|
-
User: { $ref: 'https://example.com/user-schema.json#' }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
106
|
+
User: { $ref: 'https://example.com/user-schema.json#' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
65
109
|
}
|
|
66
110
|
|
|
67
|
-
// This
|
|
111
|
+
// This bundles all external documents and turns external references into internal ones
|
|
68
112
|
await bundle(document, {
|
|
69
113
|
plugins: [fetchUrls()],
|
|
70
|
-
|
|
114
|
+
// Removes the parts of the external documents that are not referenced
|
|
115
|
+
treeShake: true,
|
|
71
116
|
})
|
|
72
117
|
|
|
73
118
|
console.log(document)
|
|
74
119
|
```
|
|
75
120
|
|
|
76
|
-
|
|
121
|
+
##### Limiting the number of concurrent requests
|
|
77
122
|
|
|
78
123
|
```ts
|
|
124
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
125
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
126
|
+
|
|
79
127
|
await bundle(document, {
|
|
80
128
|
plugins: [
|
|
81
129
|
fetchUrls({
|
|
82
|
-
|
|
130
|
+
// Run at most 10 requests at the same time
|
|
131
|
+
limit: 10,
|
|
83
132
|
}),
|
|
84
133
|
],
|
|
85
|
-
treeShake: false
|
|
134
|
+
treeShake: false,
|
|
86
135
|
})
|
|
87
|
-
|
|
88
136
|
```
|
|
89
137
|
|
|
90
|
-
|
|
91
|
-
|
|
138
|
+
##### Custom headers
|
|
139
|
+
|
|
140
|
+
Headers are matched against the host of the reference, so a token is only ever sent to the domains you list.
|
|
92
141
|
|
|
93
142
|
```ts
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
treeShake: false
|
|
113
|
-
},
|
|
114
|
-
)
|
|
143
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
144
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
145
|
+
|
|
146
|
+
await bundle(document, {
|
|
147
|
+
plugins: [
|
|
148
|
+
fetchUrls({
|
|
149
|
+
headers: [
|
|
150
|
+
{
|
|
151
|
+
domains: ['example.com'],
|
|
152
|
+
headers: {
|
|
153
|
+
Authorization: 'Bearer <TOKEN>',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
treeShake: false,
|
|
160
|
+
})
|
|
115
161
|
```
|
|
116
162
|
|
|
117
|
-
|
|
118
|
-
|
|
163
|
+
##### Custom fetch function
|
|
164
|
+
|
|
165
|
+
For advanced use cases like proxying requests or implementing custom network logic, you can provide your own fetch implementation. This allows you to handle things like CORS restrictions, custom authentication flows, or request and response transformations.
|
|
119
166
|
|
|
120
167
|
```ts
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
},
|
|
136
|
-
)
|
|
168
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
169
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
170
|
+
|
|
171
|
+
await bundle(document, {
|
|
172
|
+
plugins: [
|
|
173
|
+
fetchUrls({
|
|
174
|
+
fetch: async (input, init) => {
|
|
175
|
+
console.log('Custom fetch logic')
|
|
176
|
+
return fetch(input, init)
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
treeShake: false,
|
|
181
|
+
})
|
|
137
182
|
```
|
|
138
183
|
|
|
139
|
-
|
|
184
|
+
##### Bundle from a remote URL
|
|
185
|
+
|
|
186
|
+
The input itself can be a URL, as long as a loader can handle it.
|
|
140
187
|
|
|
141
188
|
```ts
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
plugins: [
|
|
146
|
-
fetchUrls(),
|
|
147
|
-
],
|
|
148
|
-
treeShake: false
|
|
149
|
-
},
|
|
150
|
-
)
|
|
189
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
190
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
151
191
|
|
|
152
|
-
|
|
192
|
+
const result = await bundle('https://example.com/openapi.json', {
|
|
193
|
+
plugins: [fetchUrls()],
|
|
194
|
+
treeShake: false,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// The bundled document
|
|
153
198
|
console.log(result)
|
|
154
199
|
```
|
|
155
200
|
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
#### readFiles
|
|
202
|
+
|
|
203
|
+
Resolves local files. This loader only works in a Node.js environment.
|
|
158
204
|
|
|
159
205
|
```ts
|
|
160
206
|
import { bundle } from '@scalar/json-magic/bundle'
|
|
@@ -166,91 +212,137 @@ const document = {
|
|
|
166
212
|
paths: {},
|
|
167
213
|
components: {
|
|
168
214
|
schemas: {
|
|
169
|
-
User: { $ref: './user-schema.json#' }
|
|
170
|
-
}
|
|
171
|
-
}
|
|
215
|
+
User: { $ref: './user-schema.json#' },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
172
218
|
}
|
|
173
219
|
|
|
174
|
-
// This will bundle all external documents and turn all references from external into internal
|
|
175
220
|
await bundle(document, {
|
|
176
221
|
plugins: [readFiles()],
|
|
177
|
-
treeShake: false
|
|
222
|
+
treeShake: false,
|
|
178
223
|
})
|
|
179
224
|
|
|
180
225
|
console.log(document)
|
|
181
226
|
```
|
|
182
227
|
|
|
183
|
-
|
|
184
|
-
|
|
228
|
+
##### Bundle from a local file
|
|
229
|
+
|
|
230
|
+
You can pass the file path directly, as long as `readFiles` is registered to read it.
|
|
185
231
|
|
|
186
232
|
```ts
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{
|
|
190
|
-
plugins: [
|
|
191
|
-
readFiles(),
|
|
192
|
-
],
|
|
193
|
-
treeShake: false
|
|
194
|
-
},
|
|
195
|
-
)
|
|
233
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
234
|
+
import { readFiles } from '@scalar/json-magic/bundle/plugins/node'
|
|
196
235
|
|
|
197
|
-
|
|
236
|
+
const result = await bundle('./input.json', {
|
|
237
|
+
plugins: [readFiles()],
|
|
238
|
+
treeShake: false,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// The bundled document
|
|
198
242
|
console.log(result)
|
|
199
243
|
```
|
|
200
244
|
|
|
201
|
-
|
|
245
|
+
A document that mixes remote and local references needs both loaders:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
249
|
+
import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node'
|
|
250
|
+
|
|
251
|
+
await bundle('./openapi.json', {
|
|
252
|
+
plugins: [readFiles(), fetchUrls()],
|
|
253
|
+
treeShake: false,
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### parseJson
|
|
258
|
+
|
|
259
|
+
Accepts a raw JSON string as the input.
|
|
202
260
|
|
|
203
|
-
You can pass raw json string as input
|
|
204
261
|
```ts
|
|
205
262
|
import { bundle } from '@scalar/json-magic/bundle'
|
|
206
263
|
import { parseJson } from '@scalar/json-magic/bundle/plugins/browser'
|
|
207
264
|
|
|
208
|
-
const result = await bundle(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
parseJson(),
|
|
213
|
-
],
|
|
214
|
-
treeShake: false
|
|
215
|
-
},
|
|
216
|
-
)
|
|
265
|
+
const result = await bundle('{ "openapi": "3.1.1" }', {
|
|
266
|
+
plugins: [parseJson()],
|
|
267
|
+
treeShake: false,
|
|
268
|
+
})
|
|
217
269
|
|
|
218
|
-
//
|
|
270
|
+
// The bundled document
|
|
219
271
|
console.log(result)
|
|
220
272
|
```
|
|
221
273
|
|
|
222
|
-
|
|
274
|
+
#### parseYaml
|
|
275
|
+
|
|
276
|
+
Accepts a raw YAML string as the input.
|
|
223
277
|
|
|
224
|
-
You can pass raw yaml string as input
|
|
225
278
|
```ts
|
|
226
279
|
import { bundle } from '@scalar/json-magic/bundle'
|
|
227
280
|
import { parseYaml } from '@scalar/json-magic/bundle/plugins/browser'
|
|
228
281
|
|
|
229
|
-
const result = await bundle(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
parseYaml(),
|
|
234
|
-
],
|
|
235
|
-
treeShake: false
|
|
236
|
-
},
|
|
237
|
-
)
|
|
282
|
+
const result = await bundle('openapi: "3.1.1"\n', {
|
|
283
|
+
plugins: [parseYaml()],
|
|
284
|
+
treeShake: false,
|
|
285
|
+
})
|
|
238
286
|
|
|
239
|
-
//
|
|
287
|
+
// The bundled document
|
|
240
288
|
console.log(result)
|
|
241
289
|
```
|
|
242
290
|
|
|
243
|
-
####
|
|
291
|
+
#### Writing a custom loader
|
|
292
|
+
|
|
293
|
+
A loader is a plain object, so you can resolve references from anywhere: a database, an in-memory map, or a custom protocol.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { bundle, type LoaderPlugin } from '@scalar/json-magic/bundle'
|
|
297
|
+
|
|
298
|
+
const workspaceFiles: LoaderPlugin = {
|
|
299
|
+
type: 'loader',
|
|
300
|
+
validate: (value) => value.startsWith('workspace:'),
|
|
301
|
+
exec: async (value) => {
|
|
302
|
+
const raw = await readFromWorkspace(value.replace('workspace:', ''))
|
|
303
|
+
|
|
304
|
+
if (raw === undefined) {
|
|
305
|
+
return { ok: false }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { ok: true, data: JSON.parse(raw), raw }
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await bundle(document, {
|
|
313
|
+
plugins: [workspaceFiles],
|
|
314
|
+
treeShake: false,
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`exec` returns `{ ok: true, data, raw }` on success and `{ ok: false }` on failure. A failed resolution is reported through the `onResolveError` hook and logged as a warning, it does not throw.
|
|
319
|
+
|
|
320
|
+
### Options
|
|
321
|
+
|
|
322
|
+
| Option | Type | Default | Description |
|
|
323
|
+
| --- | --- | --- | --- |
|
|
324
|
+
| `plugins` | `Plugin[]` | — | Loader and lifecycle plugins used during bundling. Required. |
|
|
325
|
+
| `treeShake` | `boolean` | — | Only keep the parts of external documents that are actually referenced. Required. |
|
|
326
|
+
| `depth` | `number` | unlimited | How deeply nested `$ref` pointers are followed. See the note below. |
|
|
327
|
+
| `root` | `UnknownObject` | the input | Base document to write external documents into, used for partial bundling. |
|
|
328
|
+
| `origin` | `string` | the input | Base path used to resolve relative references. |
|
|
329
|
+
| `cache` | `Map<string, Promise<ResolveResult>>` | new map | Cache of in-flight resolutions, reused across `bundle` calls to avoid refetching. |
|
|
330
|
+
| `visitedNodes` | `Set<unknown>` | new set | Nodes that have already been bundled, used to avoid re-bundling during partial bundles. |
|
|
331
|
+
| `urlMap` | `boolean` | `false` | Keep the mapping of generated keys to their original URLs in the output. |
|
|
332
|
+
| `externalDocumentsKey` | `string` | `'x-ext'` | Key that holds the bundled external documents. |
|
|
333
|
+
| `externalDocumentsMappingsKey` | `string` | `'x-ext-urls'` | Key that holds the mapping between generated keys and original URLs. |
|
|
334
|
+
| `compress` | `(value: string) => string \| Promise<string>` | hash | Function used to shorten URLs and file paths into document keys. |
|
|
335
|
+
| `hooks` | `object` | — | Lifecycle hooks, see [Hooks](#hooks). |
|
|
244
336
|
|
|
245
|
-
|
|
337
|
+
#### depth
|
|
246
338
|
|
|
247
|
-
The `depth` option controls how deeply the bundler
|
|
339
|
+
The `depth` option controls how deeply the bundler resolves `$ref` references. When you set `depth` to a number, the bundler only follows references up to that level of nesting. This is useful for creating partial bundles or limiting resource usage.
|
|
248
340
|
|
|
249
|
-
**Note:** When using `depth`, the resulting bundle may not be fully self-contained
|
|
341
|
+
**Note:** When using `depth`, the resulting bundle may not be fully self-contained, since nested references deeper than the given depth stay unresolved. If you use `depth` together with `visitedNodes`, be aware that parent nodes may be marked as visited even if their child references have not been fully resolved yet. Use this option with care if you require a complete bundle.
|
|
250
342
|
|
|
251
343
|
```ts
|
|
252
|
-
import { bundle } from '@scalar/
|
|
253
|
-
import { fetchUrls } from '@scalar/
|
|
344
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
345
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
254
346
|
|
|
255
347
|
await bundle(input, {
|
|
256
348
|
plugins: [fetchUrls()],
|
|
@@ -259,13 +351,13 @@ await bundle(input, {
|
|
|
259
351
|
})
|
|
260
352
|
```
|
|
261
353
|
|
|
262
|
-
|
|
354
|
+
#### externalDocumentsKey
|
|
263
355
|
|
|
264
|
-
The `externalDocumentsKey` option controls the key used to store external references. This key
|
|
356
|
+
The `externalDocumentsKey` option controls the key used to store external references. This key contains all bundled external documents, which keeps a clean separation between the main document and its bundled references. **Defaults** to `x-ext`.
|
|
265
357
|
|
|
266
358
|
```ts
|
|
267
|
-
import { bundle } from '@scalar/
|
|
268
|
-
import { fetchUrls } from '@scalar/
|
|
359
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
360
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
269
361
|
|
|
270
362
|
await bundle(input, {
|
|
271
363
|
plugins: [fetchUrls()],
|
|
@@ -274,65 +366,140 @@ await bundle(input, {
|
|
|
274
366
|
})
|
|
275
367
|
```
|
|
276
368
|
|
|
277
|
-
|
|
369
|
+
#### externalDocumentsMappingsKey
|
|
278
370
|
|
|
279
|
-
The `externalDocumentsMappingsKey` option controls the key used to maintain a mapping between
|
|
371
|
+
The `externalDocumentsMappingsKey` option controls the key used to maintain a mapping between generated keys and their original URLs. This mapping is essential for tracking the source of bundled references. It is removed from the output of a full bundle unless `urlMap` is enabled. **Defaults** to `x-ext-urls`.
|
|
280
372
|
|
|
281
373
|
```ts
|
|
282
|
-
import { bundle } from '@scalar/
|
|
283
|
-
import { fetchUrls } from '@scalar/
|
|
374
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
375
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
284
376
|
|
|
285
377
|
await bundle(input, {
|
|
286
378
|
plugins: [fetchUrls()],
|
|
287
379
|
treeShake: false,
|
|
288
380
|
urlMap: true,
|
|
289
|
-
|
|
381
|
+
externalDocumentsMappingsKey: 'x-external-urls',
|
|
382
|
+
})
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Hooks
|
|
386
|
+
|
|
387
|
+
Hooks let you observe and extend the bundling process, which is handy for progress reporting and error tracking.
|
|
388
|
+
|
|
389
|
+
| Hook | Called when |
|
|
390
|
+
| --- | --- |
|
|
391
|
+
| `onResolveStart` | The bundler starts resolving a `$ref` |
|
|
392
|
+
| `onResolveSuccess` | A `$ref` was resolved successfully |
|
|
393
|
+
| `onResolveError` | A `$ref` could not be resolved |
|
|
394
|
+
| `onBeforeNodeProcess` | Before a node is processed, may be async |
|
|
395
|
+
| `onAfterNodeProcess` | After a node is processed, may be async |
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
import { bundle } from '@scalar/json-magic/bundle'
|
|
399
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
400
|
+
|
|
401
|
+
const errors: string[] = []
|
|
402
|
+
|
|
403
|
+
await bundle(document, {
|
|
404
|
+
plugins: [fetchUrls()],
|
|
405
|
+
treeShake: true,
|
|
406
|
+
hooks: {
|
|
407
|
+
onResolveStart: (node) => console.log('Resolving:', node.$ref),
|
|
408
|
+
onResolveSuccess: (node) => console.log('Resolved:', node.$ref),
|
|
409
|
+
onResolveError: (node) => errors.push(`Failed to resolve ${node.$ref}`),
|
|
410
|
+
},
|
|
411
|
+
})
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
The same hooks can be shipped as a reusable lifecycle plugin, which is then passed alongside the loaders:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
import { bundle, type LifecyclePlugin } from '@scalar/json-magic/bundle'
|
|
418
|
+
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
|
|
419
|
+
|
|
420
|
+
const logProgress: LifecyclePlugin = {
|
|
421
|
+
type: 'lifecycle',
|
|
422
|
+
onResolveSuccess: (node) => console.log('Resolved:', node.$ref),
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await bundle(document, {
|
|
426
|
+
plugins: [fetchUrls(), logProgress],
|
|
427
|
+
treeShake: false,
|
|
290
428
|
})
|
|
291
429
|
```
|
|
292
430
|
|
|
293
431
|
## dereference
|
|
294
432
|
|
|
295
|
-
|
|
433
|
+
`dereference` resolves the `$ref` pointers of a document and returns the result wrapped in a [magic proxy](#magic-proxy), so referenced values are read through the `$ref-value` property.
|
|
296
434
|
|
|
297
|
-
|
|
435
|
+
It works in two modes:
|
|
298
436
|
|
|
299
|
-
- **Synchronous (`sync: true`)**:
|
|
300
|
-
- **Asynchronous (`sync: false
|
|
437
|
+
- **Synchronous (`sync: true`)**: only internal references are resolved, no network requests are made, and the result is returned directly.
|
|
438
|
+
- **Asynchronous (`sync: false`, the default)**: external references are bundled into the document first, so both internal and external references resolve. A Promise is returned.
|
|
301
439
|
|
|
302
440
|
### Options
|
|
303
441
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
442
|
+
| Option | Type | Default | Description |
|
|
443
|
+
| --- | --- | --- | --- |
|
|
444
|
+
| `sync` | `boolean` | `false` | Resolve internal references only, without returning a Promise. |
|
|
445
|
+
| `plugins` | `Plugin[]` | `[fetchUrls()]` | Loaders used to resolve external references in async mode. |
|
|
307
446
|
|
|
308
|
-
The result is an object with a `success` property.
|
|
447
|
+
The result is an object with a `success` property. On success it carries the dereferenced `data`, and on failure it carries an `errors` array describing the references that could not be resolved.
|
|
309
448
|
|
|
310
449
|
```ts
|
|
311
450
|
import { dereference } from '@scalar/json-magic/dereference'
|
|
312
451
|
|
|
313
452
|
const result = dereference({ a: 'hello', b: { $ref: '#/a' } }, { sync: true })
|
|
314
453
|
|
|
315
|
-
|
|
316
|
-
|
|
454
|
+
if (result.success) {
|
|
455
|
+
// 'hello'
|
|
456
|
+
console.log(result.data.b['$ref-value'])
|
|
457
|
+
}
|
|
317
458
|
```
|
|
318
459
|
|
|
319
|
-
To resolve
|
|
460
|
+
To resolve external references as well, drop `sync` (or set it to `false`) and await the result.
|
|
320
461
|
|
|
321
462
|
```ts
|
|
322
463
|
import { dereference } from '@scalar/json-magic/dereference'
|
|
323
464
|
|
|
324
|
-
const result = await dereference({
|
|
465
|
+
const result = await dereference({
|
|
466
|
+
a: 'hello',
|
|
467
|
+
b: { $ref: 'http://example.com/document.json#/somepath' },
|
|
468
|
+
})
|
|
325
469
|
|
|
326
|
-
|
|
327
|
-
console.log(result)
|
|
470
|
+
if (result.success) {
|
|
471
|
+
console.log(result.data)
|
|
472
|
+
} else {
|
|
473
|
+
console.error(result.errors)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
To resolve references from somewhere other than HTTP, pass your own loaders:
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
import { dereference } from '@scalar/json-magic/dereference'
|
|
481
|
+
import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node'
|
|
482
|
+
|
|
483
|
+
const result = await dereference(document, {
|
|
484
|
+
plugins: [readFiles(), fetchUrls()],
|
|
485
|
+
})
|
|
328
486
|
```
|
|
329
487
|
|
|
330
488
|
## diff
|
|
331
489
|
|
|
332
|
-
|
|
490
|
+
Compare two JSON objects, merge changes made in parallel, and surface the conflicts that need to be resolved manually.
|
|
333
491
|
|
|
334
|
-
|
|
492
|
+
| Function | Description |
|
|
493
|
+
| --- | --- |
|
|
494
|
+
| `diff(base, updated)` | Returns the list of `add`, `update` and `delete` changes that turn `base` into `updated` |
|
|
495
|
+
| `merge(diffA, diffB)` | Combines two changesets made against the same base into `{ diffs, conflicts }` |
|
|
496
|
+
| `apply(base, diffs)` | Applies a changeset to a document and returns it |
|
|
335
497
|
|
|
498
|
+
These three functions work on the documents themselves, not on copies of them. `apply` mutates the document it is given, `merge` merges one changeset into the entries of the other, and the changes a diff carries are live references into the documents that were compared. That means an applied document keeps sharing subtrees with the document it was diffed against, and a merge writes into the document behind its second changeset. Deep clone the documents before diffing them whenever you need the originals to stay untouched — cloning only the document you apply to is not enough.
|
|
499
|
+
|
|
500
|
+
`apply` throws an `InvalidChangesDetectedError` when a change points at a path that does not exist, when a change targets the document itself (an empty path, which a diff produces whenever the two documents differ at the root), and when a path reaches the prototype chain through `__proto__`, `constructor` or `prototype`.
|
|
501
|
+
|
|
502
|
+
### Quickstart
|
|
336
503
|
|
|
337
504
|
```ts
|
|
338
505
|
import { apply, diff, merge } from '@scalar/json-magic/diff'
|
|
@@ -366,19 +533,30 @@ const objectV2 = {
|
|
|
366
533
|
}
|
|
367
534
|
|
|
368
535
|
// Merge the changes of both versions with the same parent object
|
|
369
|
-
const { diffs, conflicts } = merge(
|
|
370
|
-
diff(baseObject, objectV1),
|
|
371
|
-
diff(baseObject, objectV2),
|
|
372
|
-
)
|
|
536
|
+
const { diffs, conflicts } = merge(diff(baseObject, objectV1), diff(baseObject, objectV2))
|
|
373
537
|
|
|
374
538
|
// Apply changes from v1 and v2 to the parent object to get the final object
|
|
375
539
|
const finalDocument = apply(baseObject, diffs)
|
|
376
540
|
```
|
|
377
541
|
|
|
542
|
+
### Conflicts
|
|
543
|
+
|
|
544
|
+
`merge` only returns the changes it can combine safely. When both sides touch the same path, the pair is returned in `conflicts` for you to resolve, and it is left out of `diffs`.
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
import { diff, merge } from '@scalar/json-magic/diff'
|
|
548
|
+
|
|
549
|
+
const { diffs, conflicts } = merge(diff(base, mine), diff(base, theirs))
|
|
550
|
+
|
|
551
|
+
for (const [mineChanges, theirChanges] of conflicts) {
|
|
552
|
+
// Decide which side wins, then push the winner onto diffs
|
|
553
|
+
diffs.push(...mineChanges)
|
|
554
|
+
}
|
|
555
|
+
```
|
|
378
556
|
|
|
379
557
|
## magic-proxy
|
|
380
558
|
|
|
381
|
-
A
|
|
559
|
+
A JavaScript proxy that resolves internal references as you access properties. Nothing is copied or rewritten: the underlying document keeps its `$ref` pointers, and referenced values are read on demand through the virtual `$ref-value` property. Resolved values are cached, and proxies are stable for the same target object, which makes it safe to use with reactive frameworks like Vue.
|
|
382
560
|
|
|
383
561
|
### Quick start
|
|
384
562
|
|
|
@@ -388,30 +566,71 @@ import { createMagicProxy, getRaw } from '@scalar/json-magic/magic-proxy'
|
|
|
388
566
|
const result = createMagicProxy({
|
|
389
567
|
a: 'hello',
|
|
390
568
|
b: {
|
|
391
|
-
$ref: '#/a'
|
|
392
|
-
}
|
|
569
|
+
$ref: '#/a',
|
|
570
|
+
},
|
|
393
571
|
})
|
|
394
572
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
* $ref: '#/a',
|
|
401
|
-
* '$ref-value': 'hello'
|
|
402
|
-
* }
|
|
403
|
-
* }
|
|
404
|
-
*/
|
|
405
|
-
console.log(result)
|
|
573
|
+
// 'hello', resolved on access
|
|
574
|
+
console.log(result.b['$ref-value'])
|
|
575
|
+
|
|
576
|
+
// '#/a', the original pointer is still there
|
|
577
|
+
console.log(result.b.$ref)
|
|
406
578
|
|
|
407
|
-
const rawObject = getRaw(result)
|
|
408
579
|
/**
|
|
580
|
+
* getRaw returns the untouched object:
|
|
409
581
|
* {
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
582
|
+
* a: 'hello',
|
|
583
|
+
* b: {
|
|
584
|
+
* $ref: '#/a'
|
|
585
|
+
* }
|
|
414
586
|
* }
|
|
415
587
|
*/
|
|
588
|
+
const rawObject = getRaw(result)
|
|
416
589
|
console.log(rawObject)
|
|
417
|
-
```
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Deeply nested values are proxied too, so `$ref-value` works at any depth. Writes pass through to the underlying object, and writing to `$ref-value` updates the value the reference points at.
|
|
593
|
+
|
|
594
|
+
Properties prefixed with `__scalar_` are treated as internal: they read as `undefined`, they are left out of `Object.keys`, and `in` checks return `false`. Pass `{ showInternal: true }` to expose them.
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
const proxy = createMagicProxy({ __scalar_meta: 'hidden' }, { showInternal: true })
|
|
598
|
+
|
|
599
|
+
// 'hidden'
|
|
600
|
+
console.log(proxy.__scalar_meta)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Helpers
|
|
604
|
+
|
|
605
|
+
Standalone helpers, each with its own entry point.
|
|
606
|
+
|
|
607
|
+
| Helper | Description |
|
|
608
|
+
| --- | --- |
|
|
609
|
+
| `@scalar/json-magic/helpers/escape-json-pointer` | Escape `~` and `/` in a JSON pointer segment |
|
|
610
|
+
| `@scalar/json-magic/helpers/unescape-json-pointer` | Reverse of `escapeJsonPointer` |
|
|
611
|
+
| `@scalar/json-magic/helpers/get-segments-from-path` | Split a JSON pointer into unescaped segments |
|
|
612
|
+
| `@scalar/json-magic/helpers/get-value-by-path` | Read the value at a list of path segments |
|
|
613
|
+
| `@scalar/json-magic/helpers/set-value-at-path` | Write a value at a JSON pointer, creating missing nodes |
|
|
614
|
+
| `@scalar/json-magic/helpers/is-file-path` | Check whether a string looks like a file path |
|
|
615
|
+
| `@scalar/json-magic/helpers/is-http-url` | Check whether a string is an HTTP or HTTPS URL |
|
|
616
|
+
| `@scalar/json-magic/helpers/is-json-object` | Check whether a string is a JSON object |
|
|
617
|
+
| `@scalar/json-magic/helpers/is-yaml` | Check whether a string is valid YAML |
|
|
618
|
+
| `@scalar/json-magic/helpers/normalize` | Parse a JSON or YAML string into an object |
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
import { getValueByPath } from '@scalar/json-magic/helpers/get-value-by-path'
|
|
622
|
+
|
|
623
|
+
const { value } = getValueByPath({ components: { schemas: { User: { type: 'object' } } } }, [
|
|
624
|
+
'components',
|
|
625
|
+
'schemas',
|
|
626
|
+
'User',
|
|
627
|
+
])
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## Community
|
|
631
|
+
|
|
632
|
+
We are API nerds. You too? Let's chat on Discord: <https://discord.gg/scalar>
|
|
633
|
+
|
|
634
|
+
## License
|
|
635
|
+
|
|
636
|
+
The source code in this repository is licensed under [MIT](https://github.com/scalar/scalar/blob/main/LICENSE).
|