@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.
- package/.turbo/turbo-build.log +4 -3
- package/CHANGELOG.md +28 -0
- package/README.md +21 -3
- package/dist/bundle/bundle.d.ts +84 -14
- package/dist/bundle/bundle.d.ts.map +1 -1
- package/dist/bundle/bundle.js +60 -15
- package/dist/bundle/bundle.js.map +3 -3
- package/dist/bundle/index.d.ts +2 -1
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js.map +2 -2
- package/dist/bundle/plugins/fetch-urls/index.d.ts +2 -2
- package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
- package/dist/bundle/plugins/fetch-urls/index.js +1 -0
- package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
- package/dist/bundle/plugins/parse-json/index.d.ts +2 -2
- package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
- package/dist/bundle/plugins/parse-json/index.js +1 -0
- package/dist/bundle/plugins/parse-json/index.js.map +2 -2
- package/dist/bundle/plugins/parse-yaml/index.d.ts +2 -2
- package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
- package/dist/bundle/plugins/parse-yaml/index.js +1 -0
- package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
- package/dist/bundle/plugins/read-files/index.d.ts +2 -2
- package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
- package/dist/bundle/plugins/read-files/index.js +1 -0
- package/dist/bundle/plugins/read-files/index.js.map +2 -2
- package/dist/diff/apply.d.ts +1 -1
- package/dist/diff/apply.d.ts.map +1 -1
- package/dist/diff/apply.js.map +2 -2
- package/dist/diff/diff.d.ts +2 -2
- package/dist/diff/diff.d.ts.map +1 -1
- package/dist/diff/diff.js.map +2 -2
- package/dist/diff/merge.d.ts +3 -3
- package/dist/diff/merge.d.ts.map +1 -1
- package/dist/diff/merge.js.map +2 -2
- package/dist/magic-proxy/proxy.d.ts +23 -42
- package/dist/magic-proxy/proxy.d.ts.map +1 -1
- package/dist/magic-proxy/proxy.js +103 -80
- package/dist/magic-proxy/proxy.js.map +3 -3
- package/dist/utils/is-object.d.ts +1 -1
- package/dist/utils/is-object.d.ts.map +1 -1
- package/dist/utils/is-object.js.map +2 -2
- package/package.json +11 -10
- package/src/bundle/bundle.test.ts +591 -47
- package/src/bundle/bundle.ts +173 -32
- package/src/bundle/index.ts +2 -1
- package/src/bundle/plugins/fetch-urls/index.ts +3 -2
- package/src/bundle/plugins/parse-json/index.ts +3 -2
- package/src/bundle/plugins/parse-yaml/index.ts +3 -2
- package/src/bundle/plugins/read-files/index.ts +4 -2
- package/src/dereference/dereference.test.ts +26 -18
- package/src/diff/apply.ts +8 -3
- package/src/diff/diff.ts +3 -3
- package/src/diff/merge.ts +6 -6
- package/src/magic-proxy/proxy.test.ts +1095 -100
- package/src/magic-proxy/proxy.ts +150 -171
- package/src/utils/is-object.ts +1 -1
package/src/bundle/bundle.ts
CHANGED
|
@@ -77,7 +77,7 @@ export type ResolveResult = { ok: true; data: unknown } | { ok: false }
|
|
|
77
77
|
* // No matching plugin returns { ok: false }
|
|
78
78
|
* await resolveContents('#/components/schemas/User', [urlPlugin, filePlugin])
|
|
79
79
|
*/
|
|
80
|
-
async function resolveContents(value: string, plugins:
|
|
80
|
+
async function resolveContents(value: string, plugins: LoaderPlugin[]): Promise<ResolveResult> {
|
|
81
81
|
const plugin = plugins.find((p) => p.validate(value))
|
|
82
82
|
|
|
83
83
|
if (plugin) {
|
|
@@ -206,6 +206,12 @@ function resolveReferencePath(base: string, relativePath: string) {
|
|
|
206
206
|
if (isRemoteUrl(base)) {
|
|
207
207
|
const url = new URL(base)
|
|
208
208
|
|
|
209
|
+
// If the url stars with a / we want it to resolve from the origin so we replace the pathname
|
|
210
|
+
if (relativePath.startsWith('/')) {
|
|
211
|
+
url.pathname = relativePath
|
|
212
|
+
return url.toString()
|
|
213
|
+
}
|
|
214
|
+
|
|
209
215
|
const mergedPath = path.join(path.dirname(url.pathname), relativePath)
|
|
210
216
|
return new URL(mergedPath, base).toString()
|
|
211
217
|
}
|
|
@@ -360,22 +366,68 @@ const resolveAndCopyReferences = (
|
|
|
360
366
|
}
|
|
361
367
|
|
|
362
368
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* the
|
|
369
|
+
* A loader plugin for resolving external references during bundling.
|
|
370
|
+
* Loader plugins are responsible for handling specific types of external references,
|
|
371
|
+
* such as files, URLs, or custom protocols. Each loader plugin must provide:
|
|
372
|
+
*
|
|
373
|
+
* - A `validate` function to determine if the plugin can handle a given reference string.
|
|
374
|
+
* - An `exec` function to asynchronously fetch and resolve the referenced data,
|
|
375
|
+
* returning a Promise that resolves to a `ResolveResult`.
|
|
368
376
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
377
|
+
* Loader plugins enable extensible support for different reference sources in the bundler.
|
|
378
|
+
*
|
|
379
|
+
* @property type - The plugin type, always 'loader' for loader plugins.
|
|
380
|
+
* @property validate - Function to check if the plugin can handle a given reference value.
|
|
381
|
+
* @property exec - Function to fetch and resolve the reference, returning the resolved data.
|
|
371
382
|
*/
|
|
372
|
-
export type
|
|
373
|
-
|
|
383
|
+
export type LoaderPlugin = {
|
|
384
|
+
type: 'loader'
|
|
385
|
+
// Returns true if this plugin can handle the given reference value
|
|
374
386
|
validate: (value: string) => boolean
|
|
375
|
-
//
|
|
387
|
+
// Asynchronously fetches and resolves the reference, returning the resolved data
|
|
376
388
|
exec: (value: string) => Promise<ResolveResult>
|
|
377
389
|
}
|
|
378
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Context information for a node during traversal or processing.
|
|
393
|
+
*
|
|
394
|
+
* Note: The `path` parameter represents the path to the current node being processed.
|
|
395
|
+
* If you are performing a partial bundle (i.e., providing a custom root), this path will be relative
|
|
396
|
+
* to the root you provide, not the absolute root of the original document. You may need to prefix
|
|
397
|
+
* it with your own base path if you want to construct a full path from the absolute document root.
|
|
398
|
+
*
|
|
399
|
+
* - `path`: The JSON pointer path (as an array of strings) from the document root to the current node.
|
|
400
|
+
* - `resolutionCache`: A cache for storing promises of resolved references.
|
|
401
|
+
*/
|
|
402
|
+
type NodeProcessContext = {
|
|
403
|
+
path: readonly string[]
|
|
404
|
+
resolutionCache: Map<string, Promise<Readonly<ResolveResult>>>
|
|
405
|
+
parentNode: UnknownObject | null
|
|
406
|
+
rootNode: UnknownObject
|
|
407
|
+
loaders: LoaderPlugin[]
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* A plugin type for lifecycle hooks, allowing custom logic to be injected into the bundler's process.
|
|
412
|
+
* This type extends the Config['hooks'] interface and is identified by type: 'lifecycle'.
|
|
413
|
+
*/
|
|
414
|
+
export type LifecyclePlugin = { type: 'lifecycle' } & Config['hooks']
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Represents a plugin used by the bundler for extensibility.
|
|
418
|
+
*
|
|
419
|
+
* Plugins can be either:
|
|
420
|
+
* - Loader plugins: Responsible for resolving and loading external references (e.g., from files, URLs, or custom sources).
|
|
421
|
+
* - Lifecycle plugins: Provide lifecycle hooks to customize or extend the bundling process.
|
|
422
|
+
*
|
|
423
|
+
* Loader plugins must implement:
|
|
424
|
+
* - `validate`: Checks if the plugin can handle a given reference value.
|
|
425
|
+
* - `exec`: Asynchronously resolves and returns the referenced data.
|
|
426
|
+
*
|
|
427
|
+
* Lifecycle plugins extend the bundler's lifecycle hooks for custom logic.
|
|
428
|
+
*/
|
|
429
|
+
export type Plugin = LoaderPlugin | LifecyclePlugin
|
|
430
|
+
|
|
379
431
|
/**
|
|
380
432
|
* Configuration options for the bundler.
|
|
381
433
|
* Controls how external references are resolved and processed during bundling.
|
|
@@ -402,6 +454,13 @@ type Config = {
|
|
|
402
454
|
*/
|
|
403
455
|
depth?: number
|
|
404
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Optional origin path for the bundler.
|
|
459
|
+
* Used to resolve relative paths in references, especially when the input is a string URL or file path.
|
|
460
|
+
* If not provided, the bundler will use the input value as the origin.
|
|
461
|
+
*/
|
|
462
|
+
origin?: string
|
|
463
|
+
|
|
405
464
|
/**
|
|
406
465
|
* Optional cache to store promises of resolved references.
|
|
407
466
|
* Helps avoid duplicate fetches/reads of the same resource by storing
|
|
@@ -441,12 +500,31 @@ type Config = {
|
|
|
441
500
|
* Allows tracking the progress and status of reference resolution.
|
|
442
501
|
*/
|
|
443
502
|
hooks?: Partial<{
|
|
444
|
-
/**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Optional hook called when the bundler starts resolving a $ref.
|
|
505
|
+
* Useful for tracking or logging the beginning of a reference resolution.
|
|
506
|
+
*/
|
|
507
|
+
onResolveStart: (node: UnknownObject & Record<'$ref', unknown>) => void
|
|
508
|
+
/**
|
|
509
|
+
* Optional hook called when the bundler fails to resolve a $ref.
|
|
510
|
+
* Can be used for error handling, logging, or custom error reporting.
|
|
511
|
+
*/
|
|
512
|
+
onResolveError: (node: UnknownObject & Record<'$ref', unknown>) => void
|
|
513
|
+
/**
|
|
514
|
+
* Optional hook called when the bundler successfully resolves a $ref.
|
|
515
|
+
* Useful for tracking successful resolutions or custom post-processing.
|
|
516
|
+
*/
|
|
517
|
+
onResolveSuccess: (node: UnknownObject & Record<'$ref', unknown>) => void
|
|
518
|
+
/**
|
|
519
|
+
* Optional hook invoked before processing a node.
|
|
520
|
+
* Can be used for preprocessing, mutation, or custom logic before the node is handled by the bundler.
|
|
521
|
+
*/
|
|
522
|
+
onBeforeNodeProcess: (node: UnknownObject, context: NodeProcessContext) => void | Promise<void>
|
|
523
|
+
/**
|
|
524
|
+
* Optional hook invoked after processing a node.
|
|
525
|
+
* Useful for postprocessing, cleanup, or custom logic after the node has been handled by the bundler.
|
|
526
|
+
*/
|
|
527
|
+
onAfterNodeProcess: (node: UnknownObject, context: NodeProcessContext) => void | Promise<void>
|
|
450
528
|
}>
|
|
451
529
|
}
|
|
452
530
|
|
|
@@ -541,6 +619,9 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
541
619
|
// to avoid duplicate fetches/reads of the same resource
|
|
542
620
|
const cache = config.cache ?? new Map<string, Promise<ResolveResult>>()
|
|
543
621
|
|
|
622
|
+
const loaderPlugins = config.plugins.filter((it) => it.type === 'loader')
|
|
623
|
+
const lifecyclePlugin = config.plugins.filter((it) => it.type === 'lifecycle')
|
|
624
|
+
|
|
544
625
|
/**
|
|
545
626
|
* Resolves the input value by either returning it directly if it's not a string,
|
|
546
627
|
* or attempting to resolve it using the provided plugins if it is a string.
|
|
@@ -550,7 +631,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
550
631
|
if (typeof input !== 'string') {
|
|
551
632
|
return input
|
|
552
633
|
}
|
|
553
|
-
const result = await resolveContents(input,
|
|
634
|
+
const result = await resolveContents(input, loaderPlugins)
|
|
554
635
|
|
|
555
636
|
if (result.ok && typeof result.data === 'object') {
|
|
556
637
|
return result.data
|
|
@@ -583,6 +664,10 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
583
664
|
// For string inputs that are URLs or file paths, uses the input as the origin.
|
|
584
665
|
// For non-string inputs or other string types, returns an empty string.
|
|
585
666
|
const defaultOrigin = () => {
|
|
667
|
+
if (config.origin) {
|
|
668
|
+
return config.origin
|
|
669
|
+
}
|
|
670
|
+
|
|
586
671
|
if (typeof input !== 'string') {
|
|
587
672
|
return ''
|
|
588
673
|
}
|
|
@@ -603,7 +688,14 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
603
688
|
documentRoot[extensions.externalDocumentsMappings],
|
|
604
689
|
)
|
|
605
690
|
|
|
606
|
-
const bundler = async (
|
|
691
|
+
const bundler = async (
|
|
692
|
+
root: unknown,
|
|
693
|
+
origin: string = defaultOrigin(),
|
|
694
|
+
isChunkParent = false,
|
|
695
|
+
depth = 0,
|
|
696
|
+
path: readonly string[] = [],
|
|
697
|
+
parent: UnknownObject = null,
|
|
698
|
+
) => {
|
|
607
699
|
// If a maximum depth is set in the config, stop bundling when the current depth reaches or exceeds it
|
|
608
700
|
if (config.depth !== undefined && depth > config.depth) {
|
|
609
701
|
return
|
|
@@ -621,22 +713,39 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
621
713
|
// Mark this node as processed before continuing
|
|
622
714
|
processedNodes.add(root)
|
|
623
715
|
|
|
716
|
+
// Invoke the onBeforeNodeProcess hook for the current node before any further processing
|
|
717
|
+
await config.hooks?.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
718
|
+
path,
|
|
719
|
+
resolutionCache: cache,
|
|
720
|
+
parentNode: parent,
|
|
721
|
+
rootNode: documentRoot as UnknownObject,
|
|
722
|
+
loaders: loaderPlugins,
|
|
723
|
+
})
|
|
724
|
+
// Invoke onBeforeNodeProcess hooks from all registered lifecycle plugins
|
|
725
|
+
for (const plugin of lifecyclePlugin) {
|
|
726
|
+
await plugin.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
727
|
+
path,
|
|
728
|
+
resolutionCache: cache,
|
|
729
|
+
parentNode: parent,
|
|
730
|
+
rootNode: documentRoot as UnknownObject,
|
|
731
|
+
loaders: loaderPlugins,
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
|
|
624
735
|
if (typeof root === 'object' && '$ref' in root && typeof root['$ref'] === 'string') {
|
|
625
736
|
const ref = root['$ref']
|
|
626
737
|
const isChunk = '$global' in root && typeof root['$global'] === 'boolean' && root['$global']
|
|
627
738
|
|
|
628
739
|
if (isLocalRef(ref)) {
|
|
629
740
|
if (isPartialBundling) {
|
|
741
|
+
const segments = getSegmentsFromPath(ref.substring(1))
|
|
742
|
+
const parent = segments.length > 0 ? getNestedValue(documentRoot, segments.slice(0, -1)) : undefined
|
|
743
|
+
|
|
630
744
|
// When doing partial bundling, we need to recursively bundle all dependencies
|
|
631
745
|
// referenced by this local reference to ensure the partial bundle is complete.
|
|
632
746
|
// This includes not just the direct reference but also all its dependencies,
|
|
633
747
|
// creating a complete and self-contained partial bundle.
|
|
634
|
-
await bundler(
|
|
635
|
-
getNestedValue(documentRoot, getSegmentsFromPath(ref.substring(1))),
|
|
636
|
-
origin,
|
|
637
|
-
isChunkParent,
|
|
638
|
-
depth + 1,
|
|
639
|
-
)
|
|
748
|
+
await bundler(getNestedValue(documentRoot, segments), origin, isChunkParent, depth + 1, segments, parent)
|
|
640
749
|
}
|
|
641
750
|
return
|
|
642
751
|
}
|
|
@@ -655,10 +764,11 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
655
764
|
const seen = cache.has(resolvedPath)
|
|
656
765
|
|
|
657
766
|
if (!seen) {
|
|
658
|
-
cache.set(resolvedPath, resolveContents(resolvedPath,
|
|
767
|
+
cache.set(resolvedPath, resolveContents(resolvedPath, loaderPlugins))
|
|
659
768
|
}
|
|
660
769
|
|
|
661
770
|
config?.hooks?.onResolveStart?.(root)
|
|
771
|
+
lifecyclePlugin.forEach((it) => it.onResolveStart?.(root))
|
|
662
772
|
|
|
663
773
|
// Resolve the remote document
|
|
664
774
|
const result = await cache.get(resolvedPath)
|
|
@@ -686,7 +796,11 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
686
796
|
// to handle any nested references it may contain. We pass the resolvedPath as the new origin
|
|
687
797
|
// to ensure any relative references within this content are resolved correctly relative to
|
|
688
798
|
// their new location in the bundled document.
|
|
689
|
-
await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1
|
|
799
|
+
await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1, [
|
|
800
|
+
extensions.externalDocuments,
|
|
801
|
+
compressedPath,
|
|
802
|
+
documentRoot[extensions.externalDocumentsMappings],
|
|
803
|
+
])
|
|
690
804
|
|
|
691
805
|
// Store the mapping between hashed keys and original URLs in x-ext-urls
|
|
692
806
|
// This allows tracking which external URLs were bundled and their corresponding locations
|
|
@@ -721,28 +835,55 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
721
835
|
// This is necessary because we need to maintain the correct path context
|
|
722
836
|
// for the embedded document while preserving its internal structure
|
|
723
837
|
root.$ref = prefixInternalRef(`#${path}`, [extensions.externalDocuments, compressedPath])
|
|
838
|
+
|
|
724
839
|
config?.hooks?.onResolveSuccess?.(root)
|
|
840
|
+
lifecyclePlugin.forEach((it) => it.onResolveSuccess?.(root))
|
|
841
|
+
|
|
725
842
|
return
|
|
726
843
|
}
|
|
727
844
|
|
|
728
845
|
config?.hooks?.onResolveError?.(root)
|
|
846
|
+
lifecyclePlugin.forEach((it) => it.onResolveError?.(root))
|
|
847
|
+
|
|
729
848
|
return console.warn(
|
|
730
849
|
`Failed to resolve external reference "${resolvedPath}". The reference may be invalid, inaccessible, or missing a loader for this type of reference.`,
|
|
731
850
|
)
|
|
732
851
|
}
|
|
733
852
|
|
|
734
|
-
// Recursively
|
|
735
|
-
// This ensures
|
|
736
|
-
// We skip
|
|
853
|
+
// Recursively traverse all child properties of the current object to resolve nested $ref references.
|
|
854
|
+
// This step ensures that any $refs located deeper within the object hierarchy are discovered and processed.
|
|
855
|
+
// We explicitly skip the extension keys (x-ext and x-ext-urls) to avoid reprocessing already bundled or mapped content.
|
|
737
856
|
await Promise.all(
|
|
738
857
|
Object.entries(root).map(async ([key, value]) => {
|
|
739
|
-
if (key === extensions.externalDocuments) {
|
|
858
|
+
if (key === extensions.externalDocuments || key === extensions.externalDocumentsMappings) {
|
|
740
859
|
return
|
|
741
860
|
}
|
|
742
861
|
|
|
743
|
-
await bundler(value, origin, isChunkParent, depth + 1)
|
|
862
|
+
await bundler(value, origin, isChunkParent, depth + 1, [...path, key], root as UnknownObject)
|
|
744
863
|
}),
|
|
745
864
|
)
|
|
865
|
+
|
|
866
|
+
// Invoke the optional onAfterNodeProcess hook from the config, if provided.
|
|
867
|
+
// This allows for custom post-processing logic after a node has been handled by the bundler.
|
|
868
|
+
await config.hooks?.onAfterNodeProcess?.(root as UnknownObject, {
|
|
869
|
+
path,
|
|
870
|
+
resolutionCache: cache,
|
|
871
|
+
parentNode: parent,
|
|
872
|
+
rootNode: documentRoot as UnknownObject,
|
|
873
|
+
loaders: loaderPlugins,
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
// Iterate through all lifecycle plugins and invoke their onAfterNodeProcess hooks, if defined.
|
|
877
|
+
// This enables plugins to perform additional post-processing or cleanup after the node is processed.
|
|
878
|
+
for (const plugin of lifecyclePlugin) {
|
|
879
|
+
await plugin.onAfterNodeProcess?.(root as UnknownObject, {
|
|
880
|
+
path,
|
|
881
|
+
resolutionCache: cache,
|
|
882
|
+
parentNode: parent,
|
|
883
|
+
rootNode: documentRoot as UnknownObject,
|
|
884
|
+
loaders: loaderPlugins,
|
|
885
|
+
})
|
|
886
|
+
}
|
|
746
887
|
}
|
|
747
888
|
|
|
748
889
|
await bundler(rawSpecification)
|
package/src/bundle/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalize } from '@/utils/normalize'
|
|
2
2
|
import { createLimiter } from '@/bundle/create-limiter'
|
|
3
|
-
import type {
|
|
3
|
+
import type { LoaderPlugin, ResolveResult } from '@/bundle'
|
|
4
4
|
import { isRemoteUrl } from '@/bundle/bundle'
|
|
5
5
|
|
|
6
6
|
type FetchConfig = Partial<{
|
|
@@ -83,11 +83,12 @@ export async function fetchUrl(
|
|
|
83
83
|
* const result = await urlPlugin.exec('https://example.com/schema.json')
|
|
84
84
|
* }
|
|
85
85
|
*/
|
|
86
|
-
export function fetchUrls(config?: FetchConfig & Partial<{ limit: number | null }>):
|
|
86
|
+
export function fetchUrls(config?: FetchConfig & Partial<{ limit: number | null }>): LoaderPlugin {
|
|
87
87
|
// If there is a limit specified we limit the number of concurrent calls
|
|
88
88
|
const limiter = config?.limit ? createLimiter(config.limit) : <T>(fn: () => Promise<T>) => fn()
|
|
89
89
|
|
|
90
90
|
return {
|
|
91
|
+
type: 'loader',
|
|
91
92
|
validate: isRemoteUrl,
|
|
92
93
|
exec: (value) => fetchUrl(value, limiter, config),
|
|
93
94
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isJsonObject } from '@/utils/is-json-object'
|
|
2
|
-
import type {
|
|
2
|
+
import type { LoaderPlugin, ResolveResult } from '@/bundle'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates a plugin that parses JSON strings into JavaScript objects.
|
|
@@ -11,8 +11,9 @@ import type { Plugin, ResolveResult } from '@/bundle'
|
|
|
11
11
|
* // result = { name: 'John', age: 30 }
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
|
-
export function parseJson():
|
|
14
|
+
export function parseJson(): LoaderPlugin {
|
|
15
15
|
return {
|
|
16
|
+
type: 'loader',
|
|
16
17
|
validate: isJsonObject,
|
|
17
18
|
exec: async (value): Promise<ResolveResult> => {
|
|
18
19
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LoaderPlugin, ResolveResult } from '@/bundle'
|
|
2
2
|
import { isYaml } from '@/utils/is-yaml'
|
|
3
3
|
import YAML from 'yaml'
|
|
4
4
|
|
|
@@ -12,8 +12,9 @@ import YAML from 'yaml'
|
|
|
12
12
|
* // result = { name: 'John', age: 30 }
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
|
-
export function parseYaml():
|
|
15
|
+
export function parseYaml(): LoaderPlugin {
|
|
16
16
|
return {
|
|
17
|
+
type: 'loader',
|
|
17
18
|
validate: isYaml,
|
|
18
19
|
exec: async (value): Promise<ResolveResult> => {
|
|
19
20
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalize } from '@/utils/normalize'
|
|
2
|
-
import {
|
|
2
|
+
import type { LoaderPlugin, ResolveResult } from '@/bundle'
|
|
3
|
+
import { isFilePath } from '@/bundle/bundle'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Reads and normalizes data from a local file
|
|
@@ -47,8 +48,9 @@ export async function readFile(path: string): Promise<ResolveResult> {
|
|
|
47
48
|
* const result = await filePlugin.exec('./local-schema.json')
|
|
48
49
|
* }
|
|
49
50
|
*/
|
|
50
|
-
export function readFiles():
|
|
51
|
+
export function readFiles(): LoaderPlugin {
|
|
51
52
|
return {
|
|
53
|
+
type: 'loader',
|
|
52
54
|
validate: isFilePath,
|
|
53
55
|
exec: readFile,
|
|
54
56
|
}
|
|
@@ -37,18 +37,22 @@ describe('dereference', () => {
|
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
39
|
profile: {
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
'$ref': '#/users',
|
|
41
|
+
'$ref-value': {
|
|
42
|
+
name: 'John Doe',
|
|
43
|
+
age: 30,
|
|
44
|
+
address: {
|
|
45
|
+
city: 'New York',
|
|
46
|
+
street: '5th Avenue',
|
|
47
|
+
},
|
|
46
48
|
},
|
|
47
49
|
},
|
|
48
50
|
address: {
|
|
49
|
-
'
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
'$ref': '#/users/address',
|
|
52
|
+
'$ref-value': {
|
|
53
|
+
city: 'New York',
|
|
54
|
+
street: '5th Avenue',
|
|
55
|
+
},
|
|
52
56
|
},
|
|
53
57
|
},
|
|
54
58
|
})
|
|
@@ -97,18 +101,22 @@ describe('dereference', () => {
|
|
|
97
101
|
success: true,
|
|
98
102
|
data: {
|
|
99
103
|
profile: {
|
|
100
|
-
'
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
'$ref': '#/x-ext/5bd1cdd',
|
|
105
|
+
'$ref-value': {
|
|
106
|
+
name: 'Jane Doe',
|
|
107
|
+
age: 25,
|
|
108
|
+
address: {
|
|
109
|
+
city: 'Los Angeles',
|
|
110
|
+
street: 'Sunset Boulevard',
|
|
111
|
+
},
|
|
106
112
|
},
|
|
107
113
|
},
|
|
108
114
|
address: {
|
|
109
|
-
'
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
'$ref': '#/x-ext/5bd1cdd/address',
|
|
116
|
+
'$ref-value': {
|
|
117
|
+
city: 'Los Angeles',
|
|
118
|
+
street: 'Sunset Boulevard',
|
|
119
|
+
},
|
|
112
120
|
},
|
|
113
121
|
'x-ext': {
|
|
114
122
|
'5bd1cdd': userProfile,
|
package/src/diff/apply.ts
CHANGED
|
@@ -36,9 +36,12 @@ export class InvalidChangesDetectedError extends Error {
|
|
|
36
36
|
* const updated = apply(original, changes)
|
|
37
37
|
* // Result: original document with content added to the 200 response
|
|
38
38
|
*/
|
|
39
|
-
export const apply =
|
|
39
|
+
export const apply = <T extends Record<string, unknown>>(
|
|
40
|
+
document: Record<string, unknown>,
|
|
41
|
+
diff: Difference<T>[],
|
|
42
|
+
): T => {
|
|
40
43
|
// Traverse the object and apply the change
|
|
41
|
-
const applyChange = (current: any, path: string[], d: Difference
|
|
44
|
+
const applyChange = (current: any, path: string[], d: Difference<T>, depth = 0) => {
|
|
42
45
|
if (path[depth] === undefined) {
|
|
43
46
|
throw new InvalidChangesDetectedError(
|
|
44
47
|
`Process aborted. Path ${path.join('.')} at depth ${depth} is undefined, check diff object`,
|
|
@@ -74,5 +77,7 @@ export const apply = (document: Record<string, unknown>, diff: Difference[]): Re
|
|
|
74
77
|
applyChange(document, d.path, d)
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
// It is safe to cast here because this function mutates the input document
|
|
81
|
+
// to match the target type T as described by the diff changeset.
|
|
82
|
+
return document as T
|
|
78
83
|
}
|
package/src/diff/diff.ts
CHANGED
|
@@ -12,7 +12,7 @@ type ChangeType = 'add' | 'update' | 'delete'
|
|
|
12
12
|
* @property changes - The new value for the property (for add/update) or the old value (for delete)
|
|
13
13
|
* @property type - The type of change that occurred
|
|
14
14
|
*/
|
|
15
|
-
export type Difference = { path: string[]; changes: any; type: ChangeType }
|
|
15
|
+
export type Difference<_T> = { path: string[]; changes: any; type: ChangeType }
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Get the difference between two objects.
|
|
@@ -49,8 +49,8 @@ export type Difference = { path: string[]; changes: any; type: ChangeType }
|
|
|
49
49
|
* // { path: ['user', 'settings', 'theme'], changes: 'dark', type: 'update' }
|
|
50
50
|
* // ]
|
|
51
51
|
*/
|
|
52
|
-
export const diff =
|
|
53
|
-
const diff: Difference[] = []
|
|
52
|
+
export const diff = <T extends Record<string, unknown>>(doc1: Record<string, unknown>, doc2: T) => {
|
|
53
|
+
const diff: Difference<T>[] = []
|
|
54
54
|
|
|
55
55
|
const bfs = (el1: unknown, el2: unknown, prefix = []) => {
|
|
56
56
|
// If the types are different, we know that the property has been added, deleted or updated
|
package/src/diff/merge.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { isArrayEqual, isKeyCollisions, mergeObjects } from '@/diff/utils'
|
|
|
39
39
|
* // ]
|
|
40
40
|
* // }
|
|
41
41
|
*/
|
|
42
|
-
export const merge = (diff1: Difference[], diff2: Difference[]) => {
|
|
42
|
+
export const merge = <T>(diff1: Difference<T>[], diff2: Difference<T>[]) => {
|
|
43
43
|
// Here we need to use a trie to optimize searching for a prefix
|
|
44
44
|
// With the naive approach time complexity of the algorithm would be
|
|
45
45
|
// O(n * m)
|
|
@@ -49,7 +49,7 @@ export const merge = (diff1: Difference[], diff2: Difference[]) => {
|
|
|
49
49
|
// Assuming that the maximum depth of the nested objects would be constant lets say 0 <= D <= 100
|
|
50
50
|
// we try to optimize for that using the tire data structure.
|
|
51
51
|
// So the new time complexity would be O(n * D) where D is the maximum depth of the nested object
|
|
52
|
-
const trie = new Trie<{ index: number; changes: Difference }>()
|
|
52
|
+
const trie = new Trie<{ index: number; changes: Difference<T> }>()
|
|
53
53
|
|
|
54
54
|
// Create the trie
|
|
55
55
|
for (const [index, diff] of diff1.entries()) {
|
|
@@ -62,10 +62,10 @@ export const merge = (diff1: Difference[], diff2: Difference[]) => {
|
|
|
62
62
|
// Keep related conflicts together for easy A, B pick conflict resolution
|
|
63
63
|
// map key is going to be conflicting index of first diff list where the diff will be
|
|
64
64
|
// a delete operation or an add/update operation with a one to many conflicts
|
|
65
|
-
const conflictsMap1 = new Map<number, [Difference[], Difference[]]>()
|
|
65
|
+
const conflictsMap1 = new Map<number, [Difference<T>[], Difference<T>[]]>()
|
|
66
66
|
// map key will be the index from the second diff list where the diff will be
|
|
67
67
|
// a delete operation with one to many conflicts
|
|
68
|
-
const conflictsMap2 = new Map<number, [Difference[], Difference[]]>()
|
|
68
|
+
const conflictsMap2 = new Map<number, [Difference<T>[], Difference<T>[]]>()
|
|
69
69
|
|
|
70
70
|
for (const [index, diff] of diff2.entries()) {
|
|
71
71
|
trie.findMatch(diff.path, (value) => {
|
|
@@ -123,11 +123,11 @@ export const merge = (diff1: Difference[], diff2: Difference[]) => {
|
|
|
123
123
|
})
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const conflicts: [Difference[], Difference[]][] = [...conflictsMap1.values(), ...conflictsMap2.values()]
|
|
126
|
+
const conflicts: [Difference<T>[], Difference<T>[]][] = [...conflictsMap1.values(), ...conflictsMap2.values()]
|
|
127
127
|
|
|
128
128
|
// Filter all changes that should be skipped because of conflicts
|
|
129
129
|
// or auto conflict resolution
|
|
130
|
-
const diffs: Difference[] = [
|
|
130
|
+
const diffs: Difference<T>[] = [
|
|
131
131
|
...diff1.filter((_, index) => !skipDiff1.has(index)),
|
|
132
132
|
...diff2.filter((_, index) => !skipDiff2.has(index)),
|
|
133
133
|
]
|