insomnia-plugin-jsonc 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,16 +1,70 @@
1
1
  # insomnia-plugin-jsonc
2
2
 
3
- Strip JSON comments from your request body.
3
+ Write comments and trailing commas in your JSON request bodies. They are stripped before the request is sent.
4
+
5
+ ## Example
6
+
7
+ What you write in Insomnia:
8
+
9
+ ```jsonc
10
+ {
11
+ "page": 1, // current page
12
+ /* "debug": true, */
13
+ "url": "https://example.com", // `//` inside strings is kept
14
+ "tags": ["a", "b",],
15
+ }
16
+ ```
17
+
18
+ What the server receives:
19
+
20
+ ```json
21
+ {
22
+ "page": 1,
23
+
24
+ "url": "https://example.com",
25
+ "tags": ["a", "b"]
26
+ }
27
+ ```
4
28
 
5
29
  ## Installation
6
30
 
7
- - Open Insomnia options (Ctrl + , | Cmd + ,)
8
- - Type `insomnia-plugin-jsonc` on the Plugins tab
31
+ ### Plugin Hub
32
+
33
+ Install from the [Insomnia Plugin Hub](https://insomnia.rest/plugins/insomnia-plugin-jsonc).
34
+
35
+ ### From Insomnia
36
+
37
+ 1. Open Preferences (Ctrl + , | Cmd + ,)
38
+ 2. Go to the Plugins tab
39
+ 3. Enter `insomnia-plugin-jsonc` and click Install Plugin
40
+
41
+ ### Manually
42
+
43
+ Copy this folder into the Insomnia plugins directory:
44
+
45
+ - Windows: `%APPDATA%\Insomnia\plugins\`
46
+ - macOS: `~/Library/Application Support/Insomnia/plugins/`
47
+ - Linux: `~/.config/Insomnia/plugins/`
48
+
49
+ ## What is stripped
50
+
51
+ - Line comments: `// ...` up to the end of the line
52
+ - Block comments: `/* ... */`, which can span several lines
53
+ - Trailing commas before `]` or `}`, even with a comment in between
54
+
55
+ Content inside strings is never touched, so `"https://example.com"` or `"a /* b */"` stay as they are.
56
+
57
+ The plugin only applies to JSON bodies: `application/json`, with or without parameters (`application/json; charset=utf-8`), and `+json` types (`application/vnd.api+json`). Other bodies are sent unchanged.
58
+
59
+ ## Limitations
60
+
61
+ - Whitespace around a removed comment is kept, so the sent body may contain extra spaces or blank lines.
62
+ - The result is not validated: invalid JSON is sent as is, and the server reports the error.
9
63
 
10
- ## Usage
64
+ ## How it works
11
65
 
