@scalar/json-magic 0.4.1 → 0.4.3

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.
@@ -1,10 +1,10 @@
1
1
 
2
- > @scalar/json-magic@0.4.1 build /home/runner/work/scalar/scalar/packages/json-magic
2
+ > @scalar/json-magic@0.4.3 build /home/runner/work/scalar/scalar/packages/json-magic
3
3
  > scalar-build-esbuild
4
4
 
5
- @scalar/json-magic: Build completed in 33.98ms
5
+ @scalar/json-magic: Build completed in 32.73ms
6
6
 
7
- > @scalar/json-magic@0.4.1 types:build /home/runner/work/scalar/scalar/packages/json-magic
7
+ > @scalar/json-magic@0.4.3 types:build /home/runner/work/scalar/scalar/packages/json-magic
8
8
  > scalar-types-build
9
9
 
10
- Types build completed in 1.54s
10
+ Types build completed in 1.77s
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @scalar/json-magic
2
2
 
3
+ ## 0.4.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [bff46e5]
8
+ - @scalar/helpers@0.0.11
9
+
10
+ ## 0.4.2
11
+
12
+ ### Patch Changes
13
+
14
+ - 3bd1209: fix: do not throw when we set on an invalid ref
15
+ - 1943b99: chore: emit warning when trying to set an invalid ref
16
+
3
17
  ## 0.4.1
4
18
 
5
19
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/magic-proxy/proxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAW5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,aAAa,EACnG,QAAQ,CAAC,EACT,UAAU;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,EACpC,OAAM,CAAC,GAAG,CAAU,EACpB,4BAAkC,EAClC,+BAAqC,MAgLtC,CAAA;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAUnC"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/magic-proxy/proxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAW5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,aAAa,EACnG,QAAQ,CAAC,EACT,UAAU;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,EACpC,OAAM,CAAC,GAAG,CAAU,EACpB,4BAAkC,EAClC,+BAAqC,MAsLtC,CAAA;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAUnC"}
@@ -1,7 +1,7 @@
1
1
  import { isLocalRef } from "../bundle/bundle.js";
2
2
  import { getSegmentsFromPath } from "../utils/get-segments-from-path.js";
3
3
  import { isObject } from "../utils/is-object.js";
4
- import { getValueByPath, parseJsonPointer } from "../utils/json-path-utils.js";
4
+ import { createPathFromSegments, getValueByPath, parseJsonPointer } from "../utils/json-path-utils.js";
5
5
  const isMagicProxy = Symbol("isMagicProxy");
6
6
  const magicProxyTarget = Symbol("magicProxyTarget");
7
7
  const REF_VALUE = "$ref-value";
