@zjy4fun/json-open 0.1.1 → 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
+
40
+ ```bash
41
+ npm i -g @zjy4fun/json-open
42
+ ```
39
43
 
40
- Configure npm for GitHub Packages first:
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,56 +77,71 @@ 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
+ ## CLI Usage
84
85
 
85
- 1. **API debugging**
86
- Inspect response shape quickly, especially nested data.
86
+ ```bash
87
+ json [inline-json]
88
+ ```
87
89
 
88
- 2. **Backend/frontend integration checks**
89
- Verify missing fields or type mismatches after API changes.
90
+ Input sources:
90
91
 
91
- 3. **Ad-hoc JSON inspection**
92
- Visualize JSON copied from logs, queues, or snapshots.
92
+ - `stdin` (pipe)
93
+ - Inline JSON argument
93
94
 
94
- 4. **Team discussion/demo**
95
- Share a clearer structure view when discussing payloads.
95
+ Options:
96
96
 
97
- ---
97
+ - `-h, --help` Show help
98
+ - `-v, --version` Show version
98
99
 
99
- ## Command behavior
100
+ Examples:
100
101
 
101
102
  ```bash
102
- json
103
+ json --help
104
+ json --version
105
+ json '{"ok":true}'
103
106
  ```
104
107
 
105
- Input source:
108
+ If no input is provided, usage help is printed.
109
+
110
+ ---
106
111
 
107
- - stdin (pipe)
108
- - inline argument JSON string
112
+ ## Serialized JSON String Support
109
113
 
110
- Example:
114
+ `json-open` now automatically handles serialized/escaped JSON strings — a common pain point when working with logs, databases, and APIs.
111
115
 
112
116
  ```bash
113
- json '{"ok":true}'
114
- ```
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 }
115
120
 
116
- If no input is provided, it prints usage help.
121
+ # Nested JSON strings inside objects
122
+ json '{"status":"ok","data":"{\"users\":[{\"id\":1}]}"}'
123
+ # → auto-expands "data" field into a real JSON tree
117
124
 
118
- ---
125
+ # Multi-level serialization
126
+ json '"\"[1,2,3]\""'
127
+ # → recursively unwraps to [1, 2, 3]
128
+ ```
119
129
 
120
- ## Release & distribution
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
121
135
 
122
- Included GitHub Actions workflows:
136
+ ---
123
137
 
124
- - `CI`: basic validation flow
125
- - `Publish to GitHub Packages`: publish to GPR
126
- - `Publish to npm (Trusted Publishing)`: reserved for npm OIDC flow
138
+ ## Common Use Cases
127
139
 
128
- Current primary distribution: **GitHub Packages**.
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**
129
145
 
130
146
  ---
131
147
 
@@ -135,24 +151,18 @@ Issues and PRs are welcome.
135
151
 
136
152
  ### Good contribution ideas
137
153
 
138
- - Better error diagnostics (e.g. JSON syntax location)
154
+ - Better JSON parse error location hints
139
155
  - Theme switch (light/dark)
140
156
  - Direct file path support (e.g. `json ./data.json`)
141
- - Rich interactions (search, highlight, copy JSON path)
157
+ - Search / highlight / copy JSON path
142
158
 
143
- ### Local development
159
+ ### Local dev
144
160
 
145
161
  ```bash
146
162
  npm install
147
163
  npm test
148
164
  ```
149
165
 
150
- Before submitting:
151
-
152
- - Ensure code runs correctly
153
- - Ensure README examples still work
154
- - Keep changes focused and clear
155
-
156
166
  ---
157
167
 
158
168
  ## License
package/README.zh-CN.md CHANGED
@@ -107,9 +107,16 @@ json
107
107
  - stdin(管道)
108
108
  - 命令行参数中的 JSON 字符串
109
109
 
110
+ 参数选项:
111
+
112
+ - `-h, --help` 显示帮助
113
+ - `-v, --version` 显示版本
114
+
110
115
  示例:
111
116
 
112
117
  ```bash
118
+ json --help
119
+ json --version
113
120
  json '{"ok":true}'
