-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathread-json.ts
More file actions
177 lines (173 loc) · 5.22 KB
/
read-json.ts
File metadata and controls
177 lines (173 loc) · 5.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
* @fileoverview Read-and-parse helpers for JSON files. Wraps fs reads
* in actionable error messages keyed off `ENOENT` / `EACCES` / `EPERM`
* so callers see "JSON file not found" / "Permission denied" rather
* than the bare errno. Both variants honor `throws: false` to fall
* back to `undefined` on parse or read failure.
*/
import { jsonParse } from '../json/parse'
import { getNodeFs } from '../node/fs'
import { ErrorCtor } from '../primordials/error'
import type { PathLike } from 'node:fs'
import type { ReadJsonOptions } from './types'
/**
* Read and parse a JSON file asynchronously.
* Reads the file as UTF-8 text and parses it as JSON.
* Optionally accepts a reviver function to transform parsed values.
*
* @param filepath - Path to JSON file
* @param options - Read and parse options
* @returns Promise resolving to parsed JSON value, or undefined if throws is false and an error occurs
*
* @example
* ```ts
* // Read and parse package.json
* const pkg = await readJson('./package.json')
*
* // Read JSON with custom reviver
* const data = await readJson('./data.json', {
* reviver: (key, value) => {
* if (key === 'date') return new Date(value)
* return value
* }
* })
*
* // Don't throw on parse errors
* const config = await readJson('./config.json', { throws: false })
* if (config === undefined) {
* console.log('Failed to parse config')
* }
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export async function readJson(
filepath: PathLike,
options?: ReadJsonOptions | string | undefined,
) {
const opts = typeof options === 'string' ? { encoding: options } : options
const { reviver, throws, ...fsOptions } = {
__proto__: null,
...opts,
} as unknown as ReadJsonOptions
const shouldThrow = throws === undefined || !!throws
const fs = getNodeFs()
let content = ''
try {
content = await fs.promises.readFile(filepath, {
__proto__: null,
...fsOptions,
encoding: 'utf8',
} as unknown as Parameters<typeof fs.promises.readFile>[1] & {
encoding: string
})
} catch (e) {
if (shouldThrow) {
const code = (e as NodeJS.ErrnoException).code
if (code === 'ENOENT') {
throw new ErrorCtor(
`JSON file not found: ${filepath}\n` +
'Ensure the file exists or create it with the expected structure.',
{ cause: e },
)
}
// EPERM operand fires on Windows; the if-truthy + EACCES-vs-
// EPERM operand sub-arms vary per platform.
/* c8 ignore start */
if (code === 'EACCES' || code === 'EPERM') {
throw new ErrorCtor(
`Permission denied reading JSON file: ${filepath}\n` +
'Check file permissions or run with appropriate access.',
{ cause: e },
)
}
/* c8 ignore stop */
throw e
}
return undefined
}
return jsonParse(content, {
filepath: String(filepath),
reviver,
throws: shouldThrow,
})
}
/**
* Read and parse a JSON file synchronously.
* Reads the file as UTF-8 text and parses it as JSON.
* Optionally accepts a reviver function to transform parsed values.
*
* @param filepath - Path to JSON file
* @param options - Read and parse options
* @returns Parsed JSON value, or undefined if throws is false and an error occurs
*
* @example
* ```ts
* // Read and parse tsconfig.json
* const tsconfig = readJsonSync('./tsconfig.json')
*
* // Read JSON with custom reviver
* const data = readJsonSync('./data.json', {
* reviver: (key, value) => {
* if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) {
* return new Date(value)
* }
* return value
* }
* })
*
* // Don't throw on parse errors
* const config = readJsonSync('./config.json', { throws: false })
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function readJsonSync(
filepath: PathLike,
options?: ReadJsonOptions | string | undefined,
) {
const opts = typeof options === 'string' ? { encoding: options } : options
const { reviver, throws, ...fsOptions } = {
__proto__: null,
...opts,
} as unknown as ReadJsonOptions
const shouldThrow = throws === undefined || !!throws
const fs = getNodeFs()
let content = ''
try {
content = fs.readFileSync(filepath, {
__proto__: null,
...fsOptions,
encoding: 'utf8',
} as unknown as Parameters<typeof fs.readFileSync>[1] & {
encoding: string
})
} catch (e) {
if (shouldThrow) {
const code = (e as NodeJS.ErrnoException).code
if (code === 'ENOENT') {
throw new ErrorCtor(
`JSON file not found: ${filepath}\n` +
'Ensure the file exists or create it with the expected structure.',
{ cause: e },
)
}
// EPERM operand fires on Windows; the if-truthy + EACCES-vs-
// EPERM operand sub-arms vary per platform.
/* c8 ignore start */
if (code === 'EACCES' || code === 'EPERM') {
throw new ErrorCtor(
`Permission denied reading JSON file: ${filepath}\n` +
'Check file permissions or run with appropriate access.',
{ cause: e },
)
}
/* c8 ignore stop */
throw e
}
return undefined
}
return jsonParse(content, {
filepath: String(filepath),
reviver,
throws: shouldThrow,
})
}