@@ -63,11 +63,16 @@ const createMagicProxy = (target, options, root = target, cache = /* @__PURE__ *
63
63
  if (segments.length === 0) {
64
64
  return false;
65
65
  }
66
- const parentNode = getValueByPath(root, segments.slice(0, -1));
67
- if (!parentNode || !isObject(parentNode) && !Array.isArray(parentNode)) {
68
- return false;
66
+ const getParentNode = () => getValueByPath(root, segments.slice(0, -1));
67
+ if (getParentNode() === void 0) {
68
+ createPathFromSegments(root, segments.slice(0, -1));
69
+ console.warn(
70
+ `Trying to set $ref-value for invalid reference: ${ref}
71
+
72
+ Please fix your input file to fix this issue.`
73
+ );
69
74
  }
70
- parentNode[segments.at(-1)] = newValue;
75
+ getParentNode()[segments.at(-1)] = newValue;
71
76
  return true;
72
77
  }
73
78
  return Reflect.set(target2, prop, newValue, receiver);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/magic-proxy/proxy.ts"],
4
- "sourcesContent": ["import { isLocalRef } from '@/bundle/bundle'\nimport type { UnknownObject } from '@/types'\nimport { getSegmentsFromPath } from '@/utils/get-segments-from-path'\nimport { isObject } from '@/utils/is-object'\nimport { getValueByPath, parseJsonPointer } from '@/utils/json-path-utils'\n\nconst isMagicProxy = Symbol('isMagicProxy')\nconst magicProxyTarget = Symbol('magicProxyTarget')\n\nconst REF_VALUE = '$ref-value'\nconst REF_KEY = '$ref'\n\n/**\n * Creates a \"magic\" proxy for a given object or array, enabling transparent access to\n * JSON Reference ($ref) values as if they were directly present on the object.\n *\n * - If an object contains a `$ref` property, accessing the special `$ref-value` property\n * will resolve and return the referenced value from the root object.\n * - All nested objects and arrays are recursively wrapped in proxies, so reference resolution\n * works at any depth.\n * - Properties starting with an underscore (_) are hidden and will not be accessible through\n * the proxy (returns undefined on access, false on 'in' checks, excluded from enumeration).\n * - Setting, deleting, and enumerating properties works as expected, including for proxied references.\n *\n * @param target - The object or array to wrap in a magic proxy\n * @param root - The root object for resolving local JSON references (defaults to target)\n * @returns A proxied version of the input object/array with magic $ref-value support\n *\n * @example\n * const input = {\n * definitions: {\n * foo: { bar: 123 }\n * },\n * refObj: { $ref: '#/definitions/foo' },\n * _internal: 'hidden property'\n * }\n * const proxy = createMagicProxy(input)\n *\n * // Accessing proxy.refObj['$ref-value'] will resolve to { bar: 123 }\n * console.log(proxy.refObj['$ref-value']) // { bar: 123 }\n *\n * // Properties starting with underscore are hidden\n * console.log(proxy._internal) // undefined\n * console.log('_internal' in proxy) // false\n * console.log(Object.keys(proxy)) // ['definitions', 'refObj'] (no '_internal')\n *\n * // Setting and deleting properties works as expected\n * proxy.refObj.extra = 'hello'\n * delete proxy.refObj.extra\n */\nexport const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S extends UnknownObject>(\n target: T,\n options?: { showInternal?: boolean },\n root: S | T = target,\n cache = new Map<string, unknown>(),\n proxyCache = new WeakMap<object, T>(),\n) => {\n if (!isObject(target) && !Array.isArray(target)) {\n return target\n }\n\n // Return existing proxy for the same target to ensure referential stability\n if (proxyCache.has(target)) {\n return proxyCache.get(target)\n }\n\n const handler: ProxyHandler<T> = {\n /**\n * Proxy \"get\" trap for magic proxy.\n * - If accessing the special isMagicProxy symbol, return true to identify proxy.\n * - If accessing the magicProxyTarget symbol, return the original target object.\n * - Hide properties starting with underscore by returning undefined.\n * - If accessing \"$ref-value\" and the object has a local $ref, resolve and return the referenced value as a new magic proxy.\n * - For all other properties, recursively wrap the returned value in a magic proxy (if applicable).\n */\n get(target, prop, receiver) {\n if (prop === isMagicProxy) {\n // Used to identify if an object is a magic proxy\n return true\n }\n\n if (prop === magicProxyTarget) {\n // Used to retrieve the original target object from the proxy\n return target\n }\n\n const ref = Reflect.get(target, REF_KEY, receiver)\n\n // Hide properties starting with underscore - these are considered internal/private properties\n // and should not be accessible through the magic proxy interface\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return undefined\n }\n\n // If accessing \"$ref-value\" and $ref is a local reference, resolve and return the referenced value\n if (prop === REF_VALUE && typeof ref === 'string' && isLocalRef(ref)) {\n // Check cache first for performance optimization\n if (cache.has(ref)) {\n return cache.get(ref)\n }\n\n // Resolve the reference and create a new magic proxy\n const resolvedValue = getValueByPath(root, parseJsonPointer(ref))\n const proxiedValue = createMagicProxy(resolvedValue, options, root, cache)\n\n // Store in cache for future lookups\n cache.set(ref, proxiedValue)\n return proxiedValue\n }\n\n // For all other properties, recursively wrap the value in a magic proxy\n const value = Reflect.get(target, prop, receiver)\n return createMagicProxy(value as T, options, root, cache, proxyCache)\n },\n /**\n * Proxy \"set\" trap for magic proxy.\n * Allows setting properties on the proxied object.\n * This will update the underlying target object.\n *\n * Note: it will not update if the property starts with an underscore (_)\n * Those will be considered private properties by the proxy\n */\n set(target, prop, newValue, receiver) {\n const ref = Reflect.get(target, REF_KEY, receiver)\n\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return true\n }\n\n if (prop === REF_VALUE && typeof ref === 'string' && isLocalRef(ref)) {\n const segments = getSegmentsFromPath(ref)\n\n if (segments.length === 0) {\n return false // Can not set top level $ref-value\n }\n\n const parentNode = getValueByPath(root, segments.slice(0, -1))\n\n // TODO: Maybe we create the path if it does not exist?\n // TODO: This can allow for invalid references to not throw errors\n if (!parentNode || (!isObject(parentNode) && !Array.isArray(parentNode))) {\n return false // Parent node does not exist, cannot set $ref-value\n }\n parentNode[segments.at(-1)] = newValue\n return true\n }\n\n return Reflect.set(target, prop, newValue, receiver)\n },\n /**\n * Proxy \"deleteProperty\" trap for magic proxy.\n * Allows deleting properties from the proxied object.\n * This will update the underlying target object.\n */\n deleteProperty(target, prop) {\n return Reflect.deleteProperty(target, prop)\n },\n /**\n * Proxy \"has\" trap for magic proxy.\n * - Pretend that \"$ref-value\" exists if \"$ref\" exists on the target.\n * This allows expressions like `\"$ref-value\" in obj` to return true for objects with a $ref,\n * even though \"$ref-value\" is a virtual property provided by the proxy.\n * - Hide properties starting with underscore by returning false.\n * - For all other properties, defer to the default Reflect.has behavior.\n */\n has(target, prop) {\n // Hide properties starting with underscore\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return false\n }\n\n // Pretend that \"$ref-value\" exists if \"$ref\" exists\n if (prop === REF_VALUE && REF_KEY in target) {\n return true\n }\n return Reflect.has(target, prop)\n },\n /**\n * Proxy \"ownKeys\" trap for magic proxy.\n * - Returns the list of own property keys for the proxied object.\n * - If the object has a \"$ref\" property, ensures that \"$ref-value\" is also included in the keys,\n * even though \"$ref-value\" is a virtual property provided by the proxy.\n * This allows Object.keys, Reflect.ownKeys, etc. to include \"$ref-value\" for objects with $ref.\n * - Filters out properties starting with underscore.\n */\n ownKeys(target) {\n const keys = Reflect.ownKeys(target)\n\n // Filter out properties starting with underscore\n const filteredKeys = keys.filter(\n (key) => typeof key !== 'string' || !(key.startsWith('_') && !options?.showInternal),\n )\n\n if (REF_KEY in target && !filteredKeys.includes(REF_VALUE)) {\n filteredKeys.push(REF_VALUE)\n }\n return filteredKeys\n },\n\n /**\n * Proxy \"getOwnPropertyDescriptor\" trap for magic proxy.\n * - For the virtual \"$ref-value\" property, returns a descriptor that makes it appear as a regular property.\n * - Hide properties starting with underscore by returning undefined.\n * - For all other properties, delegates to the default Reflect.getOwnPropertyDescriptor behavior.\n * - This ensures that Object.getOwnPropertyDescriptor and similar methods work correctly with the virtual property.\n */\n getOwnPropertyDescriptor(target, prop) {\n // Hide properties starting with underscore\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return undefined\n }\n\n const ref = Reflect.get(target, REF_KEY)\n\n if (prop === REF_VALUE && typeof ref === 'string') {\n return {\n configurable: true,\n enumerable: true,\n value: undefined,\n writable: false,\n }\n }\n\n // Otherwise, delegate to the default behavior\n return Reflect.getOwnPropertyDescriptor(target, prop)\n },\n }\n\n const proxied = new Proxy<T>(target, handler)\n proxyCache.set(target, proxied)\n return proxied\n}\n\n/**\n * Gets the raw (non-proxied) version of an object created by createMagicProxy.\n * This is useful when you need to access the original object without the magic proxy wrapper.\n *\n * @param obj - The magic proxy object to get the raw version of\n * @returns The raw version of the object\n * @example\n * const proxy = createMagicProxy({ foo: { $ref: '#/bar' } })\n * const raw = getRaw(proxy) // { foo: { $ref: '#/bar' } }\n */\nexport function getRaw<T>(obj: T): T {\n if (typeof obj !== 'object' || obj === null) {\n return obj\n }\n\n if ((obj as T & { [isMagicProxy]: boolean | undefined })[isMagicProxy]) {\n return (obj as T & { [magicProxyTarget]: T })[magicProxyTarget]\n }\n\n return obj\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { isLocalRef } from '@/bundle/bundle'\nimport type { UnknownObject } from '@/types'\nimport { getSegmentsFromPath } from '@/utils/get-segments-from-path'\nimport { isObject } from '@/utils/is-object'\nimport { createPathFromSegments, getValueByPath, parseJsonPointer } from '@/utils/json-path-utils'\n\nconst isMagicProxy = Symbol('isMagicProxy')\nconst magicProxyTarget = Symbol('magicProxyTarget')\n\nconst REF_VALUE = '$ref-value'\nconst REF_KEY = '$ref'\n\n/**\n * Creates a \"magic\" proxy for a given object or array, enabling transparent access to\n * JSON Reference ($ref) values as if they were directly present on the object.\n *\n * - If an object contains a `$ref` property, accessing the special `$ref-value` property\n * will resolve and return the referenced value from the root object.\n * - All nested objects and arrays are recursively wrapped in proxies, so reference resolution\n * works at any depth.\n * - Properties starting with an underscore (_) are hidden and will not be accessible through\n * the proxy (returns undefined on access, false on 'in' checks, excluded from enumeration).\n * - Setting, deleting, and enumerating properties works as expected, including for proxied references.\n *\n * @param target - The object or array to wrap in a magic proxy\n * @param root - The root object for resolving local JSON references (defaults to target)\n * @returns A proxied version of the input object/array with magic $ref-value support\n *\n * @example\n * const input = {\n * definitions: {\n * foo: { bar: 123 }\n * },\n * refObj: { $ref: '#/definitions/foo' },\n * _internal: 'hidden property'\n * }\n * const proxy = createMagicProxy(input)\n *\n * // Accessing proxy.refObj['$ref-value'] will resolve to { bar: 123 }\n * console.log(proxy.refObj['$ref-value']) // { bar: 123 }\n *\n * // Properties starting with underscore are hidden\n * console.log(proxy._internal) // undefined\n * console.log('_internal' in proxy) // false\n * console.log(Object.keys(proxy)) // ['definitions', 'refObj'] (no '_internal')\n *\n * // Setting and deleting properties works as expected\n * proxy.refObj.extra = 'hello'\n * delete proxy.refObj.extra\n */\nexport const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S extends UnknownObject>(\n target: T,\n options?: { showInternal?: boolean },\n root: S | T = target,\n cache = new Map<string, unknown>(),\n proxyCache = new WeakMap<object, T>(),\n) => {\n if (!isObject(target) && !Array.isArray(target)) {\n return target\n }\n\n // Return existing proxy for the same target to ensure referential stability\n if (proxyCache.has(target)) {\n return proxyCache.get(target)\n }\n\n const handler: ProxyHandler<T> = {\n /**\n * Proxy \"get\" trap for magic proxy.\n * - If accessing the special isMagicProxy symbol, return true to identify proxy.\n * - If accessing the magicProxyTarget symbol, return the original target object.\n * - Hide properties starting with underscore by returning undefined.\n * - If accessing \"$ref-value\" and the object has a local $ref, resolve and return the referenced value as a new magic proxy.\n * - For all other properties, recursively wrap the returned value in a magic proxy (if applicable).\n */\n get(target, prop, receiver) {\n if (prop === isMagicProxy) {\n // Used to identify if an object is a magic proxy\n return true\n }\n\n if (prop === magicProxyTarget) {\n // Used to retrieve the original target object from the proxy\n return target\n }\n\n const ref = Reflect.get(target, REF_KEY, receiver)\n\n // Hide properties starting with underscore - these are considered internal/private properties\n // and should not be accessible through the magic proxy interface\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return undefined\n }\n\n // If accessing \"$ref-value\" and $ref is a local reference, resolve and return the referenced value\n if (prop === REF_VALUE && typeof ref === 'string' && isLocalRef(ref)) {\n // Check cache first for performance optimization\n if (cache.has(ref)) {\n return cache.get(ref)\n }\n\n // Resolve the reference and create a new magic proxy\n const resolvedValue = getValueByPath(root, parseJsonPointer(ref))\n const proxiedValue = createMagicProxy(resolvedValue, options, root, cache)\n\n // Store in cache for future lookups\n cache.set(ref, proxiedValue)\n return proxiedValue\n }\n\n // For all other properties, recursively wrap the value in a magic proxy\n const value = Reflect.get(target, prop, receiver)\n return createMagicProxy(value as T, options, root, cache, proxyCache)\n },\n /**\n * Proxy \"set\" trap for magic proxy.\n * Allows setting properties on the proxied object.\n * This will update the underlying target object.\n *\n * Note: it will not update if the property starts with an underscore (_)\n * Those will be considered private properties by the proxy\n */\n set(target, prop, newValue, receiver) {\n const ref = Reflect.get(target, REF_KEY, receiver)\n\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return true\n }\n\n if (prop === REF_VALUE && typeof ref === 'string' && isLocalRef(ref)) {\n const segments = getSegmentsFromPath(ref)\n\n if (segments.length === 0) {\n return false // Can not set top level $ref-value\n }\n\n // Get the parent node or create it if it does not exist\n const getParentNode = () => getValueByPath(root, segments.slice(0, -1))\n\n if (getParentNode() === undefined) {\n createPathFromSegments(root, segments.slice(0, -1))\n\n // In this case the ref is pointing to an invalid path, so we warn the user\n console.warn(\n `Trying to set $ref-value for invalid reference: ${ref}\\n\\nPlease fix your input file to fix this issue.`,\n )\n }\n\n // Set the value on the parent node\n getParentNode()[segments.at(-1)] = newValue\n return true\n }\n\n return Reflect.set(target, prop, newValue, receiver)\n },\n /**\n * Proxy \"deleteProperty\" trap for magic proxy.\n * Allows deleting properties from the proxied object.\n * This will update the underlying target object.\n */\n deleteProperty(target, prop) {\n return Reflect.deleteProperty(target, prop)\n },\n /**\n * Proxy \"has\" trap for magic proxy.\n * - Pretend that \"$ref-value\" exists if \"$ref\" exists on the target.\n * This allows expressions like `\"$ref-value\" in obj` to return true for objects with a $ref,\n * even though \"$ref-value\" is a virtual property provided by the proxy.\n * - Hide properties starting with underscore by returning false.\n * - For all other properties, defer to the default Reflect.has behavior.\n */\n has(target, prop) {\n // Hide properties starting with underscore\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return false\n }\n\n // Pretend that \"$ref-value\" exists if \"$ref\" exists\n if (prop === REF_VALUE && REF_KEY in target) {\n return true\n }\n return Reflect.has(target, prop)\n },\n /**\n * Proxy \"ownKeys\" trap for magic proxy.\n * - Returns the list of own property keys for the proxied object.\n * - If the object has a \"$ref\" property, ensures that \"$ref-value\" is also included in the keys,\n * even though \"$ref-value\" is a virtual property provided by the proxy.\n * This allows Object.keys, Reflect.ownKeys, etc. to include \"$ref-value\" for objects with $ref.\n * - Filters out properties starting with underscore.\n */\n ownKeys(target) {\n const keys = Reflect.ownKeys(target)\n\n // Filter out properties starting with underscore\n const filteredKeys = keys.filter(\n (key) => typeof key !== 'string' || !(key.startsWith('_') && !options?.showInternal),\n )\n\n if (REF_KEY in target && !filteredKeys.includes(REF_VALUE)) {\n filteredKeys.push(REF_VALUE)\n }\n return filteredKeys\n },\n\n /**\n * Proxy \"getOwnPropertyDescriptor\" trap for magic proxy.\n * - For the virtual \"$ref-value\" property, returns a descriptor that makes it appear as a regular property.\n * - Hide properties starting with underscore by returning undefined.\n * - For all other properties, delegates to the default Reflect.getOwnPropertyDescriptor behavior.\n * - This ensures that Object.getOwnPropertyDescriptor and similar methods work correctly with the virtual property.\n */\n getOwnPropertyDescriptor(target, prop) {\n // Hide properties starting with underscore\n if (typeof prop === 'string' && prop.startsWith('_') && !options?.showInternal) {\n return undefined\n }\n\n const ref = Reflect.get(target, REF_KEY)\n\n if (prop === REF_VALUE && typeof ref === 'string') {\n return {\n configurable: true,\n enumerable: true,\n value: undefined,\n writable: false,\n }\n }\n\n // Otherwise, delegate to the default behavior\n return Reflect.getOwnPropertyDescriptor(target, prop)\n },\n }\n\n const proxied = new Proxy<T>(target, handler)\n proxyCache.set(target, proxied)\n return proxied\n}\n\n/**\n * Gets the raw (non-proxied) version of an object created by createMagicProxy.\n * This is useful when you need to access the original object without the magic proxy wrapper.\n *\n * @param obj - The magic proxy object to get the raw version of\n * @returns The raw version of the object\n * @example\n * const proxy = createMagicProxy({ foo: { $ref: '#/bar' } })\n * const raw = getRaw(proxy) // { foo: { $ref: '#/bar' } }\n */\nexport function getRaw<T>(obj: T): T {\n if (typeof obj !== 'object' || obj === null) {\n return obj\n }\n\n if ((obj as T & { [isMagicProxy]: boolean | undefined })[isMagicProxy]) {\n return (obj as T & { [magicProxyTarget]: T })[magicProxyTarget]\n }\n\n return obj\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["target"]
7
7
  }
@@ -20,4 +20,23 @@ export declare function parseJsonPointer(pointer: string): string[];
20
20
  * ```
21
21
  */
22
22
  export declare function getValueByPath<R = unknown>(obj: any, pointer: string[]): R;
23
+ /**
24
+ * Creates a nested path in an object from an array of path segments.
25
+ * Only creates intermediate objects/arrays if they don't already exist.
26
+ *
27
+ * @param obj - The target object to create the path in
28
+ * @param segments - Array of path segments to create
29
+ * @returns The final nested object/array at the end of the path
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const obj = {}
34
+ * createPathFromSegments(obj, ['components', 'schemas', 'User'])
35
+ * // Creates: { components: { schemas: { User: {} } } }
36
+ *
37
+ * createPathFromSegments(obj, ['items', '0', 'name'])
38
+ * // Creates: { items: [{ name: {} }] }
39
+ * ```
40
+ */
41
+ export declare function createPathFromSegments(obj: any, segments: string[]): any;
23
42
  //# sourceMappingURL=json-path-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-path-utils.d.ts","sourceRoot":"","sources":["../../src/utils/json-path-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,CAO1E"}
1
+ {"version":3,"file":"json-path-utils.d.ts","sourceRoot":"","sources":["../../src/utils/json-path-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,CAO1E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAWlE"}
@@ -9,7 +9,20 @@ function getValueByPath(obj, pointer) {
9
9
  return acc[part];
10
10
  }, obj);
11
11
  }