12
- Just write valid JavaScript comments in JSON body request and send your request without minding them.
66
+ A request hook strips comments with a regular expression adapted from [`json-easy-strip`](https://www.npmjs.com/package/json-easy-strip), then removes trailing commas. The body is not parsed and serialized again, so its formatting is preserved.
13
67
 
14
- ## Deps
68
+ ## License
15
69
 
16
- Use [`json-easy-strip`](https://www.npmjs.com/package/json-easy-strip)'s RegExp to strip comments.
70
+ MIT
package/cover.svg ADDED
@@ -0,0 +1,28 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 400" width="1200" height="400">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#4000BF"/>
5
+ <stop offset="1" stop-color="#2A0080"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="1200" height="400" fill="url(#bg)"/>
9
+ <!-- { } logo -->
10
+ <g transform="translate(80 72) scale(0.5)">
11
+ <g fill="none" stroke="#FFFFFF" stroke-width="32" stroke-linecap="round" stroke-linejoin="round">
12
+ <path d="M176 112h-16c-24 0-40 16-40 40v56c0 24-16 48-40 48 24 0 40 24 40 48v56c0 24 16 40 40 40h16"/>
13
+ <path d="M336 112h16c24 0 40 16 40 40v56c0 24 16 48 40 48-24 0-40 24-40 48v56c0 24-16 40-40 40h-16"/>
14
+ </g>
15
+ <g stroke="#B794FF" stroke-width="28" stroke-linecap="round">
16
+ <path d="M196 312l40-112"/>
17
+ <path d="M276 312l40-112"/>
18
+ </g>
19
+ </g>
20
+ <g font-family="Menlo, Consolas, 'DejaVu Sans Mono', monospace">
21
+ <text x="400" y="190" font-size="64" font-weight="bold" fill="#FFFFFF">JSON Comments</text>
22
+ <text x="400" y="250" font-size="30" fill="#D9C7FF">Strips comments from JSON request bodies.</text>
23
+ <!-- before → after -->
24
+ <text x="400" y="318" font-size="24" fill="#FFFFFF">{"a": 1<tspan fill="#B794FF" text-decoration="line-through">, /* note */</tspan>}</text>
25
+ <text x="720" y="318" font-size="24" fill="#B794FF">→</text>
26
+ <text x="770" y="318" font-size="24" fill="#FFFFFF">{"a": 1}</text>
27
+ </g>
28
+ </svg>
package/icon.svg ADDED
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
2
+ <rect width="512" height="512" rx="96" fill="#4000BF"/>
3
+ <!-- { } -->
4
+ <g fill="none" stroke="#FFFFFF" stroke-width="32" stroke-linecap="round" stroke-linejoin="round">
5
+ <path d="M176 112h-16c-24 0-40 16-40 40v56c0 24-16 48-40 48 24 0 40 24 40 48v56c0 24 16 40 40 40h16"/>
6
+ <path d="M336 112h16c24 0 40 16 40 40v56c0 24 16 48 40 48-24 0-40 24-40 48v56c0 24-16 40-40 40h-16"/>
7
+ </g>
8
+ <!-- // -->
9
+ <g stroke="#B794FF" stroke-width="28" stroke-linecap="round">
10
+ <path d="M196 312l40-112"/>
11
+ <path d="M276 312l40-112"/>
12
+ </g>
13
+ </svg>
package/main.js CHANGED
@@ -1,34 +1,35 @@
1
+ const { stripJSONC } = require("./strip");
2
+
1
3
  /**
2
4
  * JSON Content-Type
3
5
  */
4
6
  const CONTENT_TYPE_JSON = "application/json";
5
7
 
6
8
  /**
7
- * Strip comments from a JSON string
8
- *
9
- * Taken from [`json-easy-strip`](https://www.npmjs.com/package/json-easy-strip).
9
+ * Check if a MIME type is JSON, ignoring its parameters
10
10
  *
11
- * Extracted to not parse and stringify again the JSON string
11
+ * Matches `application/json` and structured syntax suffixes like `application/vnd.api+json`.
12
12
  *
13
- * @param data the JSON string with comments
14
- * @returns the JSON string without comments
13
+ * @param mimeType the MIME type, possibly with parameters (`application/json; charset=utf-8`)
14
+ * @returns whether the MIME type is JSON
15
15
  */
16
- const stripJSONComments = (data) =>
17
- data.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) =>
18
- g ? "" : m
19
- );
16
+ const isJSONMimeType = (mimeType) => {
17
+ const type = String(mimeType ?? "").split(";")[0].trim().toLowerCase();
18
+ return type === CONTENT_TYPE_JSON || type.endsWith("+json");
19
+ };
20
20
 
21
21
  /**
22
- * Hook to update request body and remove comments
22
+ * Hook to update request body and remove comments and trailing commas
23
23
  * @param context insomnia request context
24
24
  * @link https://docs.insomnia.rest/insomnia/context-object-reference
25
25
  */
