@scalar/json-magic 0.5.2 → 0.6.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 -4
- package/CHANGELOG.md +15 -0
- package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
- package/dist/bundle/plugins/fetch-urls/index.js +6 -0
- package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
- package/dist/bundle/plugins/parse-yaml/index.js +1 -1
- package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
- package/dist/helpers/normalize.d.ts.map +1 -1
- package/dist/helpers/normalize.js +2 -1
- package/dist/helpers/normalize.js.map +2 -2
- package/dist/magic-proxy/proxy.d.ts +6 -6
- package/dist/magic-proxy/proxy.js +10 -10
- package/dist/magic-proxy/proxy.js.map +2 -2
- package/package.json +2 -2
- package/src/bundle/plugins/fetch-urls/index.ts +9 -0
- package/src/bundle/plugins/parse-yaml/index.ts +1 -1
- package/src/helpers/normalize.ts +1 -0
- package/src/magic-proxy/proxy.test.ts +108 -76
- package/src/magic-proxy/proxy.ts +20 -20
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
> @scalar/json-magic@0.
|
|
2
|
+
> @scalar/json-magic@0.6.1 build /home/runner/work/scalar/scalar/packages/json-magic
|
|
3
3
|
> scalar-build-esbuild
|
|
4
4
|
|
|
5
|
-
[34m@scalar/json-magic: Build completed in
|
|
5
|
+
[34m@scalar/json-magic: Build completed in 25.49ms[39m
|
|
6
6
|
|
|
7
|
-
> @scalar/json-magic@0.
|
|
7
|
+
> @scalar/json-magic@0.6.1 types:build /home/runner/work/scalar/scalar/packages/json-magic
|
|
8
8
|
> scalar-types-build
|
|
9
9
|
|
|
10
|
-
[32mTypes build completed in 1.
|
|
10
|
+
[32mTypes build completed in 1.86s[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @scalar/json-magic
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2089748: chore: add logs when fetching unsupported formats
|
|
8
|
+
- 8a7fb2a: fix: schema properties starting with an underscore are hidden
|
|
9
|
+
- Updated dependencies [3f6d0b9]
|
|
10
|
+
- @scalar/helpers@0.0.12
|
|
11
|
+
|
|
12
|
+
## 0.6.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 4951456: feat: merge yaml aliases for in-mem representation
|
|
17
|
+
|
|
3
18
|
## 0.5.2
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAK3D,KAAK,WAAW,GAAG,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAA;IACtD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3F,CAAC,CAAA;AAcF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAChD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAK3D,KAAK,WAAW,GAAG,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAA;IACtD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3F,CAAC,CAAA;AAcF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAChD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAyCxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,GAAG,YAAY,CAShG"}
|
|
@@ -25,10 +25,16 @@ async function fetchUrl(url, limiter, config) {
|
|
|
25
25
|
data: normalize(body)
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
const contentType = result.headers.get("Content-Type") ?? "";
|
|
29
|
+
if (["text/html", "application/xml"].includes(contentType)) {
|
|
30
|
+
console.warn(`[WARN] We only support JSON/YAML formats, received ${contentType}`);
|
|
31
|
+
}
|
|
32
|
+
console.warn(`[WARN] Fetch failed with status ${result.status} ${result.statusText} for URL: ${url}`);
|
|
28
33
|
return {
|
|
29
34
|
ok: false
|
|
30
35
|
};
|
|
31
36
|
} catch {
|
|
37
|
+
console.warn(`[WARN] Failed to parse JSON/YAML from URL: ${url}`);
|
|
32
38
|
return {
|
|
33
39
|
ok: false
|
|
34
40
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/bundle/plugins/fetch-urls/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { LoaderPlugin, ResolveResult } from '@/bundle'\nimport { isRemoteUrl } from '@/bundle/bundle'\nimport { createLimiter } from '@/bundle/create-limiter'\nimport { normalize } from '@/helpers/normalize'\n\ntype FetchConfig = Partial<{\n headers: { headers: HeadersInit; domains: string[] }[]\n fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>\n}>\n\n/**\n * Safely checks for host from a URL\n * Needed because we cannot create a URL from a relative remote URL ex: examples/openapi.json\n */\nconst getHost = (url: string): string | null => {\n try {\n return new URL(url).host\n } catch {\n return null\n }\n}\n\n/**\n * Fetches and normalizes data from a remote URL\n * @param url - The URL to fetch data from\n * @returns A promise that resolves to either the normalized data or an error result\n * @example\n * ```ts\n * const result = await fetchUrl('https://api.example.com/data.json')\n * if (result.ok) {\n * console.log(result.data) // The normalized data\n * } else {\n * console.log('Failed to fetch data')\n * }\n * ```\n */\nexport async function fetchUrl(\n url: string,\n limiter: <T>(fn: () => Promise<T>) => Promise<T>,\n config?: FetchConfig,\n): Promise<ResolveResult> {\n try {\n const host = getHost(url)\n\n // Get the headers that match the domain\n const headers = config?.headers?.find((a) => a.domains.find((d) => d === host) !== undefined)?.headers\n\n const exec = config?.fetch ?? fetch\n\n const result = await limiter(() =>\n exec(url, {\n headers,\n }),\n )\n\n if (result.ok) {\n const body = await result.text()\n\n return {\n ok: true,\n data: normalize(body),\n }\n }\n\n return {\n ok: false,\n }\n } catch {\n return {\n ok: false,\n }\n }\n}\n\n/**\n * Creates a plugin for handling remote URL references.\n * This plugin validates and fetches data from HTTP/HTTPS URLs.\n *\n * @returns A plugin object with validate and exec functions\n * @example\n * const urlPlugin = fetchUrls()\n * if (urlPlugin.validate('https://example.com/schema.json')) {\n * const result = await urlPlugin.exec('https://example.com/schema.json')\n * }\n */\nexport function fetchUrls(config?: FetchConfig & Partial<{ limit: number | null }>): LoaderPlugin {\n // If there is a limit specified we limit the number of concurrent calls\n const limiter = config?.limit ? createLimiter(config.limit) : <T>(fn: () => Promise<T>) => fn()\n\n return {\n type: 'loader',\n validate: isRemoteUrl,\n exec: (value) => fetchUrl(value, limiter, config),\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAW1B,MAAM,UAAU,CAAC,QAA+B;AAC9C,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAgBA,eAAsB,SACpB,KACA,SACA,QACwB;AACxB,MAAI;AACF,UAAM,OAAO,QAAQ,GAAG;AAGxB,UAAM,UAAU,QAAQ,SAAS,KAAK,CAAC,MAAM,EAAE,QAAQ,KAAK,CAAC,MAAM,MAAM,IAAI,MAAM,MAAS,GAAG;AAE/F,UAAM,OAAO,QAAQ,SAAS;AAE9B,UAAM,SAAS,MAAM;AAAA,MAAQ,MAC3B,KAAK,KAAK;AAAA,QACR;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,IAAI;AACb,YAAM,OAAO,MAAM,OAAO,KAAK;AAE/B,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,MAAM,UAAU,IAAI;AAAA,MACtB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,IACN;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,MACL,IAAI;AAAA,IACN;AAAA,EACF;AACF;AAaO,SAAS,UAAU,QAAwE;AAEhG,QAAM,UAAU,QAAQ,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAI,OAAyB,GAAG;AAE9F,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,CAAC,UAAU,SAAS,OAAO,SAAS,MAAM;AAAA,EAClD;AACF;",
|
|
4
|
+
"sourcesContent": ["import type { LoaderPlugin, ResolveResult } from '@/bundle'\nimport { isRemoteUrl } from '@/bundle/bundle'\nimport { createLimiter } from '@/bundle/create-limiter'\nimport { normalize } from '@/helpers/normalize'\n\ntype FetchConfig = Partial<{\n headers: { headers: HeadersInit; domains: string[] }[]\n fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>\n}>\n\n/**\n * Safely checks for host from a URL\n * Needed because we cannot create a URL from a relative remote URL ex: examples/openapi.json\n */\nconst getHost = (url: string): string | null => {\n try {\n return new URL(url).host\n } catch {\n return null\n }\n}\n\n/**\n * Fetches and normalizes data from a remote URL\n * @param url - The URL to fetch data from\n * @returns A promise that resolves to either the normalized data or an error result\n * @example\n * ```ts\n * const result = await fetchUrl('https://api.example.com/data.json')\n * if (result.ok) {\n * console.log(result.data) // The normalized data\n * } else {\n * console.log('Failed to fetch data')\n * }\n * ```\n */\nexport async function fetchUrl(\n url: string,\n limiter: <T>(fn: () => Promise<T>) => Promise<T>,\n config?: FetchConfig,\n): Promise<ResolveResult> {\n try {\n const host = getHost(url)\n\n // Get the headers that match the domain\n const headers = config?.headers?.find((a) => a.domains.find((d) => d === host) !== undefined)?.headers\n\n const exec = config?.fetch ?? fetch\n\n const result = await limiter(() =>\n exec(url, {\n headers,\n }),\n )\n\n if (result.ok) {\n const body = await result.text()\n\n return {\n ok: true,\n data: normalize(body),\n }\n }\n\n const contentType = result.headers.get('Content-Type') ?? ''\n\n // Warn if the content type is HTML or XML as we only support JSON/YAML\n if (['text/html', 'application/xml'].includes(contentType)) {\n console.warn(`[WARN] We only support JSON/YAML formats, received ${contentType}`)\n }\n\n console.warn(`[WARN] Fetch failed with status ${result.status} ${result.statusText} for URL: ${url}`)\n return {\n ok: false,\n }\n } catch {\n console.warn(`[WARN] Failed to parse JSON/YAML from URL: ${url}`)\n return {\n ok: false,\n }\n }\n}\n\n/**\n * Creates a plugin for handling remote URL references.\n * This plugin validates and fetches data from HTTP/HTTPS URLs.\n *\n * @returns A plugin object with validate and exec functions\n * @example\n * const urlPlugin = fetchUrls()\n * if (urlPlugin.validate('https://example.com/schema.json')) {\n * const result = await urlPlugin.exec('https://example.com/schema.json')\n * }\n */\nexport function fetchUrls(config?: FetchConfig & Partial<{ limit: number | null }>): LoaderPlugin {\n // If there is a limit specified we limit the number of concurrent calls\n const limiter = config?.limit ? createLimiter(config.limit) : <T>(fn: () => Promise<T>) => fn()\n\n return {\n type: 'loader',\n validate: isRemoteUrl,\n exec: (value) => fetchUrl(value, limiter, config),\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAW1B,MAAM,UAAU,CAAC,QAA+B;AAC9C,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAgBA,eAAsB,SACpB,KACA,SACA,QACwB;AACxB,MAAI;AACF,UAAM,OAAO,QAAQ,GAAG;AAGxB,UAAM,UAAU,QAAQ,SAAS,KAAK,CAAC,MAAM,EAAE,QAAQ,KAAK,CAAC,MAAM,MAAM,IAAI,MAAM,MAAS,GAAG;AAE/F,UAAM,OAAO,QAAQ,SAAS;AAE9B,UAAM,SAAS,MAAM;AAAA,MAAQ,MAC3B,KAAK,KAAK;AAAA,QACR;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,IAAI;AACb,YAAM,OAAO,MAAM,OAAO,KAAK;AAE/B,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,MAAM,UAAU,IAAI;AAAA,MACtB;AAAA,IACF;AAEA,UAAM,cAAc,OAAO,QAAQ,IAAI,cAAc,KAAK;AAG1D,QAAI,CAAC,aAAa,iBAAiB,EAAE,SAAS,WAAW,GAAG;AAC1D,cAAQ,KAAK,sDAAsD,WAAW,EAAE;AAAA,IAClF;AAEA,YAAQ,KAAK,mCAAmC,OAAO,MAAM,IAAI,OAAO,UAAU,aAAa,GAAG,EAAE;AACpG,WAAO;AAAA,MACL,IAAI;AAAA,IACN;AAAA,EACF,QAAQ;AACN,YAAQ,KAAK,8CAA8C,GAAG,EAAE;AAChE,WAAO;AAAA,MACL,IAAI;AAAA,IACN;AAAA,EACF;AACF;AAaO,SAAS,UAAU,QAAwE;AAEhG,QAAM,UAAU,QAAQ,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAI,OAAyB,GAAG;AAE9F,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,CAAC,UAAU,SAAS,OAAO,SAAS,MAAM;AAAA,EAClD;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/bundle/plugins/parse-yaml/index.ts"],
|
|
4
|
-
"sourcesContent": ["import YAML from 'yaml'\n\nimport type { LoaderPlugin, ResolveResult } from '@/bundle'\nimport { isYaml } from '@/helpers/is-yaml'\n\n/**\n * Creates a plugin that parses YAML strings into JavaScript objects.\n * @returns A plugin object with validate and exec functions\n * @example\n * ```ts\n * const yamlPlugin = parseYaml()\n * const result = yamlPlugin.exec('name: John\\nage: 30')\n * // result = { name: 'John', age: 30 }\n * ```\n */\nexport function parseYaml(): LoaderPlugin {\n return {\n type: 'loader',\n validate: isYaml,\n exec: async (value): Promise<ResolveResult> => {\n try {\n return {\n ok: true,\n data: YAML.parse(value),\n }\n } catch {\n return {\n ok: false,\n }\n }\n },\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,OAAO,UAAU;AAGjB,SAAS,cAAc;AAYhB,SAAS,YAA0B;AACxC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,OAAO,UAAkC;AAC7C,UAAI;AACF,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,MAAM,KAAK,MAAM,
|
|
4
|
+
"sourcesContent": ["import YAML from 'yaml'\n\nimport type { LoaderPlugin, ResolveResult } from '@/bundle'\nimport { isYaml } from '@/helpers/is-yaml'\n\n/**\n * Creates a plugin that parses YAML strings into JavaScript objects.\n * @returns A plugin object with validate and exec functions\n * @example\n * ```ts\n * const yamlPlugin = parseYaml()\n * const result = yamlPlugin.exec('name: John\\nage: 30')\n * // result = { name: 'John', age: 30 }\n * ```\n */\nexport function parseYaml(): LoaderPlugin {\n return {\n type: 'loader',\n validate: isYaml,\n exec: async (value): Promise<ResolveResult> => {\n try {\n return {\n ok: true,\n data: YAML.parse(value, { merge: true, maxAliasCount: 10000 }),\n }\n } catch {\n return {\n ok: false,\n }\n }\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,UAAU;AAGjB,SAAS,cAAc;AAYhB,SAAS,YAA0B;AACxC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,OAAO,UAAkC;AAC7C,UAAI;AACF,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,MAAM,KAAK,MAAM,OAAO,EAAE,OAAO,MAAM,eAAe,IAAM,CAAC;AAAA,QAC/D;AAAA,MACF,QAAQ;AACN,eAAO;AAAA,UACL,IAAI;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/helpers/normalize.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/helpers/normalize.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,GAAG,OA6BrC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/helpers/normalize.ts"],
|
|
4
|
-
"sourcesContent": ["import { parse } from 'yaml'\n\n/**\n * Normalize a string (YAML, JSON, object) to a JavaScript datatype.\n */\nexport function normalize(content: any) {\n if (content === null) {\n return undefined\n }\n\n if (typeof content === 'string') {\n if (content.trim() === '') {\n return undefined\n }\n\n try {\n return JSON.parse(content)\n } catch (_error) {\n // Does it look like YAML?\n const hasColon = /^[^:]+:/.test(content)\n const isJson = content.slice(0, 50).trimStart().startsWith('{')\n\n if (!hasColon || isJson) {\n return undefined\n }\n\n return parse(content, {\n maxAliasCount: 10000,\n })\n }\n }\n\n return content\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,aAAa;AAKf,SAAS,UAAU,SAAc;AACtC,MAAI,YAAY,MAAM;AACpB,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,YAAY,UAAU;AAC/B,QAAI,QAAQ,KAAK,MAAM,IAAI;AACzB,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,SAAS,QAAQ;AAEf,YAAM,WAAW,UAAU,KAAK,OAAO;AACvC,YAAM,SAAS,QAAQ,MAAM,GAAG,EAAE,EAAE,UAAU,EAAE,WAAW,GAAG;AAE9D,UAAI,CAAC,YAAY,QAAQ;AACvB,eAAO;AAAA,MACT;AAEA,aAAO,MAAM,SAAS;AAAA,QACpB,eAAe;AAAA,
|
|
4
|
+
"sourcesContent": ["import { parse } from 'yaml'\n\n/**\n * Normalize a string (YAML, JSON, object) to a JavaScript datatype.\n */\nexport function normalize(content: any) {\n if (content === null) {\n return undefined\n }\n\n if (typeof content === 'string') {\n if (content.trim() === '') {\n return undefined\n }\n\n try {\n return JSON.parse(content)\n } catch (_error) {\n // Does it look like YAML?\n const hasColon = /^[^:]+:/.test(content)\n const isJson = content.slice(0, 50).trimStart().startsWith('{')\n\n if (!hasColon || isJson) {\n return undefined\n }\n\n return parse(content, {\n maxAliasCount: 10000,\n merge: true,\n })\n }\n }\n\n return content\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,aAAa;AAKf,SAAS,UAAU,SAAc;AACtC,MAAI,YAAY,MAAM;AACpB,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,YAAY,UAAU;AAC/B,QAAI,QAAQ,KAAK,MAAM,IAAI;AACzB,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,SAAS,QAAQ;AAEf,YAAM,WAAW,UAAU,KAAK,OAAO;AACvC,YAAM,SAAS,QAAQ,MAAM,GAAG,EAAE,EAAE,UAAU,EAAE,WAAW,GAAG;AAE9D,UAAI,CAAC,YAAY,QAAQ;AACvB,eAAO;AAAA,MACT;AAEA,aAAO,MAAM,SAAS;AAAA,QACpB,eAAe;AAAA,QACf,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,7 +6,7 @@ import type { UnknownObject } from '../types.js';
|
|
|
6
6
|
* Features:
|
|
7
7
|
* - If an object contains a `$ref` property, accessing the special `$ref-value` property will resolve and return the referenced value from the root object.
|
|
8
8
|
* - All nested objects and arrays are recursively wrapped in proxies, so reference resolution works at any depth.
|
|
9
|
-
* - Properties starting with
|
|
9
|
+
* - Properties starting with `__scalar_` are considered internal and are hidden by default: they return undefined on access, are excluded from enumeration, and `'in'` checks return false. This can be overridden with the `showInternal` option.
|
|
10
10
|
* - Setting, deleting, and enumerating properties works as expected, including for proxied references.
|
|
11
11
|
* - Ensures referential stability by caching proxies for the same target object.
|
|
12
12
|
*
|
|
@@ -21,17 +21,17 @@ import type { UnknownObject } from '../types.js';
|
|
|
21
21
|
* foo: { bar: 123 }
|
|
22
22
|
* },
|
|
23
23
|
* refObj: { $ref: '#/definitions/foo' },
|
|
24
|
-
*
|
|
24
|
+
* __scalar_internal: 'hidden property'
|
|
25
25
|
* }
|
|
26
26
|
* const proxy = createMagicProxy(input)
|
|
27
27
|
*
|
|
28
28
|
* // Accessing proxy.refObj['$ref-value'] will resolve to { bar: 123 }
|
|
29
29
|
* console.log(proxy.refObj['$ref-value']) // { bar: 123 }
|
|
30
30
|
*
|
|
31
|
-
* // Properties starting with
|
|
32
|
-
* console.log(proxy.
|
|
33
|
-
* console.log('
|
|
34
|
-
* console.log(Object.keys(proxy)) // ['definitions', 'refObj'] (no '
|
|
31
|
+
* // Properties starting with __scalar_ are hidden
|
|
32
|
+
* console.log(proxy.__scalar_internal) // undefined
|
|
33
|
+
* console.log('__scalar_internal' in proxy) // false
|
|
34
|
+
* console.log(Object.keys(proxy)) // ['definitions', 'refObj'] (no '__scalar_internal')
|
|
35
35
|
*
|
|
36
36
|
* // Setting and deleting properties works as expected
|
|
37
37
|
* proxy.refObj.extra = 'hello'
|
|
@@ -25,7 +25,7 @@ const createMagicProxy = (target, options, args = {
|
|
|
25
25
|
* Proxy "get" trap for magic proxy.
|
|
26
26
|
* - If accessing the special isMagicProxy symbol, return true to identify proxy.
|
|
27
27
|
* - If accessing the magicProxyTarget symbol, return the original target object.
|
|
28
|
-
* - Hide properties starting with
|
|
28
|
+
* - Hide properties starting with __scalar_ by returning undefined.
|
|
29
29
|
* - If accessing "$ref-value" and the object has a local $ref, resolve and return the referenced value as a new magic proxy.
|
|
30
30
|
* - For all other properties, recursively wrap the returned value in a magic proxy (if applicable).
|
|
31
31
|
*/
|
|
@@ -36,7 +36,7 @@ const createMagicProxy = (target, options, args = {
|
|
|
36
36
|
if (prop === magicProxyTarget) {
|
|
37
37
|
return target2;
|
|
38
38
|
}
|
|
39
|
-
if (typeof prop === "string" && prop.startsWith("
|
|
39
|
+
if (typeof prop === "string" && prop.startsWith("__scalar_") && !options?.showInternal) {
|
|
40
40
|
return void 0;
|
|
41
41
|
}
|
|
42
42
|
const ref = Reflect.get(target2, REF_KEY, receiver);
|
|
@@ -65,12 +65,12 @@ const createMagicProxy = (target, options, args = {
|
|
|
65
65
|
* Allows setting properties on the proxied object.
|
|
66
66
|
* This will update the underlying target object.
|
|
67
67
|
*
|
|
68
|
-
* Note: it will not update if the property starts with
|
|
68
|
+
* Note: it will not update if the property starts with __scalar_
|
|
69
69
|
* Those will be considered private properties by the proxy
|
|
70
70
|
*/
|
|
71
71
|
set(target2, prop, newValue, receiver) {
|
|
72
72
|
const ref = Reflect.get(target2, REF_KEY, receiver);
|
|
73
|
-
if (typeof prop === "string" && prop.startsWith("
|
|
73
|
+
if (typeof prop === "string" && prop.startsWith("__scalar_") && !options?.showInternal) {
|
|
74
74
|
return true;
|
|
75
75
|
}
|
|
76
76
|
if (prop === REF_VALUE && typeof ref === "string") {
|
|
@@ -110,11 +110,11 @@ Please fix your input file to fix this issue.`
|
|
|
110
110
|
* - Pretend that "$ref-value" exists if "$ref" exists on the target.
|
|
111
111
|
* This allows expressions like `"$ref-value" in obj` to return true for objects with a $ref,
|
|
112
112
|
* even though "$ref-value" is a virtual property provided by the proxy.
|
|
113
|
-
* - Hide properties starting with
|
|
113
|
+
* - Hide properties starting with __scalar_ by returning false.
|
|
114
114
|
* - For all other properties, defer to the default Reflect.has behavior.
|
|
115
115
|
*/
|
|
116
116
|
has(target2, prop) {
|
|
117
|
-
if (typeof prop === "string" && prop.startsWith("
|
|
117
|
+
if (typeof prop === "string" && prop.startsWith("__scalar_") && !options?.showInternal) {
|
|
118
118
|
return false;
|
|
119
119
|
}
|
|
120
120
|
if (prop === REF_VALUE && REF_KEY in target2) {
|
|
@@ -128,12 +128,12 @@ Please fix your input file to fix this issue.`
|
|
|
128
128
|
* - If the object has a "$ref" property, ensures that "$ref-value" is also included in the keys,
|
|
129
129
|
* even though "$ref-value" is a virtual property provided by the proxy.
|
|
130
130
|
* This allows Object.keys, Reflect.ownKeys, etc. to include "$ref-value" for objects with $ref.
|
|
131
|
-
* - Filters out properties starting with
|
|
131
|
+
* - Filters out properties starting with __scalar_.
|
|
132
132
|
*/
|
|
133
133
|
ownKeys(target2) {
|
|
134
134
|
const keys = Reflect.ownKeys(target2);
|
|
135
135
|
const filteredKeys = keys.filter(
|
|
136
|
-
(key) => typeof key !== "string" || !(key.startsWith("
|
|
136
|
+
(key) => typeof key !== "string" || !(key.startsWith("__scalar_") && !options?.showInternal)
|
|
137
137
|
);
|
|
138
138
|
if (REF_KEY in target2 && !filteredKeys.includes(REF_VALUE)) {
|
|
139
139
|
filteredKeys.push(REF_VALUE);
|
|
@@ -143,12 +143,12 @@ Please fix your input file to fix this issue.`
|
|
|
143
143
|
/**
|
|
144
144
|
* Proxy "getOwnPropertyDescriptor" trap for magic proxy.
|
|
145
145
|
* - For the virtual "$ref-value" property, returns a descriptor that makes it appear as a regular property.
|
|
146
|
-
* - Hide properties starting with
|
|
146
|
+
* - Hide properties starting with __scalar_ by returning undefined.
|
|
147
147
|
* - For all other properties, delegates to the default Reflect.getOwnPropertyDescriptor behavior.
|
|
148
148
|
* - This ensures that Object.getOwnPropertyDescriptor and similar methods work correctly with the virtual property.
|
|
149
149
|
*/
|
|
150
150
|
getOwnPropertyDescriptor(target2, prop) {
|
|
151
|
-
if (typeof prop === "string" && prop.startsWith("
|
|
151
|
+
if (typeof prop === "string" && prop.startsWith("__scalar_") && !options?.showInternal) {
|
|
152
152
|
return void 0;
|
|
153
153
|
}
|
|
154
154
|
const ref = Reflect.get(target2, REF_KEY);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/magic-proxy/proxy.ts"],
|
|
4
|
-
"sourcesContent": ["import { convertToLocalRef } from '@/helpers/convert-to-local-ref'\nimport { getId, getSchemas } from '@/helpers/get-schemas'\nimport { getValueByPath } from '@/helpers/get-value-by-path'\nimport { isObject } from '@/helpers/is-object'\nimport { createPathFromSegments, parseJsonPointer } from '@/helpers/json-path-utils'\nimport type { UnknownObject } from '@/types'\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 * Features:\n * - If an object contains a `$ref` property, accessing the special `$ref-value` property 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 works at any depth.\n * - Properties starting with an underscore (_) are considered internal and are hidden by default: they return undefined on access, are excluded from enumeration, and `'in'` checks return false. This can be overridden with the `showInternal` option.\n * - Setting, deleting, and enumerating properties works as expected, including for proxied references.\n * - Ensures referential stability by caching proxies for the same target object.\n *\n * @param target - The object or array to wrap in a magic proxy\n * @param options - Optional settings (e.g., showInternal to expose internal properties)\n * @param args - Internal arguments for advanced usage (root object, proxy/cache maps, current context)\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?: Partial<{ showInternal: boolean }>,\n args: {\n /**\n * The root object for resolving local JSON references.\n */\n root: S | T\n /**\n * Cache to store already created proxies for target objects to ensure referential stability.\n *\n * It is helpful when dealing with reactive frameworks like Vue,\n */\n proxyCache: WeakMap<object, T>\n /**\n * Cache to store resolved JSON references.\n */\n cache: Map<string, unknown>\n /**\n * Map of all schemas by their $id or $anchor for cross-document reference resolution.\n */\n schemas: Map<string, string>\n /**\n * The current JSON path context within the root object.\n *\n * Used to resolve $anchor references correctly.\n */\n currentContext: string\n } = {\n root: target,\n proxyCache: new WeakMap(),\n cache: new Map(),\n schemas: getSchemas(target),\n currentContext: '',\n },\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 (args.proxyCache.has(target)) {\n return args.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 // 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 // Get the $ref value of the current target (if any)\n const ref = Reflect.get(target, REF_KEY, receiver)\n // Get the identifier ($id) of the current target for context tracking\n const id = getId(target)\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') {\n // Check cache first for performance optimization\n if (args.cache.has(ref)) {\n return args.cache.get(ref)\n }\n\n const path = convertToLocalRef(ref, id ?? args.currentContext, args.schemas)\n\n if (path === undefined) {\n return undefined\n }\n\n // Resolve the reference and create a new magic proxy\n const resolvedValue = getValueByPath(args.root, parseJsonPointer(`#/${path}`))\n const proxiedValue = createMagicProxy(resolvedValue.value, options, {\n ...args,\n currentContext: resolvedValue.context,\n })\n\n // Store in cache for future lookups\n args.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, { ...args, currentContext: id ?? args.currentContext })\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') {\n const id = getId(target)\n const path = convertToLocalRef(ref, id ?? args.currentContext, args.schemas)\n\n if (path === undefined) {\n return undefined\n }\n\n const segments = parseJsonPointer(`#/${path}`)\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(args.root, segments.slice(0, -1)).value\n\n if (getParentNode() === undefined) {\n createPathFromSegments(args.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 args.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": "AAAA,SAAS,yBAAyB;AAClC,SAAS,OAAO,kBAAkB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,gBAAgB;AACzB,SAAS,wBAAwB,wBAAwB;AAGzD,MAAM,eAAe,OAAO,cAAc;AAC1C,MAAM,mBAAmB,OAAO,kBAAkB;AAElD,MAAM,YAAY;AAClB,MAAM,UAAU;AAwCT,MAAM,mBAAmB,CAC9B,QACA,SACA,OAyBI;AAAA,EACF,MAAM;AAAA,EACN,YAAY,oBAAI,QAAQ;AAAA,EACxB,OAAO,oBAAI,IAAI;AAAA,EACf,SAAS,WAAW,MAAM;AAAA,EAC1B,gBAAgB;AAClB,MACG;AACH,MAAI,CAAC,SAAS,MAAM,KAAK,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC/C,WAAO;AAAA,EACT;AAGA,MAAI,KAAK,WAAW,IAAI,MAAM,GAAG;AAC/B,WAAO,KAAK,WAAW,IAAI,MAAM;AAAA,EACnC;AAEA,QAAM,UAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAS/B,IAAIA,SAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,cAAc;AAEzB,eAAO;AAAA,MACT;AAEA,UAAI,SAAS,kBAAkB;AAE7B,eAAOA;AAAA,MACT;AAIA,UAAI,OAAO,SAAS,YAAY,KAAK,WAAW,
|
|
4
|
+
"sourcesContent": ["import { convertToLocalRef } from '@/helpers/convert-to-local-ref'\nimport { getId, getSchemas } from '@/helpers/get-schemas'\nimport { getValueByPath } from '@/helpers/get-value-by-path'\nimport { isObject } from '@/helpers/is-object'\nimport { createPathFromSegments, parseJsonPointer } from '@/helpers/json-path-utils'\nimport type { UnknownObject } from '@/types'\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 * Features:\n * - If an object contains a `$ref` property, accessing the special `$ref-value` property 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 works at any depth.\n * - Properties starting with `__scalar_` are considered internal and are hidden by default: they return undefined on access, are excluded from enumeration, and `'in'` checks return false. This can be overridden with the `showInternal` option.\n * - Setting, deleting, and enumerating properties works as expected, including for proxied references.\n * - Ensures referential stability by caching proxies for the same target object.\n *\n * @param target - The object or array to wrap in a magic proxy\n * @param options - Optional settings (e.g., showInternal to expose internal properties)\n * @param args - Internal arguments for advanced usage (root object, proxy/cache maps, current context)\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 * __scalar_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 __scalar_ are hidden\n * console.log(proxy.__scalar_internal) // undefined\n * console.log('__scalar_internal' in proxy) // false\n * console.log(Object.keys(proxy)) // ['definitions', 'refObj'] (no '__scalar_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?: Partial<{ showInternal: boolean }>,\n args: {\n /**\n * The root object for resolving local JSON references.\n */\n root: S | T\n /**\n * Cache to store already created proxies for target objects to ensure referential stability.\n *\n * It is helpful when dealing with reactive frameworks like Vue,\n */\n proxyCache: WeakMap<object, T>\n /**\n * Cache to store resolved JSON references.\n */\n cache: Map<string, unknown>\n /**\n * Map of all schemas by their $id or $anchor for cross-document reference resolution.\n */\n schemas: Map<string, string>\n /**\n * The current JSON path context within the root object.\n *\n * Used to resolve $anchor references correctly.\n */\n currentContext: string\n } = {\n root: target,\n proxyCache: new WeakMap(),\n cache: new Map(),\n schemas: getSchemas(target),\n currentContext: '',\n },\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 (args.proxyCache.has(target)) {\n return args.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 __scalar_ 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 // Hide properties starting with __scalar_ - these are considered internal/private properties\n // and should not be accessible through the magic proxy interface\n if (typeof prop === 'string' && prop.startsWith('__scalar_') && !options?.showInternal) {\n return undefined\n }\n\n // Get the $ref value of the current target (if any)\n const ref = Reflect.get(target, REF_KEY, receiver)\n // Get the identifier ($id) of the current target for context tracking\n const id = getId(target)\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') {\n // Check cache first for performance optimization\n if (args.cache.has(ref)) {\n return args.cache.get(ref)\n }\n\n const path = convertToLocalRef(ref, id ?? args.currentContext, args.schemas)\n\n if (path === undefined) {\n return undefined\n }\n\n // Resolve the reference and create a new magic proxy\n const resolvedValue = getValueByPath(args.root, parseJsonPointer(`#/${path}`))\n const proxiedValue = createMagicProxy(resolvedValue.value, options, {\n ...args,\n currentContext: resolvedValue.context,\n })\n\n // Store in cache for future lookups\n args.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, { ...args, currentContext: id ?? args.currentContext })\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 __scalar_\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('__scalar_') && !options?.showInternal) {\n return true\n }\n\n if (prop === REF_VALUE && typeof ref === 'string') {\n const id = getId(target)\n const path = convertToLocalRef(ref, id ?? args.currentContext, args.schemas)\n\n if (path === undefined) {\n return undefined\n }\n\n const segments = parseJsonPointer(`#/${path}`)\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(args.root, segments.slice(0, -1)).value\n\n if (getParentNode() === undefined) {\n createPathFromSegments(args.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 __scalar_ 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 __scalar_\n if (typeof prop === 'string' && prop.startsWith('__scalar_') && !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 __scalar_.\n */\n ownKeys(target) {\n const keys = Reflect.ownKeys(target)\n\n // Filter out properties starting with __scalar_\n const filteredKeys = keys.filter(\n (key) => typeof key !== 'string' || !(key.startsWith('__scalar_') && !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 __scalar_ 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 __scalar_\n if (typeof prop === 'string' && prop.startsWith('__scalar_') && !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 args.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": "AAAA,SAAS,yBAAyB;AAClC,SAAS,OAAO,kBAAkB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,gBAAgB;AACzB,SAAS,wBAAwB,wBAAwB;AAGzD,MAAM,eAAe,OAAO,cAAc;AAC1C,MAAM,mBAAmB,OAAO,kBAAkB;AAElD,MAAM,YAAY;AAClB,MAAM,UAAU;AAwCT,MAAM,mBAAmB,CAC9B,QACA,SACA,OAyBI;AAAA,EACF,MAAM;AAAA,EACN,YAAY,oBAAI,QAAQ;AAAA,EACxB,OAAO,oBAAI,IAAI;AAAA,EACf,SAAS,WAAW,MAAM;AAAA,EAC1B,gBAAgB;AAClB,MACG;AACH,MAAI,CAAC,SAAS,MAAM,KAAK,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC/C,WAAO;AAAA,EACT;AAGA,MAAI,KAAK,WAAW,IAAI,MAAM,GAAG;AAC/B,WAAO,KAAK,WAAW,IAAI,MAAM;AAAA,EACnC;AAEA,QAAM,UAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAS/B,IAAIA,SAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,cAAc;AAEzB,eAAO;AAAA,MACT;AAEA,UAAI,SAAS,kBAAkB;AAE7B,eAAOA;AAAA,MACT;AAIA,UAAI,OAAO,SAAS,YAAY,KAAK,WAAW,WAAW,KAAK,CAAC,SAAS,cAAc;AACtF,eAAO;AAAA,MACT;AAGA,YAAM,MAAM,QAAQ,IAAIA,SAAQ,SAAS,QAAQ;AAEjD,YAAM,KAAK,MAAMA,OAAM;AAGvB,UAAI,SAAS,aAAa,OAAO,QAAQ,UAAU;AAEjD,YAAI,KAAK,MAAM,IAAI,GAAG,GAAG;AACvB,iBAAO,KAAK,MAAM,IAAI,GAAG;AAAA,QAC3B;AAEA,cAAM,OAAO,kBAAkB,KAAK,MAAM,KAAK,gBAAgB,KAAK,OAAO;AAE3E,YAAI,SAAS,QAAW;AACtB,iBAAO;AAAA,QACT;AAGA,cAAM,gBAAgB,eAAe,KAAK,MAAM,iBAAiB,KAAK,IAAI,EAAE,CAAC;AAC7E,cAAM,eAAe,iBAAiB,cAAc,OAAO,SAAS;AAAA,UAClE,GAAG;AAAA,UACH,gBAAgB,cAAc;AAAA,QAChC,CAAC;AAGD,aAAK,MAAM,IAAI,KAAK,YAAY;AAChC,eAAO;AAAA,MACT;AAGA,YAAM,QAAQ,QAAQ,IAAIA,SAAQ,MAAM,QAAQ;AAChD,aAAO,iBAAiB,OAAY,SAAS,EAAE,GAAG,MAAM,gBAAgB,MAAM,KAAK,eAAe,CAAC;AAAA,IACrG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,IAAIA,SAAQ,MAAM,UAAU,UAAU;AACpC,YAAM,MAAM,QAAQ,IAAIA,SAAQ,SAAS,QAAQ;AAEjD,UAAI,OAAO,SAAS,YAAY,KAAK,WAAW,WAAW,KAAK,CAAC,SAAS,cAAc;AACtF,eAAO;AAAA,MACT;AAEA,UAAI,SAAS,aAAa,OAAO,QAAQ,UAAU;AACjD,cAAM,KAAK,MAAMA,OAAM;AACvB,cAAM,OAAO,kBAAkB,KAAK,MAAM,KAAK,gBAAgB,KAAK,OAAO;AAE3E,YAAI,SAAS,QAAW;AACtB,iBAAO;AAAA,QACT;AAEA,cAAM,WAAW,iBAAiB,KAAK,IAAI,EAAE;AAE7C,YAAI,SAAS,WAAW,GAAG;AACzB,iBAAO;AAAA,QACT;AAGA,cAAM,gBAAgB,MAAM,eAAe,KAAK,MAAM,SAAS,MAAM,GAAG,EAAE,CAAC,EAAE;AAE7E,YAAI,cAAc,MAAM,QAAW;AACjC,iCAAuB,KAAK,MAAM,SAAS,MAAM,GAAG,EAAE,CAAC;AAGvD,kBAAQ;AAAA,YACN,mDAAmD,GAAG;AAAA;AAAA;AAAA,UACxD;AAAA,QACF;AAGA,sBAAc,EAAE,SAAS,GAAG,EAAE,CAAC,IAAI;AACnC,eAAO;AAAA,MACT;AAEA,aAAO,QAAQ,IAAIA,SAAQ,MAAM,UAAU,QAAQ;AAAA,IACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,eAAeA,SAAQ,MAAM;AAC3B,aAAO,QAAQ,eAAeA,SAAQ,IAAI;AAAA,IAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,IAAIA,SAAQ,MAAM;AAEhB,UAAI,OAAO,SAAS,YAAY,KAAK,WAAW,WAAW,KAAK,CAAC,SAAS,cAAc;AACtF,eAAO;AAAA,MACT;AAGA,UAAI,SAAS,aAAa,WAAWA,SAAQ;AAC3C,eAAO;AAAA,MACT;AACA,aAAO,QAAQ,IAAIA,SAAQ,IAAI;AAAA,IACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,QAAQA,SAAQ;AACd,YAAM,OAAO,QAAQ,QAAQA,OAAM;AAGnC,YAAM,eAAe,KAAK;AAAA,QACxB,CAAC,QAAQ,OAAO,QAAQ,YAAY,EAAE,IAAI,WAAW,WAAW,KAAK,CAAC,SAAS;AAAA,MACjF;AAEA,UAAI,WAAWA,WAAU,CAAC,aAAa,SAAS,SAAS,GAAG;AAC1D,qBAAa,KAAK,SAAS;AAAA,MAC7B;AACA,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,yBAAyBA,SAAQ,MAAM;AAErC,UAAI,OAAO,SAAS,YAAY,KAAK,WAAW,WAAW,KAAK,CAAC,SAAS,cAAc;AACtF,eAAO;AAAA,MACT;AAEA,YAAM,MAAM,QAAQ,IAAIA,SAAQ,OAAO;AAEvC,UAAI,SAAS,aAAa,OAAO,QAAQ,UAAU;AACjD,eAAO;AAAA,UACL,cAAc;AAAA,UACd,YAAY;AAAA,UACZ,OAAO;AAAA,UACP,UAAU;AAAA,QACZ;AAAA,MACF;AAGA,aAAO,QAAQ,yBAAyBA,SAAQ,IAAI;AAAA,IACtD;AAAA,EACF;AAEA,QAAM,UAAU,IAAI,MAAS,QAAQ,OAAO;AAC5C,OAAK,WAAW,IAAI,QAAQ,OAAO;AACnC,SAAO;AACT;AAYO,SAAS,OAAU,KAAW;AACnC,MAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,WAAO;AAAA,EACT;AAEA,MAAK,IAAoD,YAAY,GAAG;AACtE,WAAQ,IAAsC,gBAAgB;AAAA,EAChE;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": ["target"]
|
|
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.
|
|
13
|
+
"version": "0.6.1",
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=20"
|
|
16
16
|
},
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"yaml": "2.8.0",
|
|
57
|
-
"@scalar/helpers": "0.0.
|
|
57
|
+
"@scalar/helpers": "0.0.12"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"fastify": "^5.3.3",
|
|
@@ -62,10 +62,19 @@ export async function fetchUrl(
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const contentType = result.headers.get('Content-Type') ?? ''
|
|
66
|
+
|
|
67
|
+
// Warn if the content type is HTML or XML as we only support JSON/YAML
|
|
68
|
+
if (['text/html', 'application/xml'].includes(contentType)) {
|
|
69
|
+
console.warn(`[WARN] We only support JSON/YAML formats, received ${contentType}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.warn(`[WARN] Fetch failed with status ${result.status} ${result.statusText} for URL: ${url}`)
|
|
65
73
|
return {
|
|
66
74
|
ok: false,
|
|
67
75
|
}
|
|
68
76
|
} catch {
|
|
77
|
+
console.warn(`[WARN] Failed to parse JSON/YAML from URL: ${url}`)
|
|
69
78
|
return {
|
|
70
79
|
ok: false,
|
|
71
80
|
}
|
package/src/helpers/normalize.ts
CHANGED
|
@@ -1154,41 +1154,41 @@ describe('createMagicProxy', () => {
|
|
|
1154
1154
|
})
|
|
1155
1155
|
|
|
1156
1156
|
describe('show underscore properties when specified', () => {
|
|
1157
|
-
it('should not hide properties starting with
|
|
1157
|
+
it('should not hide properties starting with __scalar_ from direct access', () => {
|
|
1158
1158
|
const input = {
|
|
1159
1159
|
public: 'visible',
|
|
1160
|
-
|
|
1161
|
-
|
|
1160
|
+
__scalar_private: 'hidden',
|
|
1161
|
+
__scalar_internal: 'also hidden',
|
|
1162
1162
|
normal_underscore: 'visible with underscore in middle',
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
1165
|
const result = createMagicProxy(input, { showInternal: true })
|
|
1166
1166
|
|
|
1167
1167
|
expect(result.public).toBe('visible')
|
|
1168
|
-
expect(result.
|
|
1169
|
-
expect(result.
|
|
1168
|
+
expect(result.__scalar_private).toBe('hidden')
|
|
1169
|
+
expect(result.__scalar_internal).toBe('also hidden')
|
|
1170
1170
|
expect(result.normal_underscore).toBe('visible with underscore in middle')
|
|
1171
1171
|
})
|
|
1172
1172
|
|
|
1173
|
-
it('should not hide
|
|
1173
|
+
it('should not hide __scalar_ properties from "in" operator', () => {
|
|
1174
1174
|
const input = {
|
|
1175
1175
|
public: 'visible',
|
|
1176
|
-
|
|
1177
|
-
|
|
1176
|
+
__scalar_private: 'hidden',
|
|
1177
|
+
__scalar_internal: 'also hidden',
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
1180
|
const result = createMagicProxy(input, { showInternal: true })
|
|
1181
1181
|
|
|
1182
1182
|
expect('public' in result).toBe(true)
|
|
1183
|
-
expect('
|
|
1184
|
-
expect('
|
|
1183
|
+
expect('__scalar_private' in result).toBe(true)
|
|
1184
|
+
expect('__scalar_internal' in result).toBe(true)
|
|
1185
1185
|
})
|
|
1186
1186
|
|
|
1187
|
-
it('should not exclude
|
|
1187
|
+
it('should not exclude __scalar_ properties from Object.keys enumeration', () => {
|
|
1188
1188
|
const input = {
|
|
1189
1189
|
public: 'visible',
|
|
1190
|
-
|
|
1191
|
-
|
|
1190
|
+
__scalar_private: 'hidden',
|
|
1191
|
+
__scalar_internal: 'also hidden',
|
|
1192
1192
|
another: 'visible',
|
|
1193
1193
|
}
|
|
1194
1194
|
|
|
@@ -1197,79 +1197,79 @@ describe('createMagicProxy', () => {
|
|
|
1197
1197
|
|
|
1198
1198
|
expect(keys).toContain('public')
|
|
1199
1199
|
expect(keys).toContain('another')
|
|
1200
|
-
expect(keys).toContain('
|
|
1201
|
-
expect(keys).toContain('
|
|
1200
|
+
expect(keys).toContain('__scalar_private')
|
|
1201
|
+
expect(keys).toContain('__scalar_internal')
|
|
1202
1202
|
})
|
|
1203
1203
|
|
|
1204
|
-
it('should not hide
|
|
1204
|
+
it('should not hide __scalar_ properties from getOwnPropertyDescriptor', () => {
|
|
1205
1205
|
const input = {
|
|
1206
1206
|
public: 'visible',
|
|
1207
|
-
|
|
1207
|
+
__scalar_private: 'hidden',
|
|
1208
1208
|
}
|
|
1209
1209
|
|
|
1210
1210
|
const result = createMagicProxy(input, { showInternal: true })
|
|
1211
1211
|
|
|
1212
1212
|
expect(Object.getOwnPropertyDescriptor(result, 'public')).toBeDefined()
|
|
1213
|
-
expect(Object.getOwnPropertyDescriptor(result, '
|
|
1213
|
+
expect(Object.getOwnPropertyDescriptor(result, '__scalar_private')).toBeDefined()
|
|
1214
1214
|
})
|
|
1215
1215
|
|
|
1216
|
-
it('should not hide
|
|
1216
|
+
it('should not hide __scalar_ properties in nested objects', () => {
|
|
1217
1217
|
const input = {
|
|
1218
1218
|
nested: {
|
|
1219
1219
|
public: 'visible',
|
|
1220
|
-
|
|
1220
|
+
__scalar_private: 'hidden',
|
|
1221
1221
|
deeper: {
|
|
1222
|
-
|
|
1222
|
+
__scalar_alsoHidden: 'secret',
|
|
1223
1223
|
visible: 'shown',
|
|
1224
1224
|
},
|
|
1225
1225
|
},
|
|
1226
|
-
|
|
1226
|
+
__scalar_topLevel: 'hidden',
|
|
1227
1227
|
}
|
|
1228
1228
|
|
|
1229
1229
|
const result = createMagicProxy(input, { showInternal: true })
|
|
1230
1230
|
|
|
1231
|
-
expect(result.
|
|
1231
|
+
expect(result.__scalar_topLevel).toBe('hidden')
|
|
1232
1232
|
expect(result.nested.public).toBe('visible')
|
|
1233
|
-
expect(result.nested.
|
|
1234
|
-
expect(result.nested.deeper.
|
|
1233
|
+
expect(result.nested.__scalar_private).toBe('hidden')
|
|
1234
|
+
expect(result.nested.deeper.__scalar_alsoHidden).toBe('secret')
|
|
1235
1235
|
expect(result.nested.deeper.visible).toBe('shown')
|
|
1236
1236
|
})
|
|
1237
1237
|
|
|
1238
|
-
it('should show
|
|
1238
|
+
it('should show __scalar_ properties with arrays containing objects with __scalar_ properties', () => {
|
|
1239
1239
|
const input = {
|
|
1240
1240
|
items: [
|
|
1241
|
-
{ public: 'item1',
|
|
1242
|
-
{ public: 'item2',
|
|
1241
|
+
{ public: 'item1', __scalar_private: 'hidden1' },
|
|
1242
|
+
{ public: 'item2', __scalar_private: 'hidden2' },
|
|
1243
1243
|
],
|
|
1244
1244
|
}
|
|
1245
1245
|
|
|
1246
1246
|
const result = createMagicProxy(input, { showInternal: true })
|
|
1247
1247
|
|
|
1248
1248
|
expect(result.items[0].public).toBe('item1')
|
|
1249
|
-
expect(result.items[0].
|
|
1249
|
+
expect(result.items[0].__scalar_private).toBe('hidden1')
|
|
1250
1250
|
expect(result.items[1].public).toBe('item2')
|
|
1251
|
-
expect(result.items[1].
|
|
1251
|
+
expect(result.items[1].__scalar_private).toBe('hidden2')
|
|
1252
1252
|
})
|
|
1253
1253
|
|
|
1254
|
-
it('should show
|
|
1254
|
+
it('should show __scalar_ ref properties', () => {
|
|
1255
1255
|
const input = {
|
|
1256
1256
|
definitions: {
|
|
1257
1257
|
example: {
|
|
1258
1258
|
value: 'hello',
|
|
1259
|
-
|
|
1259
|
+
__scalar_internal: 'hidden',
|
|
1260
1260
|
},
|
|
1261
1261
|
},
|
|
1262
|
-
|
|
1262
|
+
__scalar_hiddenRef: { $ref: '#/definitions/example' },
|
|
1263
1263
|
publicRef: { $ref: '#/definitions/example' },
|
|
1264
1264
|
}
|
|
1265
1265
|
|
|
1266
1266
|
const result = createMagicProxy(input, { showInternal: true })
|
|
1267
1267
|
|
|
1268
|
-
//
|
|
1269
|
-
expect(result.
|
|
1268
|
+
// __scalar_ property should be hidden
|
|
1269
|
+
expect(result.__scalar_hiddenRef).toEqual({
|
|
1270
1270
|
'$ref': '#/definitions/example',
|
|
1271
1271
|
'$ref-value': {
|
|
1272
|
-
'
|
|
1272
|
+
'__scalar_internal': 'hidden',
|
|
1273
1273
|
'value': 'hello',
|
|
1274
1274
|
},
|
|
1275
1275
|
})
|
|
@@ -1277,8 +1277,8 @@ describe('createMagicProxy', () => {
|
|
|
1277
1277
|
// Public ref should work normally
|
|
1278
1278
|
expect(result.publicRef['$ref-value'].value).toBe('hello')
|
|
1279
1279
|
|
|
1280
|
-
//
|
|
1281
|
-
expect(result.publicRef['$ref-value'].
|
|
1280
|
+
// __scalar_ properties in referenced objects should be hidden
|
|
1281
|
+
expect(result.publicRef['$ref-value'].__scalar_internal).toBe('hidden')
|
|
1282
1282
|
})
|
|
1283
1283
|
})
|
|
1284
1284
|
|
|
@@ -1830,42 +1830,46 @@ describe('createMagicProxy', () => {
|
|
|
1830
1830
|
})
|
|
1831
1831
|
})
|
|
1832
1832
|
|
|
1833
|
-
describe('hide
|
|
1834
|
-
it('should hide properties starting with
|
|
1833
|
+
describe('hide __scalar_ properties', () => {
|
|
1834
|
+
it('should hide properties starting with __scalar_ from direct access', () => {
|
|
1835
1835
|
const input = {
|
|
1836
1836
|
public: 'visible',
|
|
1837
|
-
|
|
1838
|
-
|
|
1837
|
+
__scalar_private: 'hidden',
|
|
1838
|
+
__scalar_internal: 'also hidden',
|
|
1839
1839
|
normal_underscore: 'visible with underscore in middle',
|
|
1840
|
+
_id: 'legitimate user property', // This should NOT be hidden
|
|
1841
|
+
_type: 'another legitimate property', // This should NOT be hidden
|
|
1840
1842
|
}
|
|
1841
1843
|
|
|
1842
1844
|
const result = createMagicProxy(input)
|
|
1843
1845
|
|
|
1844
1846
|
expect(result.public).toBe('visible')
|
|
1845
|
-
expect(result.
|
|
1846
|
-
expect(result.
|
|
1847
|
+
expect(result.__scalar_private).toBe(undefined)
|
|
1848
|
+
expect(result.__scalar_internal).toBe(undefined)
|
|
1847
1849
|
expect(result.normal_underscore).toBe('visible with underscore in middle')
|
|
1850
|
+
expect(result._id).toBe('legitimate user property') // Should be visible
|
|
1851
|
+
expect(result._type).toBe('another legitimate property') // Should be visible
|
|
1848
1852
|
})
|
|
1849
1853
|
|
|
1850
|
-
it('should hide
|
|
1854
|
+
it('should hide __scalar_ properties from "in" operator', () => {
|
|
1851
1855
|
const input = {
|
|
1852
1856
|
public: 'visible',
|
|
1853
|
-
|
|
1854
|
-
|
|
1857
|
+
__scalar_private: 'hidden',
|
|
1858
|
+
__scalar_internal: 'also hidden',
|
|
1855
1859
|
}
|
|
1856
1860
|
|
|
1857
1861
|
const result = createMagicProxy(input)
|
|
1858
1862
|
|
|
1859
1863
|
expect('public' in result).toBe(true)
|
|
1860
|
-
expect('
|
|
1861
|
-
expect('
|
|
1864
|
+
expect('__scalar_private' in result).toBe(false)
|
|
1865
|
+
expect('__scalar_internal' in result).toBe(false)
|
|
1862
1866
|
})
|
|
1863
1867
|
|
|
1864
|
-
it('should exclude
|
|
1868
|
+
it('should exclude __scalar_ properties from Object.keys enumeration', () => {
|
|
1865
1869
|
const input = {
|
|
1866
1870
|
public: 'visible',
|
|
1867
|
-
|
|
1868
|
-
|
|
1871
|
+
__scalar_private: 'hidden',
|
|
1872
|
+
__scalar_internal: 'also hidden',
|
|
1869
1873
|
another: 'visible',
|
|
1870
1874
|
}
|
|
1871
1875
|
|
|
@@ -1874,82 +1878,110 @@ describe('createMagicProxy', () => {
|
|
|
1874
1878
|
|
|
1875
1879
|
expect(keys).toContain('public')
|
|
1876
1880
|
expect(keys).toContain('another')
|
|
1877
|
-
expect(keys).not.toContain('
|
|
1878
|
-
expect(keys).not.toContain('
|
|
1881
|
+
expect(keys).not.toContain('__scalar_private')
|
|
1882
|
+
expect(keys).not.toContain('__scalar_internal')
|
|
1879
1883
|
})
|
|
1880
1884
|
|
|
1881
|
-
it('should hide
|
|
1885
|
+
it('should hide __scalar_ properties from getOwnPropertyDescriptor', () => {
|
|
1882
1886
|
const input = {
|
|
1883
1887
|
public: 'visible',
|
|
1884
|
-
|
|
1888
|
+
__scalar_private: 'hidden',
|
|
1885
1889
|
}
|
|
1886
1890
|
|
|
1887
1891
|
const result = createMagicProxy(input)
|
|
1888
1892
|
|
|
1889
1893
|
expect(Object.getOwnPropertyDescriptor(result, 'public')).toBeDefined()
|
|
1890
|
-
expect(Object.getOwnPropertyDescriptor(result, '
|
|
1894
|
+
expect(Object.getOwnPropertyDescriptor(result, '__scalar_private')).toBe(undefined)
|
|
1891
1895
|
})
|
|
1892
1896
|
|
|
1893
|
-
it('should hide
|
|
1897
|
+
it('should hide __scalar_ properties in nested objects', () => {
|
|
1894
1898
|
const input = {
|
|
1895
1899
|
nested: {
|
|
1896
1900
|
public: 'visible',
|
|
1897
|
-
|
|
1901
|
+
__scalar_private: 'hidden',
|
|
1898
1902
|
deeper: {
|
|
1899
|
-
|
|
1903
|
+
__scalar_alsoHidden: 'secret',
|
|
1900
1904
|
visible: 'shown',
|
|
1901
1905
|
},
|
|
1902
1906
|
},
|
|
1903
|
-
|
|
1907
|
+
__scalar_topLevel: 'hidden',
|
|
1904
1908
|
}
|
|
1905
1909
|
|
|
1906
1910
|
const result = createMagicProxy(input)
|
|
1907
1911
|
|
|
1908
|
-
expect(result.
|
|
1912
|
+
expect(result.__scalar_topLevel).toBe(undefined)
|
|
1909
1913
|
expect(result.nested.public).toBe('visible')
|
|
1910
|
-
expect(result.nested.
|
|
1911
|
-
expect(result.nested.deeper.
|
|
1914
|
+
expect(result.nested.__scalar_private).toBe(undefined)
|
|
1915
|
+
expect(result.nested.deeper.__scalar_alsoHidden).toBe(undefined)
|
|
1912
1916
|
expect(result.nested.deeper.visible).toBe('shown')
|
|
1913
1917
|
})
|
|
1914
1918
|
|
|
1915
|
-
it('should work with arrays containing objects with
|
|
1919
|
+
it('should work with arrays containing objects with __scalar_ properties', () => {
|
|
1916
1920
|
const input = {
|
|
1917
1921
|
items: [
|
|
1918
|
-
{ public: 'item1',
|
|
1919
|
-
{ public: 'item2',
|
|
1922
|
+
{ public: 'item1', __scalar_private: 'hidden1' },
|
|
1923
|
+
{ public: 'item2', __scalar_private: 'hidden2' },
|
|
1920
1924
|
],
|
|
1921
1925
|
}
|
|
1922
1926
|
|
|
1923
1927
|
const result = createMagicProxy(input)
|
|
1924
1928
|
|
|
1925
1929
|
expect(result.items[0].public).toBe('item1')
|
|
1926
|
-
expect(result.items[0].
|
|
1930
|
+
expect(result.items[0].__scalar_private).toBe(undefined)
|
|
1927
1931
|
expect(result.items[1].public).toBe('item2')
|
|
1928
|
-
expect(result.items[1].
|
|
1932
|
+
expect(result.items[1].__scalar_private).toBe(undefined)
|
|
1929
1933
|
})
|
|
1930
1934
|
|
|
1931
|
-
it('should still allow refs to work with
|
|
1935
|
+
it('should still allow refs to work with __scalar_ hiding', () => {
|
|
1932
1936
|
const input = {
|
|
1933
1937
|
definitions: {
|
|
1934
1938
|
example: {
|
|
1935
1939
|
value: 'hello',
|
|
1936
|
-
|
|
1940
|
+
__scalar_internal: 'hidden',
|
|
1937
1941
|
},
|
|
1938
1942
|
},
|
|
1939
|
-
|
|
1943
|
+
__scalar_hiddenRef: { $ref: '#/definitions/example' },
|
|
1940
1944
|
publicRef: { $ref: '#/definitions/example' },
|
|
1941
1945
|
}
|
|
1942
1946
|
|
|
1943
1947
|
const result = createMagicProxy(input)
|
|
1944
1948
|
|
|
1945
|
-
//
|
|
1946
|
-
expect(result.
|
|
1949
|
+
// __scalar_ property should be hidden
|
|
1950
|
+
expect(result.__scalar_hiddenRef).toBe(undefined)
|
|
1947
1951
|
|
|
1948
1952
|
// Public ref should work normally
|
|
1949
1953
|
expect(result.publicRef['$ref-value'].value).toBe('hello')
|
|
1950
1954
|
|
|
1951
|
-
//
|
|
1952
|
-
expect(result.publicRef['$ref-value'].
|
|
1955
|
+
// __scalar_ properties in referenced objects should be hidden
|
|
1956
|
+
expect(result.publicRef['$ref-value'].__scalar_internal).toBe(undefined)
|
|
1957
|
+
})
|
|
1958
|
+
|
|
1959
|
+
it('preserves regular underscore properties', () => {
|
|
1960
|
+
const input = {
|
|
1961
|
+
_id: 'user123',
|
|
1962
|
+
user_name: 'john',
|
|
1963
|
+
__version: '1.0',
|
|
1964
|
+
normal_property: 'visible',
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const result = createMagicProxy(input)
|
|
1968
|
+
|
|
1969
|
+
// Regular underscore properties should be visible
|
|
1970
|
+
expect(result._id).toBe('user123')
|
|
1971
|
+
expect(result.user_name).toBe('john')
|
|
1972
|
+
expect(result.__version).toBe('1.0')
|
|
1973
|
+
expect(result.normal_property).toBe('visible')
|
|
1974
|
+
|
|
1975
|
+
// Should be included in enumeration and "in" checks
|
|
1976
|
+
expect('_id' in result).toBe(true)
|
|
1977
|
+
expect('user_name' in result).toBe(true)
|
|
1978
|
+
expect('__version' in result).toBe(true)
|
|
1979
|
+
|
|
1980
|
+
const keys = Object.keys(result)
|
|
1981
|
+
expect(keys).toContain('_id')
|
|
1982
|
+
expect(keys).toContain('user_name')
|
|
1983
|
+
expect(keys).toContain('__version')
|
|
1984
|
+
expect(keys).toContain('normal_property')
|
|
1953
1985
|
})
|
|
1954
1986
|
})
|
|
1955
1987
|
})
|
package/src/magic-proxy/proxy.ts
CHANGED
|
@@ -18,7 +18,7 @@ const REF_KEY = '$ref'
|
|
|
18
18
|
* Features:
|
|
19
19
|
* - If an object contains a `$ref` property, accessing the special `$ref-value` property will resolve and return the referenced value from the root object.
|
|
20
20
|
* - All nested objects and arrays are recursively wrapped in proxies, so reference resolution works at any depth.
|
|
21
|
-
* - Properties starting with
|
|
21
|
+
* - Properties starting with `__scalar_` are considered internal and are hidden by default: they return undefined on access, are excluded from enumeration, and `'in'` checks return false. This can be overridden with the `showInternal` option.
|
|
22
22
|
* - Setting, deleting, and enumerating properties works as expected, including for proxied references.
|
|
23
23
|
* - Ensures referential stability by caching proxies for the same target object.
|
|
24
24
|
*
|
|
@@ -33,17 +33,17 @@ const REF_KEY = '$ref'
|
|
|
33
33
|
* foo: { bar: 123 }
|
|
34
34
|
* },
|
|
35
35
|
* refObj: { $ref: '#/definitions/foo' },
|
|
36
|
-
*
|
|
36
|
+
* __scalar_internal: 'hidden property'
|
|
37
37
|
* }
|
|
38
38
|
* const proxy = createMagicProxy(input)
|
|
39
39
|
*
|
|
40
40
|
* // Accessing proxy.refObj['$ref-value'] will resolve to { bar: 123 }
|
|
41
41
|
* console.log(proxy.refObj['$ref-value']) // { bar: 123 }
|
|
42
42
|
*
|
|
43
|
-
* // Properties starting with
|
|
44
|
-
* console.log(proxy.
|
|
45
|
-
* console.log('
|
|
46
|
-
* console.log(Object.keys(proxy)) // ['definitions', 'refObj'] (no '
|
|
43
|
+
* // Properties starting with __scalar_ are hidden
|
|
44
|
+
* console.log(proxy.__scalar_internal) // undefined
|
|
45
|
+
* console.log('__scalar_internal' in proxy) // false
|
|
46
|
+
* console.log(Object.keys(proxy)) // ['definitions', 'refObj'] (no '__scalar_internal')
|
|
47
47
|
*
|
|
48
48
|
* // Setting and deleting properties works as expected
|
|
49
49
|
* proxy.refObj.extra = 'hello'
|
|
@@ -99,7 +99,7 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
|
|
|
99
99
|
* Proxy "get" trap for magic proxy.
|
|
100
100
|
* - If accessing the special isMagicProxy symbol, return true to identify proxy.
|
|
101
101
|
* - If accessing the magicProxyTarget symbol, return the original target object.
|
|
102
|
-
* - Hide properties starting with
|
|
102
|
+
* - Hide properties starting with __scalar_ by returning undefined.
|
|
103
103
|
* - If accessing "$ref-value" and the object has a local $ref, resolve and return the referenced value as a new magic proxy.
|
|
104
104
|
* - For all other properties, recursively wrap the returned value in a magic proxy (if applicable).
|
|
105
105
|
*/
|
|
@@ -114,9 +114,9 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
|
|
|
114
114
|
return target
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// Hide properties starting with
|
|
117
|
+
// Hide properties starting with __scalar_ - these are considered internal/private properties
|
|
118
118
|
// and should not be accessible through the magic proxy interface
|
|
119
|
-
if (typeof prop === 'string' && prop.startsWith('
|
|
119
|
+
if (typeof prop === 'string' && prop.startsWith('__scalar_') && !options?.showInternal) {
|
|
120
120
|
return undefined
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -159,13 +159,13 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
|
|
|
159
159
|
* Allows setting properties on the proxied object.
|
|
160
160
|
* This will update the underlying target object.
|
|
161
161
|
*
|
|
162
|
-
* Note: it will not update if the property starts with
|
|
162
|
+
* Note: it will not update if the property starts with __scalar_
|
|
163
163
|
* Those will be considered private properties by the proxy
|
|
164
164
|
*/
|
|
165
165
|
set(target, prop, newValue, receiver) {
|
|
166
166
|
const ref = Reflect.get(target, REF_KEY, receiver)
|
|
167
167
|
|
|
168
|
-
if (typeof prop === 'string' && prop.startsWith('
|
|
168
|
+
if (typeof prop === 'string' && prop.startsWith('__scalar_') && !options?.showInternal) {
|
|
169
169
|
return true
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -215,12 +215,12 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
|
|
|
215
215
|
* - Pretend that "$ref-value" exists if "$ref" exists on the target.
|
|
216
216
|
* This allows expressions like `"$ref-value" in obj` to return true for objects with a $ref,
|
|
217
217
|
* even though "$ref-value" is a virtual property provided by the proxy.
|
|
218
|
-
* - Hide properties starting with
|
|
218
|
+
* - Hide properties starting with __scalar_ by returning false.
|
|
219
219
|
* - For all other properties, defer to the default Reflect.has behavior.
|
|
220
220
|
*/
|
|
221
221
|
has(target, prop) {
|
|
222
|
-
// Hide properties starting with
|
|
223
|
-
if (typeof prop === 'string' && prop.startsWith('
|
|
222
|
+
// Hide properties starting with __scalar_
|
|
223
|
+
if (typeof prop === 'string' && prop.startsWith('__scalar_') && !options?.showInternal) {
|
|
224
224
|
return false
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -236,14 +236,14 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
|
|
|
236
236
|
* - If the object has a "$ref" property, ensures that "$ref-value" is also included in the keys,
|
|
237
237
|
* even though "$ref-value" is a virtual property provided by the proxy.
|
|
238
238
|
* This allows Object.keys, Reflect.ownKeys, etc. to include "$ref-value" for objects with $ref.
|
|
239
|
-
* - Filters out properties starting with
|
|
239
|
+
* - Filters out properties starting with __scalar_.
|
|
240
240
|
*/
|
|
241
241
|
ownKeys(target) {
|
|
242
242
|
const keys = Reflect.ownKeys(target)
|
|
243
243
|
|
|
244
|
-
// Filter out properties starting with
|
|
244
|
+
// Filter out properties starting with __scalar_
|
|
245
245
|
const filteredKeys = keys.filter(
|
|
246
|
-
(key) => typeof key !== 'string' || !(key.startsWith('
|
|
246
|
+
(key) => typeof key !== 'string' || !(key.startsWith('__scalar_') && !options?.showInternal),
|
|
247
247
|
)
|
|
248
248
|
|
|
249
249
|
if (REF_KEY in target && !filteredKeys.includes(REF_VALUE)) {
|
|
@@ -255,13 +255,13 @@ export const createMagicProxy = <T extends Record<keyof T & symbol, unknown>, S
|
|
|
255
255
|
/**
|
|
256
256
|
* Proxy "getOwnPropertyDescriptor" trap for magic proxy.
|
|
257
257
|
* - For the virtual "$ref-value" property, returns a descriptor that makes it appear as a regular property.
|
|
258
|
-
* - Hide properties starting with
|
|
258
|
+
* - Hide properties starting with __scalar_ by returning undefined.
|
|
259
259
|
* - For all other properties, delegates to the default Reflect.getOwnPropertyDescriptor behavior.
|
|
260
260
|
* - This ensures that Object.getOwnPropertyDescriptor and similar methods work correctly with the virtual property.
|
|
261
261
|
*/
|
|
262
262
|
getOwnPropertyDescriptor(target, prop) {
|
|
263
|
-
// Hide properties starting with
|
|
264
|
-
if (typeof prop === 'string' && prop.startsWith('
|
|
263
|
+
// Hide properties starting with __scalar_
|
|
264
|
+
if (typeof prop === 'string' && prop.startsWith('__scalar_') && !options?.showInternal) {
|
|
265
265
|
return undefined
|
|
266
266
|
}
|
|
267
267
|
|