12
+ function createPathFromSegments(obj, segments) {
13
+ return segments.reduce((acc, part) => {
14
+ if (acc[part] === void 0) {
15
+ if (isNaN(Number(part))) {
16
+ acc[part] = {};
17
+ } else {
18
+ acc[part] = [];
19
+ }
20
+ }
21
+ return acc[part];
22
+ }, obj);
23
+ }
12
24
  export {
25
+ createPathFromSegments,
13
26
  getValueByPath,
14
27
  parseJsonPointer
15
28
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/json-path-utils.ts"],
4
- "sourcesContent": ["/**\n * Parses a JSON Pointer string into an array of path segments\n *\n * @example\n * ```ts\n * parseJsonPointer('#/components/schemas/User')\n *\n * ['components', 'schemas', 'User']\n * ```\n */\nexport function parseJsonPointer(pointer: string): string[] {\n return (\n pointer\n // Split on '/'\n .split('/')\n // Remove the leading '#' if present\n .filter((segment, index) => (index !== 0 || segment !== '#') && segment)\n )\n}\n\n/**\n * Retrieves a nested value from the source document using a path array\n *\n * @example\n * ```ts\n * getValueByPath(document, ['components', 'schemas', 'User'])\n *\n * { id: '123', name: 'John Doe' }\n * ```\n */\nexport function getValueByPath<R = unknown>(obj: any, pointer: string[]): R {\n return pointer.reduce((acc, part) => {\n if (acc === undefined || acc === null) {\n return undefined\n }\n return acc[part]\n }, obj)\n}\n"],
5
- "mappings": "AAUO,SAAS,iBAAiB,SAA2B;AAC1D,SACE,QAEG,MAAM,GAAG,EAET,OAAO,CAAC,SAAS,WAAW,UAAU,KAAK,YAAY,QAAQ,OAAO;AAE7E;AAYO,SAAS,eAA4B,KAAU,SAAsB;AAC1E,SAAO,QAAQ,OAAO,CAAC,KAAK,SAAS;AACnC,QAAI,QAAQ,UAAa,QAAQ,MAAM;AACrC,aAAO;AAAA,IACT;AACA,WAAO,IAAI,IAAI;AAAA,EACjB,GAAG,GAAG;AACR;",
4
+ "sourcesContent": ["/**\n * Parses a JSON Pointer string into an array of path segments\n *\n * @example\n * ```ts\n * parseJsonPointer('#/components/schemas/User')\n *\n * ['components', 'schemas', 'User']\n * ```\n */\nexport function parseJsonPointer(pointer: string): string[] {\n return (\n pointer\n // Split on '/'\n .split('/')\n // Remove the leading '#' if present\n .filter((segment, index) => (index !== 0 || segment !== '#') && segment)\n )\n}\n\n/**\n * Retrieves a nested value from the source document using a path array\n *\n * @example\n * ```ts\n * getValueByPath(document, ['components', 'schemas', 'User'])\n *\n * { id: '123', name: 'John Doe' }\n * ```\n */\nexport function getValueByPath<R = unknown>(obj: any, pointer: string[]): R {\n return pointer.reduce((acc, part) => {\n if (acc === undefined || acc === null) {\n return undefined\n }\n return acc[part]\n }, obj)\n}\n\n/**\n * Creates a nested path in an object from an array of path segments.\n * Only creates intermediate objects/arrays if they don't already exist.\n *\n * @param obj - The target object to create the path in\n * @param segments - Array of path segments to create\n * @returns The final nested object/array at the end of the path\n *\n * @example\n * ```ts\n * const obj = {}\n * createPathFromSegments(obj, ['components', 'schemas', 'User'])\n * // Creates: { components: { schemas: { User: {} } } }\n *\n * createPathFromSegments(obj, ['items', '0', 'name'])\n * // Creates: { items: [{ name: {} }] }\n * ```\n */\nexport function createPathFromSegments(obj: any, segments: string[]) {\n return segments.reduce((acc, part) => {\n if (acc[part] === undefined) {\n if (isNaN(Number(part))) {\n acc[part] = {}\n } else {\n acc[part] = []\n }\n }\n return acc[part]\n }, obj)\n}\n"],
5
+ "mappings": "AAUO,SAAS,iBAAiB,SAA2B;AAC1D,SACE,QAEG,MAAM,GAAG,EAET,OAAO,CAAC,SAAS,WAAW,UAAU,KAAK,YAAY,QAAQ,OAAO;AAE7E;AAYO,SAAS,eAA4B,KAAU,SAAsB;AAC1E,SAAO,QAAQ,OAAO,CAAC,KAAK,SAAS;AACnC,QAAI,QAAQ,UAAa,QAAQ,MAAM;AACrC,aAAO;AAAA,IACT;AACA,WAAO,IAAI,IAAI;AAAA,EACjB,GAAG,GAAG;AACR;AAoBO,SAAS,uBAAuB,KAAU,UAAoB;AACnE,SAAO,SAAS,OAAO,CAAC,KAAK,SAAS;AACpC,QAAI,IAAI,IAAI,MAAM,QAAW;AAC3B,UAAI,MAAM,OAAO,IAAI,CAAC,GAAG;AACvB,YAAI,IAAI,IAAI,CAAC;AAAA,MACf,OAAO;AACL,YAAI,IAAI,IAAI,CAAC;AAAA,MACf;AAAA,IACF;AACA,WAAO,IAAI,IAAI;AAAA,EACjB,GAAG,GAAG;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "url": "git+https://github.com/scalar/scalar.git",
11
11
  "directory": "packages/json-magic"
12
12
  },
