Skip to content

Commit

Permalink
fix: support Deno's JSON with comments (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
mahtaran authored Dec 27, 2024
1 parent 8d3f976 commit 80fceda
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 11 deletions.
2 changes: 1 addition & 1 deletion docs/cli/shortcuts.md
Original file line number Diff line number Diff line change
@@ -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.<br/>
Package managers that execute scripts from a `package.json` or `deno.(json|jsonc)` file can be shortened when in concurrently.<br/>
The following are supported:

| Syntax | Expands to |
Expand Down
54 changes: 48 additions & 6 deletions src/command-parser/expand-wildcard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs';
import fs, { PathOrFileDescriptor } from 'fs';

import { CommandInfo } from '../command';
import { ExpandWildcard } from './expand-wildcard';
Expand All @@ -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);
}
Expand All @@ -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');
});
Expand All @@ -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);
}
Expand Down Expand Up @@ -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({});

Expand Down Expand Up @@ -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`));
Expand Down
16 changes: 12 additions & 4 deletions src/command-parser/expand-wildcard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@ 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 ')'.
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 {};
}
Expand Down
84 changes: 84 additions & 0 deletions src/jsonc.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
32 changes: 32 additions & 0 deletions src/jsonc.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 80fceda

Please sign in to comment.