@zjy4fun/json-open 0.1.3 → 0.1.9
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/.github/workflows/publish.yml +3 -2
- package/README.md +67 -64
- package/bin/json.js +154 -7
- package/package.json +6 -2
|
@@ -18,9 +18,8 @@ jobs:
|
|
|
18
18
|
|
|
19
19
|
- uses: actions/setup-node@v4
|
|
20
20
|
with:
|
|
21
|
-
node-version:
|
|
21
|
+
node-version: 22
|
|
22
22
|
registry-url: 'https://registry.npmjs.org'
|
|
23
|
-
cache: 'npm'
|
|
24
23
|
|
|
25
24
|
- run: npm ci
|
|
26
25
|
- run: npm test --if-present
|
|
@@ -33,3 +32,5 @@ jobs:
|
|
|
33
32
|
[ "$TAG_VERSION" = "$PKG_VERSION" ] || (echo "Tag $TAG_VERSION != package.json $PKG_VERSION" && exit 1)
|
|
34
33
|
|
|
35
34
|
- run: npm publish --access public --provenance
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
CHANGED
|
@@ -1,56 +1,59 @@
|
|
|
1
1
|
# json-open
|
|
2
2
|
|
|
3
|
-
Open JSON in your browser
|
|
3
|
+
Open JSON in your browser as a collapsible tree view.
|
|
4
4
|
|
|
5
|
-
> A tiny CLI for
|
|
6
|
-
|
|
7
|
-
## Demo
|
|
5
|
+
> A tiny CLI for quickly inspecting JSON from APIs, logs, or inline text.
|
|
8
6
|
|
|
9
7
|