13
- "version": "0.4.1",
13
+ "version": "0.4.3",
14
14
  "engines": {
15
15
  "node": ">=20"
16
16
  },
@@ -50,11 +50,11 @@
50
50
  "dependencies": {
51
51
  "vue": "^3.5.17",
52
52
  "yaml": "2.8.0",
53
- "@scalar/helpers": "0.0.10"
53
+ "@scalar/helpers": "0.0.11"
54
54
  },
55
55
  "devDependencies": {
56
56
  "fastify": "^5.3.3",
57
- "vite": "6.1.6",
57
+ "vite": "7.1.5",
58
58
  "@scalar/build-tooling": "0.2.7"
59
59
  },
60
60
  "scripts": {
@@ -386,9 +386,7 @@ describe('createMagicProxy', () => {
386
386
  }).toThrowError("'set' on proxy: trap returned falsish for property '$ref-value'")
387
387
  })
388
388
 
389
- // TODO: might change this behavior in the future
390
- // so we allow setting the $ref-value for invalid refs by creating the path
391
- it('throws when trying to update an invalid ref where the parent node does not exists', () => {
389
+ it('does not throw when trying to update an invalid ref where the parent node does not exists', () => {
392
390
  const input = {
393
391
  a: {
394
392
  $ref: '#/non-existent/some-path',
@@ -404,7 +402,23 @@ describe('createMagicProxy', () => {
404
402
 
405
403
  expect(() => {
406
404
  proxied.a['$ref-value'] = 'new value'
407
- }).toThrowError("'set' on proxy: trap returned falsish for property '$ref-value'")
405
+ }).not.toThrowError("'set' on proxy: trap returned falsish for property '$ref-value'")
406
+
407
+ expect(proxied.a['$ref-value']).toBe('new value')
408
+
409
+ expect(input).toEqual({
410
+ 'a': {
411
+ '$ref': '#/non-existent/some-path',
412
+ },
413
+ 'b': {
414
+ 'c': {
415
+ 'hello': 'world',
416
+ },
417
+ },
418
+ 'non-existent': {
419
+ 'some-path': 'new value',
420
+ },
421
+ })
408
422
  })
409
423
  })
410
424
 
@@ -2,7 +2,7 @@ import { isLocalRef } from '@/bundle/bundle'
2
2
  import type { UnknownObject } from '@/types'
3
3
  import { getSegmentsFromPath } from '@/utils/get-segments-from-path'
4
4
  import { isObject } from '@/utils/is-object'
5
- import { getValueByPath, parseJsonPointer } from '@/utils/json-path-utils'
5
+ import { createPathFromSegments, getValueByPath, parseJsonPointer } from '@/utils/json-path-utils'
6
6
 
7
7
  const isMagicProxy = Symbol('isMagicProxy')
8
8
  const magicProxyTarget = Symbol('magicProxyTarget')
@@ -134,14 +134,20 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
134
134
  return false // Can not set top level $ref-value
135
135
  }
136
136
 
137
- const parentNode = getValueByPath(root, segments.slice(0, -1))
137
+ // Get the parent node or create it if it does not exist
138
+ const getParentNode = () => getValueByPath(root, segments.slice(0, -1))
138
139
 
139
- // TODO: Maybe we create the path if it does not exist?
140
- // TODO: This can allow for invalid references to not throw errors
141
- if (!parentNode || (!isObject(parentNode) && !Array.isArray(parentNode))) {
142
- return false // Parent node does not exist, cannot set $ref-value
140
+ if (getParentNode() === undefined) {
141
+ createPathFromSegments(root, segments.slice(0, -1))
142
+
143
+ // In this case the ref is pointing to an invalid path, so we warn the user
144
+ console.warn(
145
+ `Trying to set $ref-value for invalid reference: ${ref}\n\nPlease fix your input file to fix this issue.`,
146
+ )
143
147
  }
144
- parentNode[segments.at(-1)] = newValue
148
+
149
+ // Set the value on the parent node
150
+ getParentNode()[segments.at(-1)] = newValue
145
151
  return true
146
152
  }
147
153
 
@@ -1,6 +1,7 @@
1
- import { parseJsonPointer } from './json-path-utils'
2
1
  import { describe, expect, test } from 'vitest'
3
2
 
3
+ import { createPathFromSegments, parseJsonPointer } from './json-path-utils'
4
+
4
5
  describe('parseJsonPointer', () => {
5
6
  test.each([
6
7
  ['#/users/name', ['users', 'name']],
@@ -11,3 +12,46 @@ describe('parseJsonPointer', () => {
11
12
  expect(parseJsonPointer(a)).toEqual(b)
12
13
  })
13
14
  })
15
+
16
+ describe('createPathFromSegments', () => {
17
+ test('creates nested objects for non-numeric segments', () => {
18
+ const obj: any = {}
19
+ const leaf = createPathFromSegments(obj, ['components', 'schemas', 'User'])
20
+
21
+ expect(obj).toEqual({ components: { schemas: { User: {} } } })
22
+ expect(leaf).toBe(obj.components.schemas.User)
23
+ })
24
+
25
+ test('creates arrays for numeric segments', () => {
26
+ const obj: any = {}
27
+ const arr = createPathFromSegments(obj, ['items', '0'])
28
+
29
+ expect(Array.isArray(obj.items['0'])).toBe(true)
30
+ expect(arr).toBe(obj.items['0'])
31
+ })
32
+
33
+ test('does not overwrite existing values along the path', () => {
34
+ const obj: any = { a: { b: { c: { existing: true } } } }
35
+ const leaf = createPathFromSegments(obj, ['a', 'b', 'c'])
36
+
37
+ expect(leaf).toEqual({ existing: true })
38
+ expect(obj.a.b.c).toEqual({ existing: true })
39
+ })
40
+
41
+ test('returns root object when segments array is empty', () => {
42
+ const obj: any = { pre: true }
43
+ const result = createPathFromSegments(obj, [])
44
+
45
+ expect(result).toBe(obj)
46
+ expect(obj).toEqual({ pre: true })
47
+ })
48
+
49
+ test('creates nested arrays for consecutive numeric segments', () => {
50
+ const obj: any = {}
51
+ const leaf = createPathFromSegments(obj, ['arr', '0', '1'])
52
+
53
+ expect(Array.isArray(obj.arr['0'])).toBe(true)
54
+ expect(Array.isArray(obj.arr['0']['1'])).toBe(true)
55
+ expect(leaf).toBe(obj.arr['0']['1'])
56
+ })
57
+ })
@@ -36,3 +36,34 @@ export function getValueByPath<R = unknown>(obj: any, pointer: string[]): R {
36
36
  return acc[part]
37
37
  }, obj)
38
38
  }
39
+
40
+ /**
41
+ * Creates a nested path in an object from an array of path segments.
42
+ * Only creates intermediate objects/arrays if they don't already exist.
43
+ *
44
+ * @param obj - The target object to create the path in
45
+ * @param segments - Array of path segments to create
46
+ * @returns The final nested object/array at the end of the path
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const obj = {}
51
+ * createPathFromSegments(obj, ['components', 'schemas', 'User'])
52
+ * // Creates: { components: { schemas: { User: {} } } }
53
+ *
54
+ * createPathFromSegments(obj, ['items', '0', 'name'])
55
+ * // Creates: { items: [{ name: {} }] }
56
+ * ```
57
+ */
58
+ export function createPathFromSegments(obj: any, segments: string[]) {
59
+ return segments.reduce((acc, part) => {
60
+ if (acc[part] === undefined) {
61
+ if (isNaN(Number(part))) {
62
+ acc[part] = {}
63
+ } else {
64
+ acc[part] = []
65
+ }
66
+ }
67
+ return acc[part]
68
+ }, obj)
69
+ }