26
26
  const updateRequestBodyHook = (context) => {
27
27
  const body = context.request.getBody();
28
- if (body.mimeType === CONTENT_TYPE_JSON) {
28
+ // `text` is undefined on an empty body
29
+ if (isJSONMimeType(body.mimeType) && typeof body.text === "string") {
29
30
  context.request.setBody({
30
31
  ...body,
31
- text: stripJSONComments(body.text),
32
+ text: stripJSONC(body.text),
32
33
  });
33
34
  }
34
35
  };
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "insomnia-plugin-jsonc",
3
- "version": "0.1.0",
4
- "description": "Insomnia plugin to use comments in JSON body",
3
+ "version": "0.2.0",
4
+ "description": "Insomnia plugin to strip JSON body comments",
5
5
  "main": "main.js",
6
6
  "insomnia": {
7
7
  "name": "jsonc",
8
8
  "displayName": "JSON Comments Plugin",
9
- "description": "Plugin to use comments in JSON body.",
9
+ "description": "Strips comments from JSON request bodies.",
10
+ "images": {
11
+ "icon": "icon.svg",
12
+ "cover": "cover.svg"
13
+ },
10
14
  "publisher": {
11
15
  "name": "Guillaume Ongenae",
12
16
  "icon": "https://avatars.githubusercontent.com/u/9453090"
@@ -14,11 +18,11 @@
14
18
  "unlisted": false
15
19
  },
16
20
  "scripts": {
17
- "test": "node test.js"
21
+ "test": "node --test test.js"
18
22
  },
19
23
  "repository": {
20
24
  "type": "git",
21
- "url": "git+ssh://git@github.com/algoan/insomnia-plugin-jsonc.git"
25
+ "url": "git+ssh://git@github.com/g-ongenae/insomnia-plugin-jsonc.git"
22
26
  },
23
27
  "keywords": [
24
28
  "insomnia",
@@ -33,6 +37,7 @@
33
37
  "url": "https://github.com/g-ongenae/insomnia-plugin-jsonc/issues"
34
38
  },
35
39
  "homepage": "https://github.com/g-ongenae/insomnia-plugin-jsonc#readme",
36
- "dependencies": {},
37
- "devDependencies": {}
40
+ "devDependencies": {
41
+ "fast-check": "^4.10.2"
42
+ }
38
43
  }
package/strip.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Strip comments from a JSON string
3
+ *
4
+ * Adapted from [`json-easy-strip`](https://www.npmjs.com/package/json-easy-strip),
5
+ * with strings matched as a whole (any escaped character included) so that
6
+ * an escaped backslash before a closing quote is handled.
7
+ *
8
+ * Extracted to not parse and stringify again the JSON string
9
+ *
10
+ * @param data the JSON string with comments
11
+ * @returns the JSON string without comments
12
+ */
13
+ const stripJSONComments = (data) =>
14
+ data.replace(/"(?:\\.|[^"\\])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) =>
15
+ g ? "" : m
16
+ );
17
+
18
+ /**
19
+ * Strip trailing commas before a closing `]` or `}` from a JSON string
20
+ *
21
+ * Must run after `stripJSONComments`, so that a comment between the comma
22
+ * and the closing bracket does not hide it.
23
+ *
24
+ * @param data the JSON string without comments
25
+ * @returns the JSON string without trailing commas
26
+ */
27
+ const stripTrailingCommas = (data) =>
28
+ data.replace(/"(?:\\.|[^"\\])*"|,(\s*[\]}])/g, (m, g) =>
29
+ g === undefined ? m : g
30
+ );
31
+
32
+ /**
33
+ * Strip comments and trailing commas from a JSONC string
34
+ *
35
+ * @param data the JSONC string
36
+ * @returns the JSON string
37
+ */
38
+ const stripJSONC = (data) => stripTrailingCommas(stripJSONComments(data));
39
+
40
+ module.exports = { stripJSONComments, stripTrailingCommas, stripJSONC };
package/test.js CHANGED
@@ -1,44 +1,92 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fc = require('fast-check');
1
4
  const { requestHooks } = require('./main');
5
+ const { stripJSONC } = require('./strip');
2
6
 
