@scalar/json-magic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/LICENSE +21 -0
  4. package/README.md +356 -0
  5. package/dist/bundle/bundle.d.ts +292 -0
  6. package/dist/bundle/bundle.d.ts.map +1 -0
  7. package/dist/bundle/bundle.js +259 -0
  8. package/dist/bundle/bundle.js.map +7 -0
  9. package/dist/bundle/create-limiter.d.ts +21 -0
  10. package/dist/bundle/create-limiter.d.ts.map +1 -0
  11. package/dist/bundle/create-limiter.js +31 -0
  12. package/dist/bundle/create-limiter.js.map +7 -0
  13. package/dist/bundle/index.d.ts +2 -0
  14. package/dist/bundle/index.d.ts.map +1 -0
  15. package/dist/bundle/index.js +5 -0
  16. package/dist/bundle/index.js.map +7 -0
  17. package/dist/bundle/plugins/browser.d.ts +4 -0
  18. package/dist/bundle/plugins/browser.d.ts.map +1 -0
  19. package/dist/bundle/plugins/browser.js +9 -0
  20. package/dist/bundle/plugins/browser.js.map +7 -0
  21. package/dist/bundle/plugins/fetch-urls/index.d.ts +39 -0
  22. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -0
  23. package/dist/bundle/plugins/fetch-urls/index.js +48 -0
  24. package/dist/bundle/plugins/fetch-urls/index.js.map +7 -0
  25. package/dist/bundle/plugins/node.d.ts +5 -0
  26. package/dist/bundle/plugins/node.d.ts.map +1 -0
  27. package/dist/bundle/plugins/node.js +11 -0
  28. package/dist/bundle/plugins/node.js.map +7 -0
  29. package/dist/bundle/plugins/parse-json/index.d.ts +13 -0
  30. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -0
  31. package/dist/bundle/plugins/parse-json/index.js +22 -0
  32. package/dist/bundle/plugins/parse-json/index.js.map +7 -0
  33. package/dist/bundle/plugins/parse-yaml/index.d.ts +13 -0
  34. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -0
  35. package/dist/bundle/plugins/parse-yaml/index.js +23 -0
  36. package/dist/bundle/plugins/parse-yaml/index.js.map +7 -0
  37. package/dist/bundle/plugins/read-files/index.d.ts +29 -0
  38. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -0
  39. package/dist/bundle/plugins/read-files/index.js +30 -0
  40. package/dist/bundle/plugins/read-files/index.js.map +7 -0
  41. package/dist/bundle/value-generator.d.ts +79 -0
  42. package/dist/bundle/value-generator.d.ts.map +1 -0
  43. package/dist/bundle/value-generator.js +55 -0
  44. package/dist/bundle/value-generator.js.map +7 -0
  45. package/dist/dereference/dereference.d.ts +45 -0
  46. package/dist/dereference/dereference.d.ts.map +1 -0
  47. package/dist/dereference/dereference.js +37 -0
  48. package/dist/dereference/dereference.js.map +7 -0
  49. package/dist/dereference/index.d.ts +2 -0
  50. package/dist/dereference/index.d.ts.map +1 -0
  51. package/dist/dereference/index.js +5 -0
  52. package/dist/dereference/index.js.map +7 -0
  53. package/dist/diff/apply.d.ts +35 -0
  54. package/dist/diff/apply.d.ts.map +1 -0
  55. package/dist/diff/apply.js +40 -0
  56. package/dist/diff/apply.js.map +7 -0
  57. package/dist/diff/diff.d.ts +56 -0
  58. package/dist/diff/diff.d.ts.map +1 -0
  59. package/dist/diff/diff.js +33 -0
  60. package/dist/diff/diff.js.map +7 -0
  61. package/dist/diff/index.d.ts +5 -0
  62. package/dist/diff/index.d.ts.map +1 -0
  63. package/dist/diff/index.js +9 -0
  64. package/dist/diff/index.js.map +7 -0
  65. package/dist/diff/merge.d.ts +43 -0
  66. package/dist/diff/merge.d.ts.map +1 -0
  67. package/dist/diff/merge.js +61 -0
  68. package/dist/diff/merge.js.map +7 -0
  69. package/dist/diff/trie.d.ts +64 -0
  70. package/dist/diff/trie.d.ts.map +1 -0
  71. package/dist/diff/trie.js +82 -0
  72. package/dist/diff/trie.js.map +7 -0
  73. package/dist/diff/utils.d.ts +63 -0
  74. package/dist/diff/utils.d.ts.map +1 -0
  75. package/dist/diff/utils.js +48 -0
  76. package/dist/diff/utils.js.map +7 -0
  77. package/dist/magic-proxy/index.d.ts +2 -0
  78. package/dist/magic-proxy/index.d.ts.map +1 -0
  79. package/dist/magic-proxy/index.js +6 -0
  80. package/dist/magic-proxy/index.js.map +7 -0
  81. package/dist/magic-proxy/proxy.d.ts +63 -0
  82. package/dist/magic-proxy/proxy.d.ts.map +1 -0
  83. package/dist/magic-proxy/proxy.js +108 -0
  84. package/dist/magic-proxy/proxy.js.map +7 -0
  85. package/dist/polyfills/index.d.ts +2 -0
  86. package/dist/polyfills/index.d.ts.map +1 -0
  87. package/dist/polyfills/index.js +25 -0
  88. package/dist/polyfills/index.js.map +7 -0
  89. package/dist/polyfills/path.d.ts +24 -0
  90. package/dist/polyfills/path.d.ts.map +1 -0
  91. package/dist/polyfills/path.js +174 -0
  92. package/dist/polyfills/path.js.map +7 -0
  93. package/dist/types.d.ts +2 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +1 -0
  96. package/dist/types.js.map +7 -0
  97. package/dist/utils/escape-json-pointer.d.ts +7 -0
  98. package/dist/utils/escape-json-pointer.d.ts.map +1 -0
  99. package/dist/utils/escape-json-pointer.js +7 -0
  100. package/dist/utils/escape-json-pointer.js.map +7 -0
  101. package/dist/utils/get-segments-from-path.d.ts +5 -0
  102. package/dist/utils/get-segments-from-path.d.ts.map +1 -0
  103. package/dist/utils/get-segments-from-path.js +11 -0
  104. package/dist/utils/get-segments-from-path.js.map +7 -0
  105. package/dist/utils/is-json-object.d.ts +18 -0
  106. package/dist/utils/is-json-object.d.ts.map +1 -0
  107. package/dist/utils/is-json-object.js +16 -0
  108. package/dist/utils/is-json-object.js.map +7 -0
  109. package/dist/utils/is-object.d.ts +5 -0
  110. package/dist/utils/is-object.d.ts.map +1 -0
  111. package/dist/utils/is-object.js +5 -0
  112. package/dist/utils/is-object.js.map +7 -0
  113. package/dist/utils/is-yaml.d.ts +17 -0
  114. package/dist/utils/is-yaml.d.ts.map +1 -0
  115. package/dist/utils/is-yaml.js +7 -0
  116. package/dist/utils/is-yaml.js.map +7 -0
  117. package/dist/utils/json-path-utils.d.ts +23 -0
  118. package/dist/utils/json-path-utils.d.ts.map +1 -0
  119. package/dist/utils/json-path-utils.js +16 -0
  120. package/dist/utils/json-path-utils.js.map +7 -0
  121. package/dist/utils/normalize.d.ts +5 -0
  122. package/dist/utils/normalize.d.ts.map +1 -0
  123. package/dist/utils/normalize.js +28 -0
  124. package/dist/utils/normalize.js.map +7 -0
  125. package/dist/utils/unescape-json-pointer.d.ts +8 -0
  126. package/dist/utils/unescape-json-pointer.d.ts.map +1 -0
  127. package/dist/utils/unescape-json-pointer.js +7 -0
  128. package/dist/utils/unescape-json-pointer.js.map +7 -0
  129. package/esbuild.ts +13 -0
  130. package/package.json +65 -0
  131. package/src/bundle/bundle.test.ts +1843 -0
  132. package/src/bundle/bundle.ts +758 -0
  133. package/src/bundle/create-limiter.test.ts +28 -0
  134. package/src/bundle/create-limiter.ts +52 -0
  135. package/src/bundle/index.ts +2 -0
  136. package/src/bundle/plugins/browser.ts +4 -0
  137. package/src/bundle/plugins/fetch-urls/index.test.ts +147 -0
  138. package/src/bundle/plugins/fetch-urls/index.ts +94 -0
  139. package/src/bundle/plugins/node.ts +5 -0
  140. package/src/bundle/plugins/parse-json/index.test.ts +22 -0
  141. package/src/bundle/plugins/parse-json/index.ts +30 -0
  142. package/src/bundle/plugins/parse-yaml/index.test.ts +24 -0
  143. package/src/bundle/plugins/parse-yaml/index.ts +31 -0
  144. package/src/bundle/plugins/read-files/index.test.ts +35 -0
  145. package/src/bundle/plugins/read-files/index.ts +55 -0
  146. package/src/bundle/value-generator.test.ts +166 -0
  147. package/src/bundle/value-generator.ts +147 -0
  148. package/src/dereference/dereference.test.ts +137 -0
  149. package/src/dereference/dereference.ts +84 -0
  150. package/src/dereference/index.ts +2 -0
  151. package/src/diff/apply.test.ts +262 -0
  152. package/src/diff/apply.ts +78 -0
  153. package/src/diff/diff.test.ts +328 -0
  154. package/src/diff/diff.ts +94 -0
  155. package/src/diff/index.test.ts +150 -0
  156. package/src/diff/index.ts +5 -0
  157. package/src/diff/merge.test.ts +1109 -0
  158. package/src/diff/merge.ts +136 -0
  159. package/src/diff/trie.test.ts +30 -0
  160. package/src/diff/trie.ts +113 -0
  161. package/src/diff/utils.test.ts +169 -0
  162. package/src/diff/utils.ts +113 -0
  163. package/src/magic-proxy/index.ts +2 -0
  164. package/src/magic-proxy/proxy.test.ts +145 -0
  165. package/src/magic-proxy/proxy.ts +225 -0
  166. package/src/polyfills/index.ts +12 -0
  167. package/src/polyfills/path.ts +248 -0
  168. package/src/types.ts +1 -0
  169. package/src/utils/escape-json-pointer.test.ts +13 -0
  170. package/src/utils/escape-json-pointer.ts +8 -0
  171. package/src/utils/get-segments-from-path.test.ts +17 -0
  172. package/src/utils/get-segments-from-path.ts +17 -0
  173. package/src/utils/is-json-object.ts +31 -0
  174. package/src/utils/is-object.test.ts +27 -0
  175. package/src/utils/is-object.ts +4 -0
  176. package/src/utils/is-yaml.ts +18 -0
  177. package/src/utils/json-path-utils.test.ts +13 -0
  178. package/src/utils/json-path-utils.ts +38 -0
  179. package/src/utils/normalize.test.ts +91 -0
  180. package/src/utils/normalize.ts +34 -0
  181. package/src/utils/unescape-json-pointer.test.ts +23 -0
  182. package/src/utils/unescape-json-pointer.ts +9 -0
  183. package/tsconfig.build.json +12 -0
  184. package/tsconfig.json +16 -0
  185. package/vite.config.ts +8 -0
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createLimiter } from './create-limiter'
3
+
4
+ describe('createLimiter', { timeout: 10000 }, () => {
5
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
6
+
7
+ it('run in order and no more than the specified number of concurrent requests', async () => {
8
+ const limiter = createLimiter(2)
9
+
10
+ let active = 0
11
+ const maxObserved: number[] = []
12
+
13
+ const makeTask = (id: number) =>
14
+ limiter(async () => {
15
+ active++
16
+ maxObserved.push(active)
17
+ await sleep(100)
18
+ active--
19
+ return id
20
+ })
21
+
22
+ const tasks = [1, 2, 3, 4, 5].map(makeTask)
23
+ const results = await Promise.all(tasks)
24
+
25
+ expect(results).toEqual([1, 2, 3, 4, 5])
26
+ expect(Math.max(...maxObserved)).toBeLessThanOrEqual(2)
27
+ })
28
+ })
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Creates a function that limits the number of concurrent executions of async functions.
3
+ *
4
+ * @param maxConcurrent - Maximum number of concurrent executions allowed
5
+ * @returns A function that wraps async functions to limit their concurrent execution
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const limiter = createLimiter(2) // Allow max 2 concurrent executions
10
+ *
11
+ * // These will run with max 2 at a time
12
+ * const results = await Promise.all([
13
+ * limiter(() => fetch('/api/1')),
14
+ * limiter(() => fetch('/api/2')),
15
+ * limiter(() => fetch('/api/3')),
16
+ * limiter(() => fetch('/api/4'))
17
+ * ])
18
+ * ```
19
+ */
20
+ export function createLimiter(maxConcurrent: number) {
21
+ let activeCount = 0
22
+ const queue: (() => void)[] = []
23
+
24
+ const next = () => {
25
+ if (queue.length === 0 || activeCount >= maxConcurrent) {
26
+ return
27
+ }
28
+
29
+ const resolve = queue.shift()
30
+
31
+ if (resolve) {
32
+ resolve()
33
+ }
34
+ }
35
+
36
+ const run = async <T>(fn: () => Promise<T>): Promise<T> => {
37
+ if (activeCount >= maxConcurrent) {
38
+ await new Promise<void>((resolve) => queue.push(resolve))
39
+ }
40
+
41
+ activeCount++
42
+ try {
43
+ const result = await fn()
44
+ return result
45
+ } finally {
46
+ activeCount--
47
+ next()
48
+ }
49
+ }
50
+
51
+ return run
52
+ }
@@ -0,0 +1,2 @@
1
+ // biome-ignore lint/performance/noBarrelFile: <explanation>
2
+ export { bundle, type Plugin, type ResolveResult } from './bundle'
@@ -0,0 +1,4 @@
1
+ // biome-ignore lint/performance/noBarrelFile: <explanation>
2
+ export { fetchUrls } from './fetch-urls'
3
+ export { parseJson } from './parse-json'
4
+ export { parseYaml } from './parse-yaml'
@@ -0,0 +1,147 @@
1
+ import { fastify, type FastifyInstance } from 'fastify'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { fetchUrl } from '.'
4
+ import assert from 'node:assert'
5
+ import { setTimeout } from 'node:timers/promises'
6
+
7
+ describe('fetchUrl', () => {
8
+ const noLimit = <T>(fn: () => Promise<T>) => fn()
9
+
10
+ let server: FastifyInstance
11
+ const PORT = 7291
12
+
13
+ beforeEach(() => {
14
+ server = fastify({ logger: false })
15
+ })
16
+
17
+ afterEach(async () => {
18
+ await server.close()
19
+ await setTimeout(100)
20
+ })
21
+
22
+ it('reads json response', async () => {
23
+ const url = `http://localhost:${PORT}`
24
+
25
+ const response = {
26
+ message: '200OK',
27
+ }
28
+
29
+ server.get('/', (_, reply) => {
30
+ reply.send(response)
31
+ })
32
+
33
+ await server.listen({ port: PORT })
34
+
35
+ const result = await fetchUrl(url, noLimit)
36
+
37
+ expect(result.ok).toBe(true)
38
+ assert(result.ok === true)
39
+ expect(result.data).toEqual(response)
40
+ })
41
+
42
+ it('reads yaml response', async () => {
43
+ const url = `http://localhost:${PORT}`
44
+
45
+ server.get('/', (_, reply) => {
46
+ reply.header('content-type', 'application/yml').send('a: a')
47
+ })
48
+
49
+ await server.listen({ port: PORT })
50
+
51
+ const result = await fetchUrl(url, noLimit)
52
+
53
+ expect(result.ok).toBe(true)
54
+ assert(result.ok === true)
55
+ expect(result.data).toEqual({ a: 'a' })
56
+ })
57
+
58
+ it('returns error on non-200 response', async () => {
59
+ const url = `http://localhost:${PORT}`
60
+
61
+ server.get('/', (_, reply) => {
62
+ reply.status(404).send()
63
+ })
64
+
65
+ await server.listen({ port: PORT })
66
+
67
+ const result = await fetchUrl(url, noLimit)
68
+
69
+ expect(result.ok).toBe(false)
70
+ })
71
+
72
+ it('send headers to the specified domain', async () => {
73
+ const url = `http://localhost:${PORT}`
74
+ const fn = vi.fn()
75
+
76
+ const response = {
77
+ message: '200OK',
78
+ }
79
+
80
+ server.get('/', (request, reply) => {
81
+ fn(request.headers)
82
+ reply.send(response)
83
+ })
84
+
85
+ await server.listen({ port: PORT })
86
+ await fetchUrl(url, noLimit, {
87
+ headers: [{ headers: { 'Authorization': 'Bearer <TOKEN>' }, domains: [`localhost:${PORT}`] }],
88
+ })
89
+
90
+ expect(fn).toHaveBeenCalled()
91
+ expect(fn.mock.calls[0][0]).toEqual({
92
+ 'accept': '*/*',
93
+ 'accept-encoding': 'gzip, deflate',
94
+ 'accept-language': '*',
95
+ 'authorization': 'Bearer <TOKEN>',
96
+ 'connection': 'keep-alive',
97
+ 'host': `localhost:${PORT}`,
98
+ 'sec-fetch-mode': 'cors',
99
+ 'user-agent': 'node',
100
+ })
101
+ })
102
+
103
+ it('does not send headers to other domains', async () => {
104
+ const url = `http://localhost:${PORT}`
105
+ const fn = vi.fn()
106
+
107
+ const response = {
108
+ message: '200OK',
109
+ }
110
+
111
+ server.get('/', (request, reply) => {
112
+ fn(request.headers)
113
+ reply.send(response)
114
+ })
115
+
116
+ await server.listen({ port: PORT })
117
+ await fetchUrl(url, noLimit, {
118
+ headers: [{ headers: { 'Authorization': 'Bearer <TOKEN>' }, domains: ['localhost:9932', 'localhost'] }],
119
+ })
120
+
121
+ expect(fn).toHaveBeenCalled()
122
+ expect(fn.mock.calls[0][0]).toEqual({
123
+ 'accept': '*/*',
124
+ 'accept-encoding': 'gzip, deflate',
125
+ 'accept-language': '*',
126
+ 'connection': 'keep-alive',
127
+ 'host': `localhost:${PORT}`,
128
+ 'sec-fetch-mode': 'cors',
129
+ 'user-agent': 'node',
130
+ })
131
+ })
132
+
133
+ it('runs custom fetcher', async () => {
134
+ const fn = vi.fn()
135
+
136
+ await fetchUrl('https://example.com', (fn) => fn(), {
137
+ fetch: async (input, init) => {
138
+ fn(input, init)
139
+
140
+ return new Response('{}', { status: 200 })
141
+ },
142
+ })
143
+
144
+ expect(fn).toHaveBeenCalled()
145
+ expect(fn).toHaveBeenCalledWith('https://example.com', { headers: undefined })
146
+ })
147
+ })
@@ -0,0 +1,94 @@
1
+ import { normalize } from '@/utils/normalize'
2
+ import { createLimiter } from '@/bundle/create-limiter'
3
+ import type { Plugin, ResolveResult } from '@/bundle'
4
+ import { isRemoteUrl } from '@/bundle/bundle'
5
+
6
+ type FetchConfig = Partial<{
7
+ headers: { headers: HeadersInit; domains: string[] }[]
8
+ fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>
9
+ }>
10
+
11
+ /**
12
+ * Safely checks for host from a URL
13
+ * Needed because we cannot create a URL from a relative remote URL ex: examples/openapi.json
14
+ */
15
+ const getHost = (url: string): string | null => {
16
+ try {
17
+ return new URL(url).host
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Fetches and normalizes data from a remote URL
25
+ * @param url - The URL to fetch data from
26
+ * @returns A promise that resolves to either the normalized data or an error result
27
+ * @example
28
+ * ```ts
29
+ * const result = await fetchUrl('https://api.example.com/data.json')
30
+ * if (result.ok) {
31
+ * console.log(result.data) // The normalized data
32
+ * } else {
33
+ * console.log('Failed to fetch data')
34
+ * }
35
+ * ```
36
+ */
37
+ export async function fetchUrl(
38
+ url: string,
39
+ limiter: <T>(fn: () => Promise<T>) => Promise<T>,
40
+ config?: FetchConfig,
41
+ ): Promise<ResolveResult> {
42
+ try {
43
+ const host = getHost(url)
44
+
45
+ // Get the headers that match the domain
46
+ const headers = config?.headers?.find((a) => a.domains.find((d) => d === host) !== undefined)?.headers
47
+
48
+ const exec = config?.fetch ?? fetch
49
+
50
+ const result = await limiter(() =>
51
+ exec(url, {
52
+ headers,
53
+ }),
54
+ )
55
+
56
+ if (result.ok) {
57
+ const body = await result.text()
58
+
59
+ return {
60
+ ok: true,
61
+ data: normalize(body),
62
+ }
63
+ }
64
+
65
+ return {
66
+ ok: false,
67
+ }
68
+ } catch {
69
+ return {
70
+ ok: false,
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Creates a plugin for handling remote URL references.
77
+ * This plugin validates and fetches data from HTTP/HTTPS URLs.
78
+ *
79
+ * @returns A plugin object with validate and exec functions
80
+ * @example
81
+ * const urlPlugin = fetchUrls()
82
+ * if (urlPlugin.validate('https://example.com/schema.json')) {
83
+ * const result = await urlPlugin.exec('https://example.com/schema.json')
84
+ * }
85
+ */
86
+ export function fetchUrls(config?: FetchConfig & Partial<{ limit: number | null }>): Plugin {
87
+ // If there is a limit specified we limit the number of concurrent calls
88
+ const limiter = config?.limit ? createLimiter(config.limit) : <T>(fn: () => Promise<T>) => fn()
89
+
90
+ return {
91
+ validate: isRemoteUrl,
92
+ exec: (value) => fetchUrl(value, limiter, config),
93
+ }
94
+ }
@@ -0,0 +1,5 @@
1
+ // biome-ignore lint/performance/noBarrelFile: <explanation>
2
+ export { fetchUrls } from './fetch-urls'
3
+ export { readFiles } from './read-files'
4
+ export { parseJson } from './parse-json'
5
+ export { parseYaml } from './parse-yaml'
@@ -0,0 +1,22 @@
1
+ import { parseJson } from '@/bundle/plugins/parse-json'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ describe('parse-json', () => {
5
+ it.each([
6
+ ['{}', true],
7
+ ['{ "a": "b" }', true],
8
+ ['{ "a": 2 }', true],
9
+ ["{ 'a': 2 }", false],
10
+ ['{ some string', false],
11
+ ['{ ', false],
12
+ ])('should validate if strings are json valid format', (a, b) => {
13
+ expect(parseJson().validate(a)).toBe(b)
14
+ })
15
+
16
+ it('should parse json string', async () => {
17
+ expect(await parseJson().exec('{ "message": "Hello World" }')).toEqual({
18
+ ok: true,
19
+ data: { message: 'Hello World' },
20
+ })
21
+ })
22
+ })
@@ -0,0 +1,30 @@
1
+ import { isJsonObject } from '@/utils/is-json-object'
2
+ import type { Plugin, ResolveResult } from '@/bundle'
3
+
4
+ /**
5
+ * Creates a plugin that parses JSON strings into JavaScript objects.
6
+ * @returns A plugin object with validate and exec functions
7
+ * @example
8
+ * ```ts
9
+ * const jsonPlugin = parseJson()
10
+ * const result = jsonPlugin.exec('{"name": "John", "age": 30}')
11
+ * // result = { name: 'John', age: 30 }
12
+ * ```
13
+ */
14
+ export function parseJson(): Plugin {
15
+ return {
16
+ validate: isJsonObject,
17
+ exec: async (value): Promise<ResolveResult> => {
18
+ try {
19
+ return {
20
+ ok: true,
21
+ data: JSON.parse(value),
22
+ }
23
+ } catch {
24
+ return {
25
+ ok: false,
26
+ }
27
+ }
28
+ },
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ import { parseYaml } from '@/bundle/plugins/parse-yaml'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ describe('parse-yaml', () => {
5
+ it.each([
6
+ ['hi: hello\n', true],
7
+ ['- some: 1\n', true],
8
+ ['valid: value\nhey: hi\n', true],
9
+ ["{ 'a': 2 }", false],
10
+ ['{ some string', false],
11
+ ['{ ', false],
12
+ ['{}', false],
13
+ ['{ "json": "" }', false],
14
+ ])('should validate if strings are yaml valid format', (a, b) => {
15
+ expect(parseYaml().validate(a)).toBe(b)
16
+ })
17
+
18
+ it('should parse yaml string', async () => {
19
+ expect(await parseYaml().exec('{ "message": "Hello World" }')).toEqual({
20
+ ok: true,
21
+ data: { message: 'Hello World' },
22
+ })
23
+ })
24
+ })
@@ -0,0 +1,31 @@
1
+ import type { Plugin, ResolveResult } from '@/bundle/bundle'
2
+ import { isYaml } from '@/utils/is-yaml'
3
+ import YAML from 'yaml'
4
+
5
+ /**
6
+ * Creates a plugin that parses YAML strings into JavaScript objects.
7
+ * @returns A plugin object with validate and exec functions
8
+ * @example
9
+ * ```ts
10
+ * const yamlPlugin = parseYaml()
11
+ * const result = yamlPlugin.exec('name: John\nage: 30')
12
+ * // result = { name: 'John', age: 30 }
13
+ * ```
14
+ */
15
+ export function parseYaml(): Plugin {
16
+ return {
17
+ validate: isYaml,
18
+ exec: async (value): Promise<ResolveResult> => {
19
+ try {
20
+ return {
21
+ ok: true,
22
+ data: YAML.parse(value),
23
+ }
24
+ } catch {
25
+ return {
26
+ ok: false,
27
+ }
28
+ }
29
+ },
30
+ }
31
+ }
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs/promises'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { readFile } from '.'
5
+ import assert from 'node:assert'
6
+
7
+ describe('readFile', () => {
8
+ it('reads json contents of a file', async () => {
9
+ const contents = { message: 'ok' }
10
+ const path = randomUUID()
11
+ await fs.writeFile(path, JSON.stringify(contents))
12
+
13
+ const result = await readFile(path)
14
+ await fs.rm(path)
15
+
16
+ expect(result.ok).toBe(true)
17
+ assert(result.ok === true)
18
+
19
+ expect(result.data).toEqual(contents)
20
+ })
21
+
22
+ it('reads yml contents of a file', async () => {
23
+ const contents = 'a: a'
24
+ const path = randomUUID()
25
+ await fs.writeFile(path, contents)
26
+
27
+ const result = await readFile(path)
28
+ await fs.rm(path)
29
+
30
+ expect(result.ok).toBe(true)
31
+ assert(result.ok === true)
32
+
33
+ expect(result.data).toEqual({ a: 'a' })
34
+ })
35
+ })
@@ -0,0 +1,55 @@
1
+ import { normalize } from '@/utils/normalize'
2
+ import { isFilePath, type Plugin, type ResolveResult } from '@/bundle/bundle'
3
+
4
+ /**
5
+ * Reads and normalizes data from a local file
6
+ * @param path - The file path to read from
7
+ * @returns A promise that resolves to either the normalized data or an error result
8
+ * @example
9
+ * ```ts
10
+ * const result = await readFile('./schemas/user.json')
11
+ * if (result.ok) {
12
+ * console.log(result.data) // The normalized data
13
+ * } else {
14
+ * console.log('Failed to read file')
15
+ * }
16
+ * ```
17
+ */
18
+ export async function readFile(path: string): Promise<ResolveResult> {
19
+ const fs = typeof window === 'undefined' ? await import('node:fs/promises') : undefined
20
+
21
+ if (fs === undefined) {
22
+ throw 'Can not use readFiles plugin outside of a node environment'
23
+ }
24
+
25
+ try {
26
+ const fileContents = await fs.readFile(path, { encoding: 'utf-8' })
27
+
28
+ return {
29
+ ok: true,
30
+ data: normalize(fileContents),
31
+ }
32
+ } catch {
33
+ return {
34
+ ok: false,
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Creates a plugin for handling local file references.
41
+ * This plugin validates and reads data from local filesystem paths.
42
+ *
43
+ * @returns A plugin object with validate and exec functions
44
+ * @example
45
+ * const filePlugin = readFiles()
46
+ * if (filePlugin.validate('./local-schema.json')) {
47
+ * const result = await filePlugin.exec('./local-schema.json')
48
+ * }
49
+ */
50
+ export function readFiles(): Plugin {
51
+ return {
52
+ validate: isFilePath,
53
+ exec: readFile,
54
+ }
55
+ }