114
121
  ```
115
122
 
package/bin/json.js CHANGED
@@ -1,8 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs/promises'
3
+ import fsSync from 'node:fs'
3
4
  import os from 'node:os'
4
5
  import path from 'node:path'
5
6
  import { spawn } from 'node:child_process'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ function getCliVersion() {
10
+ try {
11
+ const currentFile = fileURLToPath(import.meta.url)
12
+ const packageJsonPath = path.resolve(path.dirname(currentFile), '..', 'package.json')
13
+ const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'))
14
+ return packageJson.version
15
+ } catch {
16
+ return 'unknown'
17
+ }
18
+ }
19
+
20
+ function printHelp() {
21
+ console.log(`json-open
22
+
23
+ Usage:
24
+ json <json-string>
25
+ cat data.json | json
26
+ curl https://example.com/api | json
27
+
28
+ Options:
29
+ -h, --help Show help
30
+ -v, --version Show version`)
31
+ }
6
32
 
7
33
  function readStdin() {
8
34
  return new Promise((resolve, reject) => {
@@ -74,8 +100,11 @@ function valueToHtml(value, key = null) {
74
100
  return `<div class=\"line\">${keyHtml}<span>${escapeHtml(String(value))}</span></div>`
75
101
  }
76
102
 
77
- function toHtml(jsonObj) {
78
- 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)
79
108
  return `<!doctype html>
80
109
  <html lang=\"en\">
81
110
  <head>
@@ -104,6 +133,8 @@ function toHtml(jsonObj) {
104
133
  margin-bottom: 16px;
105
134
  display: flex;
106
135
  gap: 8px;
136
+ align-items: center;
137
+ flex-wrap: wrap;
107
138
  }
108
139
  button {
109
140
  border: 1px solid #475569;
@@ -116,6 +147,51 @@ function toHtml(jsonObj) {
116
147
  button:hover {
117
148
  background: #334155;
118
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
+ }
119
195
  details {
120
196
  margin-left: 16px;
121
197
  }
@@ -154,17 +230,71 @@ function toHtml(jsonObj) {
154
230
  <div class=\"toolbar\">
155
231
  <button id=\"expand-all\">Expand all</button>
156
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>
157
240
  </div>
158
- <main>${body}</main>
241
+ <main id=\"raw-view\">${rawBody}</main>
242
+ <main id=\"parsed-view\" style=\"display:none\">${parsedBody}</main>
159
243
  <script>
160
- const details = () => Array.from(document.querySelectorAll('details'))
244
+ const details = () => Array.from(document.querySelectorAll('main:not([style*=\"display:none\"]) details'))
161
245
  document.getElementById('expand-all').addEventListener('click', () => details().forEach((d) => d.open = true))
162
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
+ }
163
262
  </script>
164
263
  </body>
165
264
  </html>`
166
265
  }
167
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
+
168
298
  function openInBrowser(filePath) {
169
299
  const platform = process.platform
170
300
  const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'
@@ -178,10 +308,22 @@ function openInBrowser(filePath) {
178
308
  }
179
309
 
180
310
  async function main() {
181
- const inlineInput = process.argv.slice(2).join(' ').trim()
311
+ const args = process.argv.slice(2)
312
+
313
+ if (args.includes('-h') || args.includes('--help')) {
314
+ printHelp()
315
+ process.exit(0)
316
+ }
317
+
318
+ if (args.includes('-v') || args.includes('--version')) {
319
+ console.log(getCliVersion())
320
+ process.exit(0)
321
+ }
322
+
323
+ const inlineInput = args.join(' ').trim()
182
324
 
183
325
  if (!inlineInput && process.stdin.isTTY) {
184
- console.error('Usage: curl https://example.com | json\n or: json "{\"hello\":\"world\"}"')
326
+ printHelp()
185
327
  process.exit(1)
186
328
  }
187
329
 
@@ -196,11 +338,54 @@ async function main() {
196
338
  try {
197
339
  parsed = JSON.parse(input)
198
340
  } catch {
199
- console.error('Input is not valid JSON.')
200
- 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
+ }
201
383
  }
202
384
 
203
- const html = toHtml(parsed)
385
+ // 生成深度解析版本(展开嵌套 JSON 字符串),但默认不启用
386
+ const deepParsed = deepParseJsonStrings(parsed)
387
+
388
+ const html = toHtml(parsed, deepParsed)
204
389
  const filePath = path.join(os.tmpdir(), `json-viewer-${Date.now()}.html`)
205
390
  await fs.writeFile(filePath, html, 'utf8')
206
391
  openInBrowser(filePath)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjy4fun/json-open",
3
- "version": "0.1.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"