Skip to content

Commit e513f1e

Browse files
committed
fix: support Deno's JSON with comments configuration
Had to manually copy the dependency because it wouldn't work in a CommonJS project. Argument in favour of #482.
1 parent e3a76f7 commit e513f1e

File tree

5 files changed

+177
-11
lines changed

5 files changed

+177
-11
lines changed

docs/cli/shortcuts.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Command Shortcuts
22

3-
Package managers that execute scripts from a `package.json` or `deno.json` file can be shortened when in concurrently.<br/>
3+
Package managers that execute scripts from a `package.json` or `deno.(json|jsonc)` file can be shortened when in concurrently.<br/>
44
The following are supported:
55

66
| Syntax | Expands to |

src/command-parser/expand-wildcard.spec.ts

+48-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import fs from 'fs';
1+
import fs, { PathOrFileDescriptor } from 'fs';
22

33
import { CommandInfo } from '../command';
44
import { ExpandWildcard } from './expand-wildcard';
@@ -23,12 +23,53 @@ afterEach(() => {
2323
});
2424

2525
describe('ExpandWildcard#readDeno', () => {
26-
it('can read deno', () => {
26+
it('can read deno.json', () => {
2727
const expectedDeno = {
2828
name: 'deno',
2929
version: '1.14.0',
3030
};
31-
jest.spyOn(fs, 'readFileSync').mockImplementation((path) => {
31+
jest.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {
32+
return path === 'deno.json';
33+
});
34+
jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
35+
if (path === 'deno.json') {
36+
return JSON.stringify(expectedDeno);
37+
}
38+
return '';
39+
});
40+
41+
const actualReadDeno = ExpandWildcard.readDeno();
42+
expect(actualReadDeno).toEqual(expectedDeno);
43+
});
44+
45+
it('can read deno.jsonc', () => {
46+
const expectedDeno = {
47+
name: 'deno',
48+
version: '1.14.0',
49+
};
50+
jest.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {
51+
return path === 'deno.jsonc';
52+
});
53+
jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
54+
if (path === 'deno.jsonc') {
55+
return '/* comment */\n' + JSON.stringify(expectedDeno);
56+
}
57+
return '';
58+
});
59+
60+
const actualReadDeno = ExpandWildcard.readDeno();
61+
expect(actualReadDeno).toEqual(expectedDeno);
62+
});
63+
64+
it('prefers deno.json over deno.jsonc', () => {
65+
const expectedDeno = {
66+
name: 'deno',
67+
version: '1.14.0',
68+
};
69+
jest.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {
70+
return path === 'deno.json' || path === 'deno.jsonc';
71+
});
72+
jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
3273
if (path === 'deno.json') {
3374
return JSON.stringify(expectedDeno);
3475
}
@@ -40,6 +81,7 @@ describe('ExpandWildcard#readDeno', () => {
4081
});
4182

4283
it('can handle errors reading deno', () => {
84+
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
4385
jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
4486
throw new Error('Error reading deno');
4587
});
@@ -55,7 +97,7 @@ describe('ExpandWildcard#readPackage', () => {
5597
name: 'concurrently',
5698
version: '6.4.0',
5799
};
58-
jest.spyOn(fs, 'readFileSync').mockImplementation((path) => {
100+
jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
59101
if (path === 'package.json') {
60102
return JSON.stringify(expectedPackage);
61103
}
@@ -105,7 +147,7 @@ it('expands to nothing if no scripts exist in package.json', () => {
105147
expect(parser.parse(createCommandInfo('npm run foo-*-baz qux'))).toEqual([]);
106148
});
107149

108-
it('expands to nothing if no tasks exist in deno.json and no scripts exist in package.json', () => {
150+
it('expands to nothing if no tasks exist in Deno config and no scripts exist in NodeJS config', () => {
109151
readDeno.mockReturnValue({});
110152
readPackage.mockReturnValue({});
111153

@@ -192,7 +234,7 @@ describe.each(['npm run', 'yarn run', 'pnpm run', 'bun run', 'node --run'])(
192234
expect(readPackage).toHaveBeenCalledTimes(1);
193235
});
194236

195-
it("doesn't read deno.json", () => {
237+
it("doesn't read Deno config", () => {
196238
readPackage.mockReturnValue({});
197239

198240
parser.parse(createCommandInfo(`${command} foo-*-baz qux`));

src/command-parser/expand-wildcard.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@ import fs from 'fs';
22
import _ from 'lodash';
33

44
import { CommandInfo } from '../command';
5+
import JSONC from '../jsonc';
56
import { CommandParser } from './command-parser';
67

78
// Matches a negative filter surrounded by '(!' and ')'.
89
const OMISSION = /\(!([^)]+)\)/;
910

1011
/**
1112
* Finds wildcards in 'npm/yarn/pnpm/bun run', 'node --run' and 'deno task'
12-
* commands and replaces them with all matching scripts in the `package.json`
13-
* and `deno.json` files of the current directory.
13+
* commands and replaces them with all matching scripts in the NodeJS and Deno
14+
* configuration files of the current directory.
1415
*/
1516
export class ExpandWildcard implements CommandParser {
1617
static readDeno() {
1718
try {
18-
const json = fs.readFileSync('deno.json', { encoding: 'utf-8' });
19-
return JSON.parse(json);
19+
let json: string = '{}';
20+
21+
if (fs.existsSync('deno.json')) {
22+
json = fs.readFileSync('deno.json', { encoding: 'utf-8' });
23+
} else if (fs.existsSync('deno.jsonc')) {
24+
json = fs.readFileSync('deno.jsonc', { encoding: 'utf-8' });
25+
}
26+
27+
return JSONC.parse(json);
2028
} catch (e) {
2129
return {};
2230
}

src/jsonc.spec.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
ORIGINAL https://www.npmjs.com/package/tiny-jsonc
3+
BY Fabio Spampinato
4+
MIT license
5+
6+
Copied due to the dependency not being compatible with CommonJS
7+
*/
8+
9+
import JSONC from './jsonc';
10+
11+
const Fixtures = {
12+
errors: {
13+
comment: '// asd',
14+
empty: '',
15+
prefix: 'invalid 123',
16+
suffix: '123 invalid',
17+
multiLineString: `
18+
{
19+
"foo": "/*
20+
*/"
21+
}
22+
`,
23+
},
24+
parse: {
25+
input: `
26+
// Example // Yes
27+
/* EXAMPLE */ /* YES */
28+
{
29+
"one": {},
30+
"two" :{},
31+
"three": {
32+
"one": null,
33+
"two" :true,
34+
"three": false,
35+
"four": "asd\\n\\u0022\\"",
36+
"five": -123.123e10,
37+
"six": [ 123, true, [],],
38+
},
39+
}
40+
// Example // Yes
41+
/* EXAMPLE */ /* YES */
42+
`,
43+
output: {
44+
one: {},
45+
two: {},
46+
three: {
47+
one: null,
48+
two: true,
49+
three: false,
50+
four: 'asd\n\u0022"',
51+
five: -123.123e10,
52+
six: [123, true, []],
53+
},
54+
},
55+
},
56+
};
57+
58+
describe('Tiny JSONC', () => {
59+
it('supports strings with comments and trailing commas', () => {
60+
const { input, output } = Fixtures.parse;
61+
62+
expect(JSONC.parse(input)).toEqual(output);
63+
});
64+
65+
it('throws on invalid input', () => {
66+
const { prefix, suffix } = Fixtures.errors;
67+
68+
expect(() => JSONC.parse(prefix)).toThrow(SyntaxError);
69+
expect(() => JSONC.parse(suffix)).toThrow(SyntaxError);
70+
});
71+
72+
it('throws on insufficient input', () => {
73+
const { comment, empty } = Fixtures.errors;
74+
75+
expect(() => JSONC.parse(comment)).toThrow(SyntaxError);
76+
expect(() => JSONC.parse(empty)).toThrow(SyntaxError);
77+
});
78+
79+
it('throws on multi-line strings', () => {
80+
const { multiLineString } = Fixtures.errors;
81+
82+
expect(() => JSONC.parse(multiLineString)).toThrow(SyntaxError);
83+
});
84+
});

src/jsonc.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
ORIGINAL https://www.npmjs.com/package/tiny-jsonc
3+
BY Fabio Spampinato
4+
MIT license
5+
6+
Copied due to the dependency not being compatible with CommonJS
7+
*/
8+
9+
/* HELPERS */
10+
const stringOrCommentRe = /("(?:\\?[^])*?")|(\/\/.*)|(\/\*[^]*?\*\/)/g;
11+
const stringOrTrailingCommaRe = /("(?:\\?[^])*?")|(,\s*)(?=]|})/g;
12+
13+
/* MAIN */
14+
const JSONC = {
15+
parse: (text: string) => {
16+
text = String(text); // To be extra safe
17+
18+
try {
19+
// Fast path for valid JSON
20+
return JSON.parse(text);
21+
} catch {
22+
// Slow path for JSONC and invalid inputs
23+
return JSON.parse(
24+
text.replace(stringOrCommentRe, '$1').replace(stringOrTrailingCommaRe, '$1'),
25+
);
26+
}
27+
},
28+
stringify: JSON.stringify,
29+
};
30+
31+
/* EXPORT */
32+
export default JSONC;

0 commit comments

Comments
 (0)