3
7
  /**
4
- * Create a fake insomnia context with a validation on the setBody
5
- * @param {object} givenBody
6
- * @param {object} expectedBody
7
- * @returns a fake insomnia context with a validation
8
+ * Run the hook on a fake insomnia context
9
+ * @param {object} givenBody the body returned by getBody
10
+ * @returns the list of bodies passed to setBody
8
11
  */
9
- const fakeContext = (id, givenBody, expectedBody) => {
10
- return {
12
+ const runHook = (givenBody) => {
13
+ const calls = [];
14
+ requestHooks[0]({
11
15
  request: {
12
16
  getBody: () => givenBody,
13
- setBody: (actualBody) => {
14
- const result = actualBody.text === expectedBody.text
15
- ? `${id} | passed`
16
- : `${id} | expected: '${expectedBody.text}' => received: '${actualBody.text}'`;
17
-
18
- console.log(result);
19
- },
20
- }
21
- };
17
+ setBody: (body) => calls.push(body),
18
+ },
19
+ });
20
+ return calls;
22
21
  };
23
22
 
24
23
  /**
25
- * List of test cases
24
+ * List of test cases: [name, given body, expected body]
26
25
  */
27
26
  const cases = [
28
- // Should not alter non JSON body
29
- [{ mimeType: 'non-json', text: 'foo' }, { mimeType: 'non-json', text: 'foo' }],
30
- // Should not alter JSON body without comments
31
- [{ mimeType: 'application/json', text: 'foo' }, { mimeType: 'application/json', text: 'foo' }],
32
- [{ mimeType: 'application/json', text: '{"foo": "bar"}' }, { mimeType: 'application/json', text: '{"foo": "bar"}' }],
33
- // Should remove comments from JSON body (space are preserved)
34
- [{ mimeType: 'application/json', text: '{"foo": /* hello */ "bar"}' }, { mimeType: 'application/json', text: '{"foo": "bar"}' }],
35
- [{ mimeType: 'application/json', text: '/* hello */ {"foo": "bar"}' }, { mimeType: 'application/json', text: ' {"foo": "bar"}' }],
36
- [{ mimeType: 'application/json', text: '{"foo": "bar" // hello\n }' }, { mimeType: 'application/json', text: '{"foo": "bar" \n }' }],
27
+ ['should not alter invalid JSON body without comments', { mimeType: 'application/json', text: 'foo' }, { mimeType: 'application/json', text: 'foo' }],
28
+ ['should not alter JSON body without comments', { mimeType: 'application/json', text: '{"foo": "bar"}' }, { mimeType: 'application/json', text: '{"foo": "bar"}' }],
29
+ // Spaces around comments are preserved
30
+ ['should remove inline block comment', { mimeType: 'application/json', text: '{"foo": /* hello */ "bar"}' }, { mimeType: 'application/json', text: '{"foo": "bar"}' }],
31
+ ['should remove leading block comment', { mimeType: 'application/json', text: '/* hello */ {"foo": "bar"}' }, { mimeType: 'application/json', text: ' {"foo": "bar"}' }],
32
+ ['should remove line comment after the last item', { mimeType: 'application/json', text: '{"a": 1 // last\n}' }, { mimeType: 'application/json', text: '{"a": 1 \n}' }],
33
+ ['should remove line comment', { mimeType: 'application/json', text: '{"foo": "bar" // hello\n }' }, { mimeType: 'application/json', text: '{"foo": "bar" \n }' }],
34
+ ['should remove trailing comma in object', { mimeType: 'application/json', text: '{"a": 1, }' }, { mimeType: 'application/json', text: '{"a": 1 }' }],
35
+ ['should remove trailing comma in array', { mimeType: 'application/json', text: '[1,\n]' }, { mimeType: 'application/json', text: '[1\n]' }],
36
+ ['should remove trailing comma followed by a comment', { mimeType: 'application/json', text: '{"a": 1, // last\n}' }, { mimeType: 'application/json', text: '{"a": 1 \n}' }],
37
+ ['should remove trailing comma followed by a block comment', { mimeType: 'application/json', text: '[1, /* last */]' }, { mimeType: 'application/json', text: '[1 ]' }],
38
+ ['should not remove comma-like content in strings', { mimeType: 'application/json', text: '{"a": ",}", "b": ",]"}' }, { mimeType: 'application/json', text: '{"a": ",}", "b": ",]"}' }],
39
+ ['should handle JSON MIME type with parameters', { mimeType: 'application/json; charset=utf-8', text: '{"a": 1 /* b */}' }, { mimeType: 'application/json; charset=utf-8', text: '{"a": 1 }' }],
40
+ ['should handle JSON MIME type case-insensitively', { mimeType: 'Application/JSON', text: '{"a": 1 /* b */}' }, { mimeType: 'Application/JSON', text: '{"a": 1 }' }],
41
+ ['should handle +json MIME types', { mimeType: 'application/vnd.api+json', text: '{"a": 1 /* b */}' }, { mimeType: 'application/vnd.api+json', text: '{"a": 1 }' }],
37
42
  ];
