diff --git a/docs/cli/shortcuts.md b/docs/cli/shortcuts.md
index d0a79d8a..c5ef6844 100644
--- a/docs/cli/shortcuts.md
+++ b/docs/cli/shortcuts.md
@@ -1,6 +1,6 @@
# Command Shortcuts
-Package managers that execute scripts from a `package.json` or `deno.json` file can be shortened when in concurrently.
+Package managers that execute scripts from a `package.json` or `deno.(json|jsonc)` file can be shortened when in concurrently.
The following are supported:
| Syntax | Expands to |
diff --git a/src/command-parser/expand-wildcard.spec.ts b/src/command-parser/expand-wildcard.spec.ts
index 93cd58dd..abfe7972 100644
--- a/src/command-parser/expand-wildcard.spec.ts
+++ b/src/command-parser/expand-wildcard.spec.ts
@@ -1,4 +1,4 @@
-import fs from 'fs';
+import fs, { PathOrFileDescriptor } from 'fs';
import { CommandInfo } from '../command';
import { ExpandWildcard } from './expand-wildcard';
@@ -23,12 +23,53 @@ afterEach(() => {
});
describe('ExpandWildcard#readDeno', () => {
- it('can read deno', () => {
+ it('can read deno.json', () => {
const expectedDeno = {
name: 'deno',
version: '1.14.0',
};
- jest.spyOn(fs, 'readFileSync').mockImplementation((path) => {
+ jest.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {
+ return path === 'deno.json';
+ });
+ jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
+ if (path === 'deno.json') {
+ return JSON.stringify(expectedDeno);
+ }
+ return '';
+ });
+
+ const actualReadDeno = ExpandWildcard.readDeno();
+ expect(actualReadDeno).toEqual(expectedDeno);
+ });
+
+ it('can read deno.jsonc', () => {
+ const expectedDeno = {
+ name: 'deno',
+ version: '1.14.0',
+ };
+ jest.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {
+ return path === 'deno.jsonc';
+ });
+ jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
+ if (path === 'deno.jsonc') {
+ return '/* comment */\n' + JSON.stringify(expectedDeno);
+ }
+ return '';
+ });
+
+ const actualReadDeno = ExpandWildcard.readDeno();
+ expect(actualReadDeno).toEqual(expectedDeno);
+ });
+
+ it('prefers deno.json over deno.jsonc', () => {
+ const expectedDeno = {
+ name: 'deno',
+ version: '1.14.0',
+ };
+ jest.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {
+ return path === 'deno.json' || path === 'deno.jsonc';
+ });
+ jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
if (path === 'deno.json') {
return JSON.stringify(expectedDeno);
}
@@ -40,6 +81,7 @@ describe('ExpandWildcard#readDeno', () => {
});
it('can handle errors reading deno', () => {
+ jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
throw new Error('Error reading deno');
});
@@ -55,7 +97,7 @@ describe('ExpandWildcard#readPackage', () => {
name: 'concurrently',
version: '6.4.0',
};
- jest.spyOn(fs, 'readFileSync').mockImplementation((path) => {
+ jest.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {
if (path === 'package.json') {
return JSON.stringify(expectedPackage);
}
@@ -105,7 +147,7 @@ it('expands to nothing if no scripts exist in package.json', () => {
expect(parser.parse(createCommandInfo('npm run foo-*-baz qux'))).toEqual([]);
});
-it('expands to nothing if no tasks exist in deno.json and no scripts exist in package.json', () => {
+it('expands to nothing if no tasks exist in Deno config and no scripts exist in NodeJS config', () => {
readDeno.mockReturnValue({});
readPackage.mockReturnValue({});
@@ -192,7 +234,7 @@ describe.each(['npm run', 'yarn run', 'pnpm run', 'bun run', 'node --run'])(
expect(readPackage).toHaveBeenCalledTimes(1);
});
- it("doesn't read deno.json", () => {
+ it("doesn't read Deno config", () => {
readPackage.mockReturnValue({});
parser.parse(createCommandInfo(`${command} foo-*-baz qux`));
diff --git a/src/command-parser/expand-wildcard.ts b/src/command-parser/expand-wildcard.ts
index 810750f3..f82b8bb8 100644
--- a/src/command-parser/expand-wildcard.ts
+++ b/src/command-parser/expand-wildcard.ts
@@ -2,6 +2,7 @@ import fs from 'fs';
import _ from 'lodash';
import { CommandInfo } from '../command';
+import JSONC from '../jsonc';
import { CommandParser } from './command-parser';
// Matches a negative filter surrounded by '(!' and ')'.
@@ -9,14 +10,21 @@ const OMISSION = /\(!([^)]+)\)/;
/**
* Finds wildcards in 'npm/yarn/pnpm/bun run', 'node --run' and 'deno task'
- * commands and replaces them with all matching scripts in the `package.json`
- * and `deno.json` files of the current directory.
+ * commands and replaces them with all matching scripts in the NodeJS and Deno
+ * configuration files of the current directory.
*/
export class ExpandWildcard implements CommandParser {
static readDeno() {
try {
- const json = fs.readFileSync('deno.json', { encoding: 'utf-8' });
- return JSON.parse(json);
+ let json: string = '{}';
+
+ if (fs.existsSync('deno.json')) {
+ json = fs.readFileSync('deno.json', { encoding: 'utf-8' });
+ } else if (fs.existsSync('deno.jsonc')) {
+ json = fs.readFileSync('deno.jsonc', { encoding: 'utf-8' });
+ }
+
+ return JSONC.parse(json);
} catch (e) {
return {};
}
diff --git a/src/jsonc.spec.ts b/src/jsonc.spec.ts
new file mode 100644
index 00000000..4e506037
--- /dev/null
+++ b/src/jsonc.spec.ts
@@ -0,0 +1,84 @@
+/*
+ORIGINAL https://www.npmjs.com/package/tiny-jsonc
+BY Fabio Spampinato
+MIT license
+
+Copied due to the dependency not being compatible with CommonJS
+*/
+
+import JSONC from './jsonc';
+
+const Fixtures = {
+ errors: {
+ comment: '// asd',
+ empty: '',
+ prefix: 'invalid 123',
+ suffix: '123 invalid',
+ multiLineString: `
+ {
+ "foo": "/*
+ */"
+ }
+ `,
+ },
+ parse: {
+ input: `
+ // Example // Yes
+ /* EXAMPLE */ /* YES */
+ {
+ "one": {},
+ "two" :{},
+ "three": {
+ "one": null,
+ "two" :true,
+ "three": false,
+ "four": "asd\\n\\u0022\\"",
+ "five": -123.123e10,
+ "six": [ 123, true, [],],
+ },
+ }
+ // Example // Yes
+ /* EXAMPLE */ /* YES */
+ `,
+ output: {
+ one: {},
+ two: {},
+ three: {
+ one: null,
+ two: true,
+ three: false,
+ four: 'asd\n\u0022"',
+ five: -123.123e10,
+ six: [123, true, []],
+ },
+ },
+ },
+};
+
+describe('Tiny JSONC', () => {
+ it('supports strings with comments and trailing commas', () => {
+ const { input, output } = Fixtures.parse;
+
+ expect(JSONC.parse(input)).toEqual(output);
+ });
+
+ it('throws on invalid input', () => {
+ const { prefix, suffix } = Fixtures.errors;
+
+ expect(() => JSONC.parse(prefix)).toThrow(SyntaxError);
+ expect(() => JSONC.parse(suffix)).toThrow(SyntaxError);
+ });
+
+ it('throws on insufficient input', () => {
+ const { comment, empty } = Fixtures.errors;
+
+ expect(() => JSONC.parse(comment)).toThrow(SyntaxError);
+ expect(() => JSONC.parse(empty)).toThrow(SyntaxError);
+ });
+
+ it('throws on multi-line strings', () => {
+ const { multiLineString } = Fixtures.errors;
+
+ expect(() => JSONC.parse(multiLineString)).toThrow(SyntaxError);
+ });
+});
diff --git a/src/jsonc.ts b/src/jsonc.ts
new file mode 100644
index 00000000..7cef4b59
--- /dev/null
+++ b/src/jsonc.ts
@@ -0,0 +1,32 @@
+/*
+ORIGINAL https://www.npmjs.com/package/tiny-jsonc
+BY Fabio Spampinato
+MIT license
+
+Copied due to the dependency not being compatible with CommonJS
+*/
+
+/* HELPERS */
+const stringOrCommentRe = /("(?:\\?[^])*?")|(\/\/.*)|(\/\*[^]*?\*\/)/g;
+const stringOrTrailingCommaRe = /("(?:\\?[^])*?")|(,\s*)(?=]|})/g;
+
+/* MAIN */
+const JSONC = {
+ parse: (text: string) => {
+ text = String(text); // To be extra safe
+
+ try {
+ // Fast path for valid JSON
+ return JSON.parse(text);
+ } catch {
+ // Slow path for JSONC and invalid inputs
+ return JSON.parse(
+ text.replace(stringOrCommentRe, '$1').replace(stringOrTrailingCommaRe, '$1'),
+ );
+ }
+ },
+ stringify: JSON.stringify,
+};
+
+/* EXPORT */
+export default JSONC;