@scalar/json-magic 0.1.0 → 0.3.0
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 +22 -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 +56 -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 +594 -44
- package/src/bundle/bundle.ts +167 -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) {
|
|
@@ -360,22 +360,68 @@ const resolveAndCopyReferences = (
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* if it can handle a specific reference, and an execution function to perform
|
|
367
|
-
* the actual resolution.
|
|
363
|
+
* A loader plugin for resolving external references during bundling.
|
|
364
|
+
* Loader plugins are responsible for handling specific types of external references,
|
|
365
|
+
* such as files, URLs, or custom protocols. Each loader plugin must provide:
|
|
368
366
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
367
|
+
* - A `validate` function to determine if the plugin can handle a given reference string.
|
|
368
|
+
* - An `exec` function to asynchronously fetch and resolve the referenced data,
|
|
369
|
+
* returning a Promise that resolves to a `ResolveResult`.
|
|
370
|
+
*
|
|
371
|
+
* Loader plugins enable extensible support for different reference sources in the bundler.
|
|
372
|
+
*
|
|
373
|
+
* @property type - The plugin type, always 'loader' for loader plugins.
|
|
374
|
+
* @property validate - Function to check if the plugin can handle a given reference value.
|
|
375
|
+
* @property exec - Function to fetch and resolve the reference, returning the resolved data.
|
|
371
376
|
*/
|
|
372
|
-
export type
|
|
373
|
-
|
|
377
|
+
export type LoaderPlugin = {
|
|
378
|
+
type: 'loader'
|
|
379
|
+
// Returns true if this plugin can handle the given reference value
|
|
374
380
|
validate: (value: string) => boolean
|
|
375
|
-
//
|
|
381
|
+
// Asynchronously fetches and resolves the reference, returning the resolved data
|
|
376
382
|
exec: (value: string) => Promise<ResolveResult>
|
|
377
383
|
}
|
|
378
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Context information for a node during traversal or processing.
|
|
387
|
+
*
|
|
388
|
+
* Note: The `path` parameter represents the path to the current node being processed.
|
|
389
|
+
* If you are performing a partial bundle (i.e., providing a custom root), this path will be relative
|
|
390
|
+
* to the root you provide, not the absolute root of the original document. You may need to prefix
|
|
391
|
+
* it with your own base path if you want to construct a full path from the absolute document root.
|
|
392
|
+
*
|
|
393
|
+
* - `path`: The JSON pointer path (as an array of strings) from the document root to the current node.
|
|
394
|
+
* - `resolutionCache`: A cache for storing promises of resolved references.
|
|
395
|
+
*/
|
|
396
|
+
type NodeProcessContext = {
|
|
397
|
+
path: readonly string[]
|
|
398
|
+
resolutionCache: Map<string, Promise<Readonly<ResolveResult>>>
|
|
399
|
+
parentNode: UnknownObject | null
|
|
400
|
+
rootNode: UnknownObject
|
|
401
|
+
loaders: LoaderPlugin[]
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* A plugin type for lifecycle hooks, allowing custom logic to be injected into the bundler's process.
|
|
406
|
+
* This type extends the Config['hooks'] interface and is identified by type: 'lifecycle'.
|
|
407
|
+
*/
|
|
408
|
+
export type LifecyclePlugin = { type: 'lifecycle' } & Config['hooks']
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Represents a plugin used by the bundler for extensibility.
|
|
412
|
+
*
|
|
413
|
+
* Plugins can be either:
|
|
414
|
+
* - Loader plugins: Responsible for resolving and loading external references (e.g., from files, URLs, or custom sources).
|
|
415
|
+
* - Lifecycle plugins: Provide lifecycle hooks to customize or extend the bundling process.
|
|
416
|
+
*
|
|
417
|
+
* Loader plugins must implement:
|
|
418
|
+
* - `validate`: Checks if the plugin can handle a given reference value.
|
|
419
|
+
* - `exec`: Asynchronously resolves and returns the referenced data.
|
|
420
|
+
*
|
|
421
|
+
* Lifecycle plugins extend the bundler's lifecycle hooks for custom logic.
|
|
422
|
+
*/
|
|
423
|
+
export type Plugin = LoaderPlugin | LifecyclePlugin
|
|
424
|
+
|
|
379
425
|
/**
|
|
380
426
|
* Configuration options for the bundler.
|
|
381
427
|
* Controls how external references are resolved and processed during bundling.
|
|
@@ -402,6 +448,13 @@ type Config = {
|
|
|
402
448
|
*/
|
|
403
449
|
depth?: number
|
|
404
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Optional origin path for the bundler.
|
|
453
|
+
* Used to resolve relative paths in references, especially when the input is a string URL or file path.
|
|
454
|
+
* If not provided, the bundler will use the input value as the origin.
|
|
455
|
+
*/
|
|
456
|
+
origin?: string
|
|
457
|
+
|
|
405
458
|
/**
|
|
406
459
|
* Optional cache to store promises of resolved references.
|
|
407
460
|
* Helps avoid duplicate fetches/reads of the same resource by storing
|
|
@@ -441,12 +494,31 @@ type Config = {
|
|
|
441
494
|
* Allows tracking the progress and status of reference resolution.
|
|
442
495
|
*/
|
|
443
496
|
hooks?: Partial<{
|
|
444
|
-
/**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
497
|
+
/**
|
|
498
|
+
* Optional hook called when the bundler starts resolving a $ref.
|
|
499
|
+
* Useful for tracking or logging the beginning of a reference resolution.
|
|
500
|
+
*/
|
|
501
|
+
onResolveStart: (node: UnknownObject & Record<'$ref', unknown>) => void
|
|
502
|
+
/**
|
|
503
|
+
* Optional hook called when the bundler fails to resolve a $ref.
|
|
504
|
+
* Can be used for error handling, logging, or custom error reporting.
|
|
505
|
+
*/
|
|
506
|
+
onResolveError: (node: UnknownObject & Record<'$ref', unknown>) => void
|
|
507
|
+
/**
|
|
508
|
+
* Optional hook called when the bundler successfully resolves a $ref.
|
|
509
|
+
* Useful for tracking successful resolutions or custom post-processing.
|
|
510
|
+
*/
|
|
511
|
+
onResolveSuccess: (node: UnknownObject & Record<'$ref', unknown>) => void
|
|
512
|
+
/**
|
|
513
|
+
* Optional hook invoked before processing a node.
|
|
514
|
+
* Can be used for preprocessing, mutation, or custom logic before the node is handled by the bundler.
|
|
515
|
+
*/
|
|
516
|
+
onBeforeNodeProcess: (node: UnknownObject, context: NodeProcessContext) => void | Promise<void>
|
|
517
|
+
/**
|
|
518
|
+
* Optional hook invoked after processing a node.
|
|
519
|
+
* Useful for postprocessing, cleanup, or custom logic after the node has been handled by the bundler.
|
|
520
|
+
*/
|
|
521
|
+
onAfterNodeProcess: (node: UnknownObject, context: NodeProcessContext) => void | Promise<void>
|
|
450
522
|
}>
|
|
451
523
|
}
|
|
452
524
|
|
|
@@ -541,6 +613,9 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
541
613
|
// to avoid duplicate fetches/reads of the same resource
|
|
542
614
|
const cache = config.cache ?? new Map<string, Promise<ResolveResult>>()
|
|
543
615
|
|
|
616
|
+
const loaderPlugins = config.plugins.filter((it) => it.type === 'loader')
|
|
617
|
+
const lifecyclePlugin = config.plugins.filter((it) => it.type === 'lifecycle')
|
|
618
|
+
|
|
544
619
|
/**
|
|
545
620
|
* Resolves the input value by either returning it directly if it's not a string,
|
|
546
621
|
* or attempting to resolve it using the provided plugins if it is a string.
|
|
@@ -550,7 +625,7 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
550
625
|
if (typeof input !== 'string') {
|
|
551
626
|
return input
|
|
552
627
|
}
|
|
553
|
-
const result = await resolveContents(input,
|
|
628
|
+
const result = await resolveContents(input, loaderPlugins)
|
|
554
629
|
|
|
555
630
|
if (result.ok && typeof result.data === 'object') {
|
|
556
631
|
return result.data
|
|
@@ -583,6 +658,10 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
583
658
|
// For string inputs that are URLs or file paths, uses the input as the origin.
|
|
584
659
|
// For non-string inputs or other string types, returns an empty string.
|
|
585
660
|
const defaultOrigin = () => {
|
|
661
|
+
if (config.origin) {
|
|
662
|
+
return config.origin
|
|
663
|
+
}
|
|
664
|
+
|
|
586
665
|
if (typeof input !== 'string') {
|
|
587
666
|
return ''
|
|
588
667
|
}
|
|
@@ -603,7 +682,14 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
603
682
|
documentRoot[extensions.externalDocumentsMappings],
|
|
604
683
|
)
|
|
605
684
|
|
|
606
|
-
const bundler = async (
|
|
685
|
+
const bundler = async (
|
|
686
|
+
root: unknown,
|
|
687
|
+
origin: string = defaultOrigin(),
|
|
688
|
+
isChunkParent = false,
|
|
689
|
+
depth = 0,
|
|
690
|
+
path: readonly string[] = [],
|
|
691
|
+
parent: UnknownObject = null,
|
|
692
|
+
) => {
|
|
607
693
|
// If a maximum depth is set in the config, stop bundling when the current depth reaches or exceeds it
|
|
608
694
|
if (config.depth !== undefined && depth > config.depth) {
|
|
609
695
|
return
|
|
@@ -621,22 +707,39 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
621
707
|
// Mark this node as processed before continuing
|
|
622
708
|
processedNodes.add(root)
|
|
623
709
|
|
|
710
|
+
// Invoke the onBeforeNodeProcess hook for the current node before any further processing
|
|
711
|
+
await config.hooks?.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
712
|
+
path,
|
|
713
|
+
resolutionCache: cache,
|
|
714
|
+
parentNode: parent,
|
|
715
|
+
rootNode: documentRoot as UnknownObject,
|
|
716
|
+
loaders: loaderPlugins,
|
|
717
|
+
})
|
|
718
|
+
// Invoke onBeforeNodeProcess hooks from all registered lifecycle plugins
|
|
719
|
+
for (const plugin of lifecyclePlugin) {
|
|
720
|
+
await plugin.onBeforeNodeProcess?.(root as UnknownObject, {
|
|
721
|
+
path,
|
|
722
|
+
resolutionCache: cache,
|
|
723
|
+
parentNode: parent,
|
|
724
|
+
rootNode: documentRoot as UnknownObject,
|
|
725
|
+
loaders: loaderPlugins,
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
|
|
624
729
|
if (typeof root === 'object' && '$ref' in root && typeof root['$ref'] === 'string') {
|
|
625
730
|
const ref = root['$ref']
|
|
626
731
|
const isChunk = '$global' in root && typeof root['$global'] === 'boolean' && root['$global']
|
|
627
732
|
|
|
628
733
|
if (isLocalRef(ref)) {
|
|
629
734
|
if (isPartialBundling) {
|
|
735
|
+
const segments = getSegmentsFromPath(ref.substring(1))
|
|
736
|
+
const parent = segments.length > 0 ? getNestedValue(documentRoot, segments.slice(0, -1)) : undefined
|
|
737
|
+
|
|
630
738
|
// When doing partial bundling, we need to recursively bundle all dependencies
|
|
631
739
|
// referenced by this local reference to ensure the partial bundle is complete.
|
|
632
740
|
// This includes not just the direct reference but also all its dependencies,
|
|
633
741
|
// 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
|
-
)
|
|
742
|
+
await bundler(getNestedValue(documentRoot, segments), origin, isChunkParent, depth + 1, segments, parent)
|
|
640
743
|
}
|
|
641
744
|
return
|
|
642
745
|
}
|
|
@@ -655,10 +758,11 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
655
758
|
const seen = cache.has(resolvedPath)
|
|
656
759
|
|
|
657
760
|
if (!seen) {
|
|
658
|
-
cache.set(resolvedPath, resolveContents(resolvedPath,
|
|
761
|
+
cache.set(resolvedPath, resolveContents(resolvedPath, loaderPlugins))
|
|
659
762
|
}
|
|
660
763
|
|
|
661
764
|
config?.hooks?.onResolveStart?.(root)
|
|
765
|
+
lifecyclePlugin.forEach((it) => it.onResolveStart?.(root))
|
|
662
766
|
|
|
663
767
|
// Resolve the remote document
|
|
664
768
|
const result = await cache.get(resolvedPath)
|
|
@@ -686,7 +790,11 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
686
790
|
// to handle any nested references it may contain. We pass the resolvedPath as the new origin
|
|
687
791
|
// to ensure any relative references within this content are resolved correctly relative to
|
|
688
792
|
// their new location in the bundled document.
|
|
689
|
-
await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1
|
|
793
|
+
await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1, [
|
|
794
|
+
extensions.externalDocuments,
|
|
795
|
+
compressedPath,
|
|
796
|
+
documentRoot[extensions.externalDocumentsMappings],
|
|
797
|
+
])
|
|
690
798
|
|
|
691
799
|
// Store the mapping between hashed keys and original URLs in x-ext-urls
|
|
692
800
|
// This allows tracking which external URLs were bundled and their corresponding locations
|
|
@@ -721,28 +829,55 @@ export async function bundle(input: UnknownObject | string, config: Config) {
|
|
|
721
829
|
// This is necessary because we need to maintain the correct path context
|
|
722
830
|
// for the embedded document while preserving its internal structure
|
|
723
831
|
root.$ref = prefixInternalRef(`#${path}`, [extensions.externalDocuments, compressedPath])
|
|
832
|
+
|
|
724
833
|
config?.hooks?.onResolveSuccess?.(root)
|
|
834
|
+
lifecyclePlugin.forEach((it) => it.onResolveSuccess?.(root))
|
|
835
|
+
|
|
725
836
|
return
|
|
726
837
|
}
|
|
727
838
|
|
|
728
839
|
config?.hooks?.onResolveError?.(root)
|
|
840
|
+
lifecyclePlugin.forEach((it) => it.onResolveError?.(root))
|
|
841
|
+
|
|
729
842
|
return console.warn(
|
|
730
843
|
`Failed to resolve external reference "${resolvedPath}". The reference may be invalid, inaccessible, or missing a loader for this type of reference.`,
|
|
731
844
|
)
|
|
732
845
|
}
|
|
733
846
|
|
|
734
|
-
// Recursively
|
|
735
|
-
// This ensures
|
|
736
|
-
// We skip
|
|
847
|
+
// Recursively traverse all child properties of the current object to resolve nested $ref references.
|
|
848
|
+
// This step ensures that any $refs located deeper within the object hierarchy are discovered and processed.
|
|
849
|
+
// We explicitly skip the extension keys (x-ext and x-ext-urls) to avoid reprocessing already bundled or mapped content.
|
|
737
850
|
await Promise.all(
|
|
738
851
|
Object.entries(root).map(async ([key, value]) => {
|
|
739
|
-
if (key === extensions.externalDocuments) {
|
|
852
|
+
if (key === extensions.externalDocuments || key === extensions.externalDocumentsMappings) {
|
|
740
853
|
return
|
|
741
854
|
}
|
|
742
855
|
|
|
743
|
-
await bundler(value, origin, isChunkParent, depth + 1)
|
|
856
|
+
await bundler(value, origin, isChunkParent, depth + 1, [...path, key], root as UnknownObject)
|
|
744
857
|
}),
|
|
745
858
|
)
|
|
859
|
+
|
|
860
|
+
// Invoke the optional onAfterNodeProcess hook from the config, if provided.
|
|
861
|
+
// This allows for custom post-processing logic after a node has been handled by the bundler.
|
|
862
|
+
await config.hooks?.onAfterNodeProcess?.(root as UnknownObject, {
|
|
863
|
+
path,
|
|
864
|
+
resolutionCache: cache,
|
|
865
|
+
parentNode: parent,
|
|
866
|
+
rootNode: documentRoot as UnknownObject,
|
|
867
|
+
loaders: loaderPlugins,
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
// Iterate through all lifecycle plugins and invoke their onAfterNodeProcess hooks, if defined.
|
|
871
|
+
// This enables plugins to perform additional post-processing or cleanup after the node is processed.
|
|
872
|
+
for (const plugin of lifecyclePlugin) {
|
|
873
|
+
await plugin.onAfterNodeProcess?.(root as UnknownObject, {
|
|
874
|
+
path,
|
|
875
|
+
resolutionCache: cache,
|
|
876
|
+
parentNode: parent,
|
|
877
|
+
rootNode: documentRoot as UnknownObject,
|
|
878
|
+
loaders: loaderPlugins,
|
|
879
|
+
})
|
|
880
|
+
}
|
|
746
881
|
}
|
|
747
882
|
|
|
748
883
|
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
|
]
|