@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.
@@ -18,9 +18,8 @@ jobs:
18
18
 
19
19
  - uses: actions/setup-node@v4
20
20
  with:
21
- node-version: 20
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 with a collapsible tree view (supports stdin and inline JSON text).
3
+ Open JSON in your browser as a collapsible tree view.
4
4
 
5
- > A tiny CLI for command-line users: feed JSON, instantly inspect it in a browser with foldable structure.
6
-
7
- ## Demo
5
+ > A tiny CLI for quickly inspecting JSON from APIs, logs, or inline text.
8
6
 
9
7
  ![json-open demo](./demo.gif)
10
8
 
11
9
  ---
12
10
 
13
- ## Why this exists
11
+ ## Why json-open?
14
12
 
15
- Reading JSON in a terminal is often painful:
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
- - Heavy tools feel overkill for quick debugging
17
+ - Full-featured tools can feel heavy for quick checks
20
18
 
21
- `json-open` keeps this simple: **make JSON inspection fast and visual**.
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
- - ✅ Pipe input: `curl ... | json`
28
- - ✅ Inline JSON: `json '{"a":1}'`
29
- - ✅ Collapsible tree view in browser
30
- - ✅ Expand all / Collapse all buttons
31
- - ✅ Rendered from local temp file (no remote upload)
32
- - ✅ Cross-platform browser open (macOS / Linux / Windows)
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
- ### Option A: GitHub Packages (current primary channel)
38
+ ### npm (recommended)
39
39
 
40
- Configure npm for GitHub Packages first:
40
+ ```bash
41
+ npm i -g @zjy4fun/json-open
42
+ ```
43
+
44
+ After install, use the global command:
41
45
 
42
46
  ```bash
43
- echo "@zjy4fun:registry=https://npm.pkg.github.com" >> ~/.npmrc
44
- echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN" >> ~/.npmrc
47
+ json --version
45
48
  ```
46
49
 
47
- Then install:
50
+ ### Run once with npx (no global install)
48
51
 
49
52
  ```bash
50
- npm i -g @zjy4fun/json-open
53
+ npx @zjy4fun/json-open '{"hello":"world"}'
51
54
  ```
52
55
 
53
- ### Option B: Local development install
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 start
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 view.
80
+ The command opens your default browser and shows a structured JSON tree.
80
81
 
81
82
  ---
82
83
 
83
- ## Common use cases
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 source:
90
+ Input sources:
106
91
 
107
- - stdin (pipe)
108
- - inline argument JSON string
92
+ - `stdin` (pipe)
93
+ - Inline JSON argument
109
94
 
110
95
  Options:
111
96
 
112
- - `-h, --help` show help
113
- - `-v, --version` show CLI 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, it prints usage help.
108
+ If no input is provided, usage help is printed.
124
109
 
125
110
  ---
126
111
 
127
- ## Release & distribution
112
+ ## Serialized JSON String Support
128
113
 
129
- Included GitHub Actions workflows:
114
+ `json-open` now automatically handles serialized/escaped JSON strings — a common pain point when working with logs, databases, and APIs.
130
115
 
131
- - `CI`: basic validation flow
132
- - `Publish to GitHub Packages`: publish to GPR
133
- - `Publish to npm (Trusted Publishing)`: reserved for npm OIDC flow
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
- Current primary distribution: **GitHub Packages**.
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 error diagnostics (e.g. JSON syntax location)
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
- - Rich interactions (search, highlight, copy JSON path)
157
+ - Search / highlight / copy JSON path
149
158
 
150
- ### Local development
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 body = valueToHtml(jsonObj)
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>${body}</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
- console.error('Input is not valid JSON.')
238
- process.exit(1)
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
- const html = toHtml(parsed)
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",
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"