insomnia-plugin-jsonc 0.2.0 → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2026 Guillaume Ongenae
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  Write comments and trailing commas in your JSON request bodies. They are stripped before the request is sent.
4
4
 
5
+ - **The original**: published on npm in January 2022, before the other JSON comments plugins for Insomnia.
6
+ - **The fastest**: 3 to 10 times faster than [`strip-json-comments`](https://github.com/sindresorhus/strip-json-comments), and up to 20 times faster on large bodies than the regular expression other plugins use. See [`bench/`](bench/README.md).
7
+ - **Unbreakable**: handles the cases other implementations get wrong, such as a string ending with an escaped backslash (`"C:\\dir\\"`) or a line comment ended by a lone `\r`. Strings are never altered, empty and malformed bodies never make it throw, and the body is scanned in linear time, even on malformed input. No runtime dependencies, no network access.
8
+
5
9
  ## Example
6
10
 
7
11
  What you write in Insomnia:
8
12
 
13
+ <!-- prettier-ignore -->
9
14
  ```jsonc
10
15
  {
11
16
  "page": 1, // current page
@@ -63,7 +68,9 @@ The plugin only applies to JSON bodies: `application/json`, with or without para
63
68
 
64
69
  ## How it works
65
70
 
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.
71
+ A request hook scans the body once, character by character, jumping over strings and comments. It removes comments and trailing commas, and copies the rest as is: the body is not parsed and serialized again, so its formatting is preserved. A body with nothing to strip is returned untouched.
72
+
73
+ See [`bench/`](bench/README.md) for a comparison with a regular expression, another scanner and [`strip-json-comments`](https://github.com/sindresorhus/strip-json-comments).
67
74
 
68
75
  ## License
69
76
 
package/main.js CHANGED
@@ -14,7 +14,10 @@ const CONTENT_TYPE_JSON = "application/json";
14
14
  * @returns whether the MIME type is JSON
15
15
  */
16
16
  const isJSONMimeType = (mimeType) => {
17
- const type = String(mimeType ?? "").split(";")[0].trim().toLowerCase();
17
+ const type = String(mimeType ?? "")
18
+ .split(";")[0]
19
+ .trim()
20
+ .toLowerCase();
18
21
  return type === CONTENT_TYPE_JSON || type.endsWith("+json");
19
22
  };
20
23
 
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "insomnia-plugin-jsonc",
3
- "version": "0.2.0",
4
- "description": "Insomnia plugin to strip JSON body comments",
3
+ "version": "0.2.1",
4
+ "description": "Write // and /* */ comments in JSON request bodies in Insomnia",
5
5
  "main": "main.js",
6
+ "files": [
7
+ "main.js",
8
+ "strip.js",
9
+ "icon.svg",
10
+ "cover.svg"
11
+ ],
6
12
  "insomnia": {
7
13
  "name": "jsonc",
8
- "displayName": "JSON Comments Plugin",
14
+ "displayName": "JSON Comments & Trailing Commas",
9
15
  "description": "Strips comments from JSON request bodies.",
10
16
  "images": {
11
17
  "icon": "icon.svg",
@@ -18,7 +24,10 @@
18
24
  "unlisted": false
19
25
  },
20
26
  "scripts": {
21
- "test": "node --test test.js"
27
+ "test": "node --test test.js",
28
+ "prepublishOnly": "npm test",
29
+ "format": "npx prettier@3 --write .",
30
+ "bench": "node --expose-gc bench/bench.js"
22
31
  },
23
32
  "repository": {
24
33
  "type": "git",
@@ -38,6 +47,7 @@
38
47
  },
39
48
  "homepage": "https://github.com/g-ongenae/insomnia-plugin-jsonc#readme",
40
49
  "devDependencies": {
41
- "fast-check": "^4.10.2"
50
+ "fast-check": "^4.10.2",
51
+ "strip-json-comments": "^5.0.3"
42
52
  }
43
53
  }
package/strip.js CHANGED
@@ -1,40 +1,150 @@
1
+ const QUOTE = 34; // "
2
+ const SLASH = 47; // /
3
+ const STAR = 42; // *
4
+ const BACKSLASH = 92; // \
5
+ const COMMA = 44; // ,
6
+ const CLOSING_BRACKET = 93; // ]
7
+ const CLOSING_BRACE = 125; // }
8
+
9
+ /**
10
+ * Check if a character code is JSON whitespace
11
+ * @param code the character code
12
+ * @returns whether it is a space, a tab, a line feed or a carriage return
13
+ */
14
+ const isWhitespace = (code) =>
15
+ code === 32 || code === 10 || code === 13 || code === 9;
16
+
1
17
  /**
2
- * Strip comments from a JSON string
18
+ * Check if a character code ends a line comment
3
19
  *
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.
20
+ * Same line terminators as JavaScript, which `.` does not match in the previous regex.
21
+ *
22
+ * @param code the character code
23
+ * @returns whether it is `\n`, `\r`, `\u2028` or `\u2029`
24
+ */
25
+ const isLineTerminator = (code) =>
26
+ code === 10 || code === 13 || code === 0x2028 || code === 0x2029;
27
+
28
+ /**
29
+ * Find the end of the line comment opening at `start`
7
30
  *
8
- * Extracted to not parse and stringify again the JSON string
31
+ * The line terminator is kept, so that the line structure is preserved.
9
32
  *
10
- * @param data the JSON string with comments
11
- * @returns the JSON string without comments
33
+ * @param data the JSONC string
34
+ * @param start the index of the comment's first slash
35
+ * @returns the index of the line terminator, or the length of the string
12
36
  */
13
- const stripJSONComments = (data) =>
14
- data.replace(/"(?:\\.|[^"\\])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) =>
15
- g ? "" : m
16
- );
37
+ const skipLineComment = (data, start) => {
38
+ let i = start + 2;
39
+ while (i < data.length && !isLineTerminator(data.charCodeAt(i))) i++;
40
+ return i;
41
+ };
17
42
 
18
43
  /**
19
- * Strip trailing commas before a closing `]` or `}` from a JSON string
44
+ * Find the end of the string opening at `start`
20
45
  *
21
- * Must run after `stripJSONComments`, so that a comment between the comma
22
- * and the closing bracket does not hide it.
46
+ * A quote preceded by an odd number of backslashes is escaped, so `"C:\\"` ends on its last quote.
23
47
  *
24
- * @param data the JSON string without comments
25
- * @returns the JSON string without trailing commas
48
+ * @param data the JSONC string
49
+ * @param start the index of the opening quote
50
+ * @returns the index after the closing quote, or -1 if the string is not terminated
26
51
  */
27
- const stripTrailingCommas = (data) =>
28
- data.replace(/"(?:\\.|[^"\\])*"|,(\s*[\]}])/g, (m, g) =>
29
- g === undefined ? m : g
30
- );
52
+ const skipString = (data, start) => {
53
+ let i = start;
54
+ for (;;) {
55
+ const quote = data.indexOf('"', i + 1);
56
+ if (quote === -1) return -1;
57
+ let backslash = quote - 1;
58
+ while (data.charCodeAt(backslash) === BACKSLASH) backslash--;
59
+ if ((quote - 1 - backslash) % 2 === 0) return quote + 1;
60
+ i = quote;
61
+ }
62
+ };
31
63
 
32
64
  /**
33
65
  * Strip comments and trailing commas from a JSONC string
34
66
  *
67
+ * Scans the string once, jumping over strings and comments with `indexOf`, and copies
68
+ * the kept text by slices. The string is returned as is when nothing is removed.
69
+ *
70
+ * An unterminated block comment is kept, so that invalid JSON is sent as is.
71
+ *
35
72
  * @param data the JSONC string
36
73
  * @returns the JSON string
37
74
  */
38
- const stripJSONC = (data) => stripTrailingCommas(stripJSONComments(data));
75
+ const stripJSONC = (data) => {
76
+ const length = data.length;
77
+ /** Kept slices, only allocated once something is removed */
78
+ let parts = null;
79
+ /** First index not copied to `parts` yet */
80
+ let start = 0;
81
+ /** Index of a comma only followed by whitespace or comments so far */
82
+ let comma = -1;
83
+ /** Index in `parts` of that comma, once copied */
84
+ let commaPart = -1;
85
+ /** Whether a block comment can still be closed, to not search the rest again for each opening */
86
+ let hasBlockEnd = true;
87
+
88
+ const flush = (end) => {
89
+ parts ??= [];
90
+ if (end > start) parts.push(data.slice(start, end));
91
+ };
92
+
93
+ // Copy a pending comma in its own part, to be able to drop it on a closing bracket
94
+ const detachComma = () => {
95
+ if (comma === -1 || commaPart !== -1) return;
96
+ flush(comma);
97
+ commaPart = parts.push(",") - 1;
98
+ start = comma + 1;
99
+ };
100
+
101
+ for (let i = 0; i < length;) {
102
+ const code = data.charCodeAt(i);
103
+ if (code === QUOTE) {
104
+ const end = skipString(data, i);
105
+ if (end === -1) break;
106
+ comma = -1;
107
+ i = end;
108
+ } else if (code === SLASH && data.charCodeAt(i + 1) === SLASH) {
109
+ detachComma();
110
+ flush(i);
111
+ i = start = skipLineComment(data, i);
112
+ } else if (
113
+ code === SLASH &&
114
+ data.charCodeAt(i + 1) === STAR &&
115
+ hasBlockEnd
116
+ ) {
117
+ const end = data.indexOf("*/", i + 2);
118
+ if (end === -1) {
119
+ // No later block comment can be terminated either
120
+ hasBlockEnd = false;
121
+ continue;
122
+ }
123
+ detachComma();
124
+ flush(i);
125
+ i = start = end + 2;
126
+ } else if (code === COMMA) {
127
+ comma = i;
128
+ commaPart = -1;
129
+ i++;
130
+ } else if (code === CLOSING_BRACKET || code === CLOSING_BRACE) {
131
+ if (comma !== -1 && commaPart !== -1) {
132
+ parts[commaPart] = "";
133
+ } else if (comma !== -1) {
134
+ flush(comma);
135
+ start = comma + 1;
136
+ }
137
+ comma = -1;
138
+ i++;
139
+ } else {
140
+ if (!isWhitespace(code)) comma = -1;
141
+ i++;
142
+ }
143
+ }
144
+
145
+ if (parts === null) return data;
146
+ flush(length);
147
+ return parts.join("");
148
+ };
39
149
 
40
- module.exports = { stripJSONComments, stripTrailingCommas, stripJSONC };
150
+ module.exports = { stripJSONC };
package/test.js DELETED
@@ -1,92 +0,0 @@
1
- const { test } = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const fc = require('fast-check');
4
- const { requestHooks } = require('./main');
5
- const { stripJSONC } = require('./strip');
6
-
7
- /**
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
11
- */
12
- const runHook = (givenBody) => {
13
- const calls = [];
14
- requestHooks[0]({
15
- request: {
16
- getBody: () => givenBody,
17
- setBody: (body) => calls.push(body),
18
- },
19
- });
20
- return calls;
21
- };
22
-
23
- /**
24
- * List of test cases: [name, given body, expected body]
25
- */
26
- const cases = [
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 }' }],
42
- ];
43
-
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
- });
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
- });