From e513f1ea86b90ad4837e79f557b5d09639f8d369 Mon Sep 17 00:00:00 2001 From: Luka Leer Date: Thu, 26 Dec 2024 02:35:09 +0100 Subject: [PATCH] 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. --- docs/cli/shortcuts.md | 2 +- src/command-parser/expand-wildcard.spec.ts | 54 ++++++++++++-- src/command-parser/expand-wildcard.ts | 16 +++-- src/jsonc.spec.ts | 84 ++++++++++++++++++++++ src/jsonc.ts | 32 +++++++++ 5 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 src/jsonc.spec.ts create mode 100644 src/jsonc.ts 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;