38
43
 
39
- /**
40
- * Call and test the hook
41
- */
42
- for (const [index, test] of Object.entries(cases)) {
43
- requestHooks[0](fakeContext(index, ...test));
44
+ test('should not call setBody on non JSON body', () => {
45
+ assert.deepEqual(runHook({ mimeType: 'non-json', text: 'foo // bar' }), []);
46
+ assert.deepEqual(runHook({ mimeType: 'application/jsonp', text: 'foo // bar' }), []);
47
+ assert.deepEqual(runHook({ text: 'foo // bar' }), []);
48
+ });
49
+
50
+ test('should not throw nor call setBody on empty JSON body', () => {
51
+ assert.deepEqual(runHook({ mimeType: 'application/json' }), []);
52
+ });
53
+
54
+ for (const [name, givenBody, expectedBody] of cases) {
55
+ test(name, () => {
56
+ const calls = runHook(givenBody);
57
+ assert.equal(calls.length, 1);
58
+ assert.equal(calls[0].text, expectedBody.text);
59
+ });
44
60
  }
61
+
62
+ test('should keep comment-like content in a string after an escaped backslash', () => {
63
+ const text = String.raw`{"a": "x\\", "b": "// y"}`;
64
+ const [body] = runHook({ mimeType: 'application/json', text });
65
+ assert.equal(body.text, text);
66
+ assert.deepEqual(JSON.parse(body.text), { a: 'x\\', b: '// y' });
67
+ });
68
+
69
+ test('should strip comments and trailing commas inserted in any JSON document', () => {
70
+ const comment = fc.string({ unit: 'grapheme-ascii' }).filter((c) => !c.includes('*/'));
71
+ const lineComments = fc.array(fc.record({ block: fc.option(comment), line: fc.option(comment), trailingComma: fc.boolean() }));
72
+
73
+ fc.assert(
74
+ fc.property(fc.jsonValue(), lineComments, (value, comments) => {
75
+ // JSON.stringify escapes newlines in strings, so each line starts and ends on a token boundary
76
+ const json = JSON.stringify(value, null, 2);
77
+ const text = json
78
+ .split('\n')
79
+ .map((line, i, lines) => {
80
+ const { block = null, line: lineComment = null, trailingComma = false } = comments[i] ?? {};
81
+ // A trailing comma is allowed after the last value of a non-empty array or object
82
+ const isLastValue = /^\s*[\]}]/.test(lines[i + 1] ?? '') && !/[[{]$/.test(line);
83
+ const comma = trailingComma && isLastValue ? ',' : '';
84
+ return `${block === null ? '' : `/*${block}*/`}${line}${comma}${lineComment === null ? '' : `//${lineComment}`}`;
85
+ })
86
+ .join('\n');
87
+
88
+ // Compare with the parsed original rather than `value`, since -0 does not survive JSON
89
+ assert.deepEqual(JSON.parse(stripJSONC(text)), JSON.parse(json));
90
+ }),
91
+ );
92
+ });