|
|
10
8
|
|
|
11
9
|
---
|
|
12
10
|
|
|
13
|
-
## Why
|
|
11
|
+
## Why json-open?
|
|
14
12
|
|
|
15
|
-
Reading JSON in
|
|
13
|
+
Reading JSON in terminal is often painful:
|
|
16
14
|
|
|
17
15
|
- Long output is hard to scan
|
|
18
16
|
- Deep nesting is hard to understand quickly
|
|
19
|
-
-
|
|
17
|
+
- Full-featured tools can feel heavy for quick checks
|
|
20
18
|
|
|
21
|
-
`json-open` keeps
|
|
19
|
+
`json-open` keeps it simple: **pipe JSON in, inspect it visually in seconds**.
|
|
22
20
|
|
|
23
21
|
---
|
|
24
22
|
|
|
25
23
|
## Features
|
|
26
24
|
|
|
27
|
-
- ✅
|
|
28
|
-
- ✅
|
|
29
|
-
- ✅
|
|
30
|
-
- ✅
|
|
31
|
-
- ✅
|
|
32
|
-
- ✅
|
|
25
|
+
- ✅ Read from stdin (pipe)
|
|
26
|
+
- ✅ Read inline JSON text
|
|
27
|
+
- ✅ Open browser automatically (macOS / Linux / Windows)
|
|
28
|
+
- ✅ Collapsible tree view
|
|
29
|
+
- ✅ Expand all / Collapse all
|
|
30
|
+
- ✅ Local temp file rendering (no remote upload)
|
|
31
|
+
- ✅ **Auto-parse serialized JSON strings** (escaped/double-encoded)
|
|
32
|
+
- ✅ **Deep nested JSON string expansion** (auto-unwrap JSON strings inside objects)
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
36
|
## Installation
|
|
37
37
|
|
|
38
|
-
###
|
|
38
|
+
### npm (recommended)
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
```bash
|
|
41
|
+
npm i -g @zjy4fun/json-open
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After install, use the global command:
|
|
41
45
|
|
|
42
46
|
```bash
|
|
43
|
-
|
|
44
|
-
echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN" >> ~/.npmrc
|
|
47
|
+
json --version
|
|
45
48
|
```
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
### Run once with npx (no global install)
|
|
48
51
|
|
|
49
52
|
```bash
|
|
50
|
-
|
|
53
|
+
npx @zjy4fun/json-open '{"hello":"world"}'
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
###
|
|
56
|
+
### Local development
|
|
54
57
|
|
|
55
58
|
```bash
|
|
56
59
|
git clone https://github.com/zjy4fun/json-open.git
|
|
@@ -59,11 +62,9 @@ npm install
|
|
|
59
62
|
npm link
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
After that, the `json` command is available globally.
|
|
63
|
-
|
|
64
65
|
---
|
|
65
66
|
|
|
66
|
-
## Quick
|
|
67
|
+
## Quick Start
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
# 1) API response
|
|
@@ -76,41 +77,25 @@ json '{"hello":"world","list":[1,2,3]}'
|
|
|
76
77
|
cat response.json | json
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
The command opens your browser and shows a JSON tree
|
|
80
|
+
The command opens your default browser and shows a structured JSON tree.
|
|
80
81
|
|
|
81
82
|
---
|
|
82
83
|
|
|
83
|
-
##
|
|
84
|
-
|
|
85
|
-
1. **API debugging**
|
|
86
|
-
Inspect response shape quickly, especially nested data.
|
|
87
|
-
|
|
88
|
-
2. **Backend/frontend integration checks**
|
|
89
|
-
Verify missing fields or type mismatches after API changes.
|
|
90
|
-
|
|
91
|
-
3. **Ad-hoc JSON inspection**
|
|
92
|
-
Visualize JSON copied from logs, queues, or snapshots.
|
|
93
|
-
|
|
94
|
-
4. **Team discussion/demo**
|
|
95
|
-
Share a clearer structure view when discussing payloads.
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Command behavior
|
|
84
|
+
## CLI Usage
|
|
100
85
|
|
|
101
86
|
```bash
|
|
102
|
-
json
|
|
87
|
+
json [inline-json]
|
|
103
88
|
```
|
|
104
89
|
|
|
105
|
-
Input
|
|
90
|
+
Input sources:
|
|
106
91
|
|
|
107
|
-
- stdin (pipe)
|
|
108
|
-
-
|
|
92
|
+
- `stdin` (pipe)
|
|
93
|
+
- Inline JSON argument
|
|
109
94
|
|
|
110
95
|
Options:
|
|
111
96
|
|
|
112
|
-
- `-h, --help`
|
|
113
|
-
- `-v, --version`
|
|
97
|
+
- `-h, --help` Show help
|
|
98
|
+
- `-v, --version` Show version
|
|
114
99
|
|
|
115
100
|
Examples:
|
|
116
101
|
|
|
@@ -120,19 +105,43 @@ json --version
|
|
|
120
105
|
json '{"ok":true}'
|
|
121
106
|
```
|
|
122
107
|
|
|
123
|
-
If no input is provided,
|
|
108
|
+
If no input is provided, usage help is printed.
|
|
124
109
|
|
|
125
110
|
---
|
|
126
111
|
|
|
127
|
-
##
|
|
112
|
+
## Serialized JSON String Support
|
|
128
113
|
|
|
129
|
-
|
|
114
|
+
`json-open` now automatically handles serialized/escaped JSON strings — a common pain point when working with logs, databases, and APIs.
|
|
130
115
|
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
|
|
116
|
+
```bash
|
|
117
|
+
# Double-encoded JSON string (e.g. from database or API response body)
|
|
118
|
+
json '"{\"name\":\"test\",\"age\":25}"'
|
|
119
|
+
# → auto-detects and parses as { "name": "test", "age": 25 }
|
|
134
120
|
|
|
135
|
-
|
|
121
|
+
# Nested JSON strings inside objects
|
|
122
|
+
json '{"status":"ok","data":"{\"users\":[{\"id\":1}]}"}'
|
|
123
|
+
# → auto-expands "data" field into a real JSON tree
|
|
124
|
+
|
|
125
|
+
# Multi-level serialization
|
|
126
|
+
json '"\"[1,2,3]\""'
|
|
127
|
+
# → recursively unwraps to [1, 2, 3]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This works for:
|
|
131
|
+
- Escaped JSON from `JSON.stringify()` output
|
|
132
|
+
- Log files with embedded JSON payloads
|
|
133
|
+
- API responses where a field contains a JSON string
|
|
134
|
+
- Database columns storing serialized JSON
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Common Use Cases
|
|
139
|
+
|
|
140
|
+
- API debugging (inspect response shape quickly)
|
|
141
|
+
- Backend/frontend contract checks
|
|
142
|
+
- Ad-hoc JSON visualization from logs
|
|
143
|
+
- Payload discussion/demo with teammates
|
|
144
|
+
- **Inspecting serialized JSON from databases or message queues**
|
|
136
145
|
|
|
137
146
|
---
|
|
138
147
|
|
|
@@ -142,24 +151,18 @@ Issues and PRs are welcome.
|
|
|
142
151
|
|
|
143
152
|
### Good contribution ideas
|
|
144
153
|
|
|
145
|
-
- Better
|
|
154
|
+
- Better JSON parse error location hints
|
|
146
155
|
- Theme switch (light/dark)
|
|
147
156
|
- Direct file path support (e.g. `json ./data.json`)
|
|
148
|
-
-
|
|
157
|
+
- Search / highlight / copy JSON path
|
|
149
158
|
|
|
150
|
-
### Local
|
|
159
|
+
### Local dev
|
|
151
160
|
|
|
152
161
|
```bash
|
|
153
162
|
npm install
|
|
154
163
|
npm test
|
|
155
164
|
```
|
|
156
165
|
|
|
157
|
-
Before submitting:
|
|
158
|
-
|
|
159
|
-
- Ensure code runs correctly
|
|
160
|
-
- Ensure README examples still work
|
|
161
|
-
- Keep changes focused and clear
|
|
162
|
-
|
|
163
166
|
---
|
|
164
167
|
|
|
165
168
|
## License
|
package/bin/json.js
CHANGED
|
@@ -100,8 +100,11 @@ function valueToHtml(value, key = null) {
|
|
|
100
100
|
return `<div class=\"line\">${keyHtml}<span>${escapeHtml(String(value))}</span></div>`
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function toHtml(jsonObj) {
|
|
104
|
-
const
|
|
103
|
+
function toHtml(jsonObj, deepParsedObj) {
|
|
104
|
+
const rawBody = valueToHtml(jsonObj)
|
|
105
|
+
const parsedBody = valueToHtml(deepParsedObj)
|
|
106
|
+
// 检测是否有差异(有嵌套 JSON 字符串可以展开)
|
|
107
|
+
const hasDiff = JSON.stringify(jsonObj) !== JSON.stringify(deepParsedObj)
|
|
105
108
|
return `<!doctype html>
|
|
106
109
|
<html lang=\"en\">
|
|
107
110
|
<head>
|
|
@@ -130,6 +133,8 @@ function toHtml(jsonObj) {
|
|
|
130
133
|
margin-bottom: 16px;
|
|
131
134
|
display: flex;
|
|
132
135
|
gap: 8px;
|
|
136
|
+
align-items: center;
|
|
137
|
+
flex-wrap: wrap;
|
|
133
138
|
}
|
|
134
139
|
button {
|
|
135
140
|
border: 1px solid #475569;
|
|
@@ -142,6 +147,51 @@ function toHtml(jsonObj) {
|
|
|
142
147
|
button:hover {
|
|
143
148
|
background: #334155;
|
|
144
149
|
}
|
|
150
|
+
/* 开关样式 */
|
|
151
|
+
.toggle-wrap {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
margin-left: auto;
|
|
156
|
+
font-size: 13px;
|
|
157
|
+
color: #94a3b8;
|
|
158
|
+
}
|
|
159
|
+
.toggle-wrap.hidden { display: none; }
|
|
160
|
+
.toggle {
|
|
161
|
+
position: relative;
|
|
162
|
+
width: 40px;
|
|
163
|
+
height: 22px;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
.toggle input {
|
|
167
|
+
opacity: 0;
|
|
168
|
+
width: 0;
|
|
169
|
+
height: 0;
|
|
170
|
+
}
|
|
171
|
+
.toggle .slider {
|
|
172
|
+
position: absolute;
|
|
173
|
+
inset: 0;
|
|
174
|
+
background: #334155;
|
|
175
|
+
border-radius: 22px;
|
|
176
|
+
transition: background 0.2s;
|
|
177
|
+
}
|
|
178
|
+
.toggle .slider::before {
|
|
179
|
+
content: '';
|
|
180
|
+
position: absolute;
|
|
181
|
+
width: 16px;
|
|
182
|
+
height: 16px;
|
|
183
|
+
left: 3px;
|
|
184
|
+
bottom: 3px;
|
|
185
|
+
background: #e2e8f0;
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
transition: transform 0.2s;
|
|
188
|
+
}
|
|
189
|
+
.toggle input:checked + .slider {
|
|
190
|
+
background: #58a6ff;
|
|
191
|
+
}
|
|
192
|
+
.toggle input:checked + .slider::before {
|
|
193
|
+
transform: translateX(18px);
|
|
194
|
+
}
|
|
145
195
|
details {
|
|
146
196
|
margin-left: 16px;
|
|
147
197
|
}
|
|
@@ -180,17 +230,71 @@ function toHtml(jsonObj) {
|
|
|
180
230
|
<div class=\"toolbar\">
|
|
181
231
|
<button id=\"expand-all\">Expand all</button>
|
|
182
232
|
<button id=\"collapse-all\">Collapse all</button>
|
|
233
|
+
<div class=\"toggle-wrap${hasDiff ? '' : ' hidden'}\" title=\"Parse embedded JSON strings inside values\">
|
|
234
|
+
<span>Parse JSON strings</span>
|
|
235
|
+
<label class=\"toggle\">
|
|
236
|
+
<input type=\"checkbox\" id=\"deep-parse-toggle\" />
|
|
237
|
+
<span class=\"slider\"></span>
|
|
238
|
+
</label>
|
|
239
|
+
</div>
|
|
183
240
|
</div>
|
|
184
|
-
<main>${
|
|
241
|
+
<main id=\"raw-view\">${rawBody}</main>
|
|
242
|
+
<main id=\"parsed-view\" style=\"display:none\">${parsedBody}</main>
|
|
185
243
|
<script>
|
|
186
|
-
const details = () => Array.from(document.querySelectorAll('details'))
|
|
244
|
+
const details = () => Array.from(document.querySelectorAll('main:not([style*=\"display:none\"]) details'))
|
|
187
245
|
document.getElementById('expand-all').addEventListener('click', () => details().forEach((d) => d.open = true))
|
|
188
246
|
document.getElementById('collapse-all').addEventListener('click', () => details().forEach((d) => d.open = false))
|
|
247
|
+
|
|
248
|
+
const toggle = document.getElementById('deep-parse-toggle')
|
|
249
|
+
const rawView = document.getElementById('raw-view')
|
|
250
|
+
const parsedView = document.getElementById('parsed-view')
|
|
251
|
+
if (toggle) {
|
|
252
|
+
toggle.addEventListener('change', () => {
|
|
253
|
+
if (toggle.checked) {
|
|
254
|
+
rawView.style.display = 'none'
|
|
255
|
+
parsedView.style.display = ''
|
|
256
|
+
} else {
|
|
257
|
+
rawView.style.display = ''
|
|
258
|
+
parsedView.style.display = 'none'
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
}
|
|
189
262
|
</script>
|
|
190
263
|
</body>
|
|
191
264
|
</html>`
|
|
192
265
|
}
|
|
193
266
|
|
|
267
|
+
/**
|
|
268
|
+
* 递归遍历 JSON 对象,尝试将值为 JSON 字符串的字段自动解析为对象
|
|
269
|
+
* 比如 { "data": "{\"name\":\"test\"}" } → { "data": { "name": "test" } }
|
|
270
|
+
* 这在 API 响应和日志中非常常见
|
|
271
|
+
*/
|
|
272
|
+
function deepParseJsonStrings(obj) {
|
|
273
|
+
if (obj === null || obj === undefined) return obj
|
|
274
|
+
if (Array.isArray(obj)) return obj.map(deepParseJsonStrings)
|
|
275
|
+
if (typeof obj === 'object') {
|
|
276
|
+
const result = {}
|
|
277
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
278
|
+
result[key] = deepParseJsonStrings(value)
|
|
279
|
+
}
|
|
280
|
+
return result
|
|
281
|
+
}
|
|
282
|
+
if (typeof obj === 'string') {
|
|
283
|
+
const trimmed = obj.trim()
|
|
284
|
+
// 只尝试解析看起来像 JSON 对象或数组的字符串
|
|
285
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
286
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(trimmed)
|
|
289
|
+
return deepParseJsonStrings(parsed)
|
|
290
|
+
} catch {
|
|
291
|
+
return obj
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return obj
|
|
296
|
+
}
|
|
297
|
+
|
|
194
298
|
function openInBrowser(filePath) {
|
|
195
299
|
const platform = process.platform
|
|
196
300
|
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'
|
|
@@ -234,11 +338,54 @@ async function main() {
|
|
|
234
338
|
try {
|
|
235
339
|
parsed = JSON.parse(input)
|
|
236
340
|
} catch {
|
|
237
|
-
|
|
238
|
-
|
|
341
|
+
// 尝试处理 JSON 序列化后的字符串(双重转义)
|
|
342
|
+
// 比如:"{\"name\":\"test\"}" 或 '"{\\\"name\\\":\\\"test\\\"}"'
|
|
343
|
+
// 这种情况常见于:日志输出、API 响应中嵌套的 JSON 字符串、数据库存储的 JSON
|
|
344
|
+
try {
|
|
345
|
+
// 第一步:去掉首尾引号(如果有的话)
|
|
346
|
+
let cleaned = input.trim()
|
|
347
|
+
if ((cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
|
348
|
+
(cleaned.startsWith("'") && cleaned.endsWith("'"))) {
|
|
349
|
+
cleaned = cleaned.slice(1, -1)
|
|
350
|
+
}
|
|
351
|
+
// 第二步:处理转义字符
|
|
352
|
+
// 替换 \" → ",\\ → \,\n → 换行,\t → tab
|
|
353
|
+
cleaned = cleaned
|
|
354
|
+
.replace(/\\"/g, '"')
|
|
355
|
+
.replace(/\\\\/g, '\\')
|
|
356
|
+
.replace(/\\n/g, '\n')
|
|
357
|
+
.replace(/\\t/g, '\t')
|
|
358
|
+
.replace(/\\r/g, '\r')
|
|
359
|
+
parsed = JSON.parse(cleaned)
|
|
360
|
+
console.log('ℹ️ Detected serialized JSON string, auto-unescaped.')
|
|
361
|
+
} catch {
|
|
362
|
+
// 第三步:尝试递归解析(多层序列化的情况)
|
|
363
|
+
try {
|
|
364
|
+
let result = input.trim()
|
|
365
|
+
let depth = 0
|
|
366
|
+
const maxDepth = 5
|
|
367
|
+
while (typeof result === 'string' && depth < maxDepth) {
|
|
368
|
+
result = JSON.parse(result)
|
|
369
|
+
depth++
|
|
370
|
+
}
|
|
371
|
+
if (typeof result === 'object' && result !== null) {
|
|
372
|
+
parsed = result
|
|
373
|
+
console.log(`ℹ️ Detected ${depth}-level serialized JSON string, auto-parsed.`)
|
|
374
|
+
} else {
|
|
375
|
+
console.error('Input is not valid JSON.')
|
|
376
|
+
process.exit(1)
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
console.error('Input is not valid JSON.')
|
|
380
|
+
process.exit(1)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
239
383
|
}
|
|
240
384
|
|
|
241
|
-
|
|
385
|
+
// 生成深度解析版本(展开嵌套 JSON 字符串),但默认不启用
|
|
386
|
+
const deepParsed = deepParseJsonStrings(parsed)
|
|
387
|
+
|
|
388
|
+
const html = toHtml(parsed, deepParsed)
|
|
242
389
|
const filePath = path.join(os.tmpdir(), `json-viewer-${Date.now()}.html`)
|
|
243
390
|
await fs.writeFile(filePath, html, 'utf8')
|
|
244
391
|
openInBrowser(filePath)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zjy4fun/json-open",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Open JSON (stdin or inline text) in a browser with collapsible tree view",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
"browser",
|
|
18
18
|
"formatter"
|
|
19
19
|
],
|
|
20
|
-
"author": "",
|
|
20
|
+
"author": "zjy4fun",
|
|
21
21
|
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/zjy4fun/json-open"
|
|
25
|
+
},
|
|
22
26
|
"publishConfig": {
|
|
23
27
|
"access": "public",
|
|
24
28
|
"registry": "https://registry.npmjs.org"
|