├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docma.json ├── index.js ├── lib ├── helper.d.ts ├── helper.js ├── interfaces.d.ts ├── interfaces.js ├── jsonc.d.ts ├── jsonc.js ├── jsonc.safe.d.ts └── jsonc.safe.js ├── package-lock.json ├── package.json ├── src ├── helper.ts ├── interfaces.ts ├── jsonc.safe.ts └── jsonc.ts ├── test ├── helpers │ └── streamLog.ts └── jsonc.spec.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.xml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /backup/ 2 | /wiki/ 3 | /node_modules/ 4 | /backup/ 5 | /test/coverage/ 6 | /test/tmp/ 7 | src*.zip 8 | TODO.md 9 | ignore*.* 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '10' 5 | before_script: cd $TRAVIS_BUILD_DIR 6 | script: 7 | - npm run cover 8 | - npm run coveralls 9 | notifications: 10 | email: 11 | # recipients: 12 | # - one@example.com 13 | on_success: never # default: change 14 | on_failure: always # default: always 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019, Onur Yıldırım . All rights reserved. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonc 2 | 3 | [![build-status](https://img.shields.io/travis/onury/jsonc.svg?branch=master&style=flat-square)](https://travis-ci.org/onury/jsonc) 4 | [![coverage-status](https://img.shields.io/coveralls/github/onury/jsonc/master.svg?style=flat-square)](https://coveralls.io/github/onury/jsonc?branch=master) 5 | [![npm](http://img.shields.io/npm/v/jsonc.svg?style=flat-square)](https://www.npmjs.com/package/jsonc) 6 | [![release](https://img.shields.io/github/release/onury/jsonc.svg?style=flat-square)](https://github.com/onury/jsonc) 7 | [![license](http://img.shields.io/npm/l/jsonc.svg?style=flat-square)](https://github.com/onury/jsonc/blob/master/LICENSE) 8 | 9 | > © 2021, Onur Yıldırım ([@onury](https://github.com/onury)). MIT License. 10 | 11 | Everything you need in JSON land. 12 | 13 | `npm i jsonc` 14 | 15 | ## Features 16 | 17 | - Parse JSON with comments. 18 | - Stringify objects with circular references. 19 | - Safely parse / stringify without try/catch blocks. 20 | - Read and auto-parse JSON files gracefully, sync or async (with promises). 21 | - Auto-stringify and write JSON files gracefully, sync or async (with promises). 22 | - Strips UTF-8 BOM. 23 | - Log objects as JSON (without worrying about errors). 24 | - Uglify/beautify JSON strings. 25 | - More helpful JSON errors. 26 | - Friendly API. 27 | - TypeScript support. 28 | 29 | ## Usage 30 | 31 | See the concise [API reference][docs-api]. 32 | 33 | ```js 34 | const jsonc = require('jsonc'); 35 | // or 36 | import { jsonc } from 'jsonc'; 37 | ``` 38 | 39 | This is safe for JSON with comments: 40 | ```js 41 | jsonc.parse('// comment\n{"data": /* comment */ "value"}\n'); // » { data: 'value' } 42 | ``` 43 | 44 | And this is safe for circular references: 45 | ```js 46 | const obj = { x: 1 }; 47 | obj.y = obj; // circular 48 | jsonc.stringify(obj); // » { x: 1, y: '[Circular]' } 49 | ``` 50 | 51 | But this is seriously safe: 52 | ```js 53 | // safe version of every method 54 | const jsonc = require('jsonc').safe; 55 | // or 56 | import { safe as jsonc } from 'jsonc'; 57 | 58 | const [err, result] = jsonc.parse('[invalid JSON}'); 59 | if (err) { 60 | console.log(`Failed to parse JSON: ${err.message}`); 61 | } else { 62 | console.log(result); 63 | } 64 | ``` 65 | 66 | ## Documentation 67 | 68 | See the concise [API reference][docs-api]. 69 | 70 | ## Change Log 71 | 72 | - **v2.0.0** (2019-06-17) 73 | + Requires Node.js v8 or newer. 74 | + Updated dependencies. 75 | 76 | 77 | - **v1.1.0** (2018-11-22) 78 | + Fixed an issue where TypeScript compiler would complain about `'declare' modifier`. 79 | + Improved typings for safe methods. 80 | + Updated core dependencies. 81 | 82 | 83 | - **v1.0.0** (2018-10-18) 84 | + Initial release. 85 | 86 | 87 | ## License 88 | MIT. 89 | 90 | 91 | [docs-api]:https://onury.io/jsonc/api 92 | [strip-json-comments]:https://github.com/sindresorhus/strip-json-comments 93 | [json-stringify-safe]:https://github.com/isaacs/json-stringify-safe 94 | [parse-json]:https://github.com/sindresorhus/parse-json 95 | [fs-extra]:https://www.npmjs.com/package/fs-extra 96 | -------------------------------------------------------------------------------- /docma.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": 5, 3 | "jsdoc": { 4 | "encoding": "utf8", 5 | "recurse": false, 6 | "pedantic": false, 7 | "access": ["public"], 8 | "package": null, 9 | "module": true, 10 | "undocumented": false, 11 | "undescribed": false, 12 | "ignored": false, 13 | "hierarchy": true, 14 | "sort": "grouped", 15 | "relativePath": null, 16 | "filter": null, 17 | "allowUnknownTags": true, 18 | "plugins": [], 19 | "includePattern": ".+\\.js(doc|x)?$" 20 | }, 21 | "markdown": { 22 | "gfm": true, 23 | "tables": true, 24 | "breaks": false, 25 | "pedantic": false, 26 | "sanitize": false, 27 | "smartLists": true, 28 | "smartypants": false, 29 | "tasks": true, 30 | "emoji": true 31 | }, 32 | "app": { 33 | "title": "jsonc", 34 | "meta": null, 35 | "base": "/jsonc", 36 | "entrance": "content:guide", 37 | "routing": "path", 38 | "server": "github" 39 | }, 40 | "template": { 41 | "path": "zebra", 42 | "options": { 43 | "title": { 44 | "label": "jsonc", 45 | "href": "/jsonc/" 46 | }, 47 | "logo": null, // URL String or { dark: String, light: String } 48 | "sidebar": { 49 | "enabled": true, 50 | "outline": "tree", // "flat" | "tree" 51 | "collapsed": false, 52 | "toolbar": true, 53 | "itemsFolded": false, 54 | "itemsOverflow": "crop", // "crop" | "shrink" 55 | "badges": true, // true | false | 56 | "search": true, 57 | "animations": true 58 | }, 59 | "symbols": { 60 | "autoLink": true, // "internal" | "external" | true (both) 61 | "params": "list", // "list" | "table" 62 | "enums": "list", // "list" | "table" 63 | "props": "list", // "list" | "table" 64 | "meta": false 65 | }, 66 | "contentView": { 67 | "bookmarks": "h1,h2,h3" 68 | }, 69 | "navbar": { 70 | "enabled": true, 71 | "fixed": true, 72 | "dark": false, 73 | "animations": true, 74 | "menu": [ 75 | { 76 | "iconClass": "ico-mouse-pointer", 77 | "label": "Guide", 78 | "href": "./" 79 | }, 80 | { 81 | "iconClass": "ico-book", 82 | "label": "API Reference", 83 | "href": "./api" 84 | }, 85 | { 86 | "iconClass": "ico-md ico-download", 87 | "label": "Download", 88 | "items": [ 89 | { 90 | "label": "npm i jsonc", 91 | "href": "https://www.npmjs.com/package/jsonc", 92 | "target": "_blank" 93 | }, 94 | { 95 | "label": "yarn add jsonc", 96 | "href": "https://yarn.pm/jsonc", 97 | "target": "_blank" 98 | }, 99 | { 100 | "separator": true 101 | }, 102 | { 103 | "label": "Download as Archive", 104 | "href": "https://github.com/onury/jsonc/releases", 105 | "target": "_blank" 106 | } 107 | // , 108 | // { 109 | // "separator": true 110 | // }, 111 | // { 112 | // "label": "Change Log", 113 | // "href": "./changelog" 114 | // } 115 | ] 116 | }, 117 | { 118 | "iconClass": "ico-md ico-github", 119 | "label": "GitHub", 120 | "href": "https://github.com/onury/jsonc", 121 | "target": "_blank" 122 | } 123 | ] 124 | } 125 | } 126 | }, 127 | "src": [ 128 | "./lib/**/*.js", 129 | // "./CHANGELOG.md", 130 | { 131 | "guide": "./README.md" 132 | } 133 | ], 134 | "dest": "../onury.github.io/jsonc", 135 | "clean": true 136 | } 137 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var jsonc = require('./lib/jsonc').jsonc; 2 | module.exports = jsonc; 3 | // adding circular ref to allow easy importing in both ES5/6 and TS projects 4 | module.exports.jsonc = jsonc; 5 | module.exports.safe = jsonc.safe; -------------------------------------------------------------------------------- /lib/helper.d.ts: -------------------------------------------------------------------------------- 1 | import * as mkdirp from 'mkdirp'; 2 | import { IConfig, IStringifyOptions, Replacer } from './interfaces'; 3 | declare const helper: { 4 | isObject(o: any): boolean; 5 | isPrimitive(value: any): boolean; 6 | strLog(value: any, pretty: boolean): string; 7 | getLogger(config: IConfig, pretty: boolean): Function; 8 | getStringifyOptions(options: IStringifyOptions | Replacer, space: string | number): IStringifyOptions; 9 | fs: any; 10 | mkdirp: typeof mkdirp; 11 | promise: { 12 | readFile: any; 13 | writeFile: any; 14 | mkdirp: any; 15 | }; 16 | safeSync(fn: (...args: any[]) => T): (...args: any[]) => [U, T]; 17 | safeAsync(promise: Promise): Promise<[U, T]>; 18 | }; 19 | export { helper }; 20 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | // dep modules 15 | var fast_safe_stringify_1 = require("fast-safe-stringify"); 16 | var fs = require("graceful-fs"); 17 | var mkdirp = require("mkdirp"); 18 | // vars 19 | var oproto = Object.prototype; 20 | // simple promisification. this won't work for callbacks with more than 2 21 | // args. 22 | function promisify(fn) { 23 | return function () { 24 | var args = []; 25 | for (var _i = 0; _i < arguments.length; _i++) { 26 | args[_i] = arguments[_i]; 27 | } 28 | return new Promise(function (resolve, reject) { 29 | fn.apply(void 0, args.concat([function (err, result) { 30 | if (err) { 31 | reject(err); 32 | } 33 | else { 34 | resolve(result); 35 | } 36 | }])); 37 | }); 38 | }; 39 | } 40 | var defaultStringifyOpts = { 41 | replacer: null, 42 | space: 0, 43 | handleCircular: true 44 | }; 45 | var helper = { 46 | isObject: function (o) { 47 | return oproto.toString.call(o) === '[object Object]'; 48 | }, 49 | isPrimitive: function (value) { 50 | var t = typeof value; 51 | return value === null 52 | || value === undefined 53 | || (t !== 'function' && t !== 'object'); 54 | }, 55 | strLog: function (value, pretty) { 56 | if (helper.isPrimitive(value)) 57 | return value; 58 | var s = pretty ? ' ' : null; 59 | return fast_safe_stringify_1.default(value, null, s); 60 | }, 61 | getLogger: function (config, pretty) { 62 | return function () { 63 | var args = []; 64 | for (var _i = 0; _i < arguments.length; _i++) { 65 | args[_i] = arguments[_i]; 66 | } 67 | var stream = config.stream; 68 | var msg = args.map(function (arg) { 69 | if (arg instanceof Error) { 70 | stream = config.streamErr; 71 | return arg.stack 72 | /* istanbul ignore next */ 73 | || arg.message 74 | /* istanbul ignore next */ 75 | || String(arg); 76 | } 77 | return helper.strLog(arg, pretty); 78 | }).join(' '); 79 | stream.write(msg + '\n'); 80 | }; 81 | }, 82 | getStringifyOptions: function (options, space) { 83 | if (helper.isObject(options)) { 84 | return __assign({}, defaultStringifyOpts, options); // as IStringifyOptions 85 | } 86 | if (typeof options === 'function' || Array.isArray(options)) { 87 | return __assign({}, defaultStringifyOpts, { replacer: options, space: space }); 88 | } 89 | return __assign({}, defaultStringifyOpts, { space: space }); 90 | }, 91 | fs: fs, 92 | mkdirp: mkdirp, 93 | promise: { 94 | readFile: promisify(fs.readFile), 95 | writeFile: promisify(fs.writeFile), 96 | mkdirp: promisify(mkdirp) 97 | }, 98 | safeSync: function (fn) { 99 | return function () { 100 | var args = []; 101 | for (var _i = 0; _i < arguments.length; _i++) { 102 | args[_i] = arguments[_i]; 103 | } 104 | try { 105 | return [null, fn.apply(void 0, args)]; 106 | } 107 | catch (err) { 108 | return [err, undefined]; 109 | } 110 | }; 111 | }, 112 | safeAsync: function (promise) { 113 | return promise 114 | .then(function (data) { return [null, data]; }) 115 | .catch(function (err) { return [err, undefined]; }); 116 | } 117 | }; 118 | exports.helper = helper; 119 | -------------------------------------------------------------------------------- /lib/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare type Reviver = (key: string, value: any) => any; 3 | export declare type Replacer = (key: string, value: any) => any | string[] | number[]; 4 | export interface IParseOptions { 5 | reviver?: Reviver; 6 | stripComments?: boolean; 7 | } 8 | export interface IStringifyOptions { 9 | replacer?: Replacer; 10 | space?: string | number; 11 | handleCircular?: boolean; 12 | } 13 | export interface IReadOptions extends IParseOptions { 14 | } 15 | export interface IWriteOptions { 16 | mode?: number; 17 | autoPath?: boolean; 18 | replacer?: Replacer; 19 | space?: string | number; 20 | } 21 | export interface IConfig { 22 | stream?: NodeJS.WriteStream; 23 | streamErr?: NodeJS.WriteStream; 24 | } 25 | -------------------------------------------------------------------------------- /lib/interfaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /lib/jsonc.d.ts: -------------------------------------------------------------------------------- 1 | import { IConfig, IParseOptions, IReadOptions, IStringifyOptions, IWriteOptions, Replacer, Reviver } from './interfaces'; 2 | import { jsoncSafe } from './jsonc.safe'; 3 | /** 4 | * JSON utility class that can handle comments and circular references; and 5 | * other extra functionality. 6 | * @class 7 | * @author Onur Yıldırım 8 | * @license MIT 9 | * @see {@link https://github.com/onury/jsonc|GitHub Repo} 10 | * @see {@link https://github.com/onury/jsonc#related-modules|Related Modules} 11 | * 12 | * @example 13 | * const jsonc = require('jsonc'); 14 | * // or 15 | * import { jsonc } from 'jsonc'; 16 | * 17 | * const result = jsonc.parse('// comments\n{ "key": "value" }'); 18 | * console.log(result); // { key: "value" } 19 | */ 20 | declare class jsonc { 21 | /** @private */ 22 | private static _; 23 | /** 24 | * Configures `jsonc` object. 25 | * 26 | * @param {IConfig} cfg - Configurations. 27 | * @param {NodeJS.WriteStream} [stream] - Stream to write logs to. This is 28 | * used with `.log()` and `.logp()` methods. 29 | * @param {NodeJS.WriteStream} [streamErr] - Stream to write error logs to. 30 | * This is used with `.log()` and `.logp()` methods. 31 | * 32 | * @example 33 | * // Output logs to stdout but logs containing errors to a file. 34 | * jsonc.config({ 35 | * stream: process.stdout, 36 | * streamErr: fs.createWriteStream('path/to/log.txt') 37 | * }); 38 | * jsonc.log({ info: 'this is logged to console' }); 39 | * jsonc.log(new Error('this is logged to file')); 40 | */ 41 | static config(cfg: IConfig): void; 42 | /** 43 | * Stringifies and logs the given arguments to console. This will 44 | * automatically handle circular references; so it won't throw. 45 | * 46 | * If an `Error` instance is passed, it will log the `.stack` property on 47 | * the instance, without stringifying the object. 48 | * 49 | * @param {...any[]} [args] - Arguments to be logged. 50 | * @returns {void} 51 | */ 52 | static log(...args: any[]): void; 53 | /** 54 | * Pretty version of `log()` method. Stringifies and logs the given 55 | * arguments to console, with indents. This will automatically handle 56 | * circular references; so it won't throw. 57 | * 58 | * If an `Error` instance is passed, it will log the `.stack` property on 59 | * the instance, without stringifying the object. 60 | * 61 | * @param {...any[]} [args] - Arguments to be logged. 62 | * @returns {void} 63 | */ 64 | static logp(...args: any[]): void; 65 | /** 66 | * Parses the given JSON string into a JavaScript object. The input string 67 | * can include comments. 68 | * 69 | * @param {string} str - JSON string to be parsed. 70 | * @param {IParseOptions|Reviver} [options] - Either a parse options 71 | * object or a reviver function. 72 | * @param {Reviver} [options.reviver] - A function that can filter 73 | * and transform the results. It receives each of the keys and values, and 74 | * its return value is used instead of the original value. If it returns 75 | * what it received, then the structure is not modified. If it returns 76 | * `undefined` then the member is deleted. 77 | * @param {Boolean} [options.stripComments=true] - Whether to strip 78 | * comments from the JSON string. Note that it will throw if this is set to 79 | * `false` and the string includes comments. 80 | * 81 | * @returns {any} - Parsed value. 82 | * 83 | * @throws {JSONError} - If JSON string is not valid. Note that any 84 | * comments within JSON are removed by default; so this will not throw for 85 | * comments unless you explicitly set `stripComments` to `false`. 86 | * 87 | * @example 88 | * const parsed = jsonc.parse('// comments\n{"success":true}\n'); 89 | * console.log(parsed); // { success: true } 90 | */ 91 | static parse(str: string, options?: IParseOptions | Reviver): any; 92 | /** 93 | * Outputs a JSON string from the given JavaScript object. 94 | * 95 | * @param {*} value - JavaScript value to be stringified. 96 | * @param {IStringifyOptions|Replacer} [options] - Stringify options or a 97 | * replacer. 98 | * @param {Replacer} [options.replacer] - Determines how object values are 99 | * stringified for objects. It can be a function or an array of strings or 100 | * numbers. 101 | * @param {string|number} [options.space] - Specifies the indentation of 102 | * nested structures. If it is omitted, the text will be packed without 103 | * extra whitespace. If it is a number, it will specify the number of 104 | * spaces to indent at each level. If it is a string (such as `"\t"` or 105 | * `" "`), it contains the characters used to indent at each level. 106 | * @param {string|number} [space] - This takes effect if second argument is 107 | * the `replacer` or a falsy value. This is for supporting the signature of 108 | * native `JSON.stringify()` method. 109 | * @param {boolean} [options.handleCircular=true] - Whether to handle 110 | * circular references (if any) by replacing their values with the string 111 | * `"[Circular]"`. You can also use a replacer function to replace or 112 | * remove circular references instead. 113 | * 114 | * @returns {string} - JSON string. 115 | * 116 | * @throws {Error} - If there are any circular references within the 117 | * original input. In this case, use `jsonc.safe.stringify()` method 118 | * instead. 119 | * 120 | * @example 121 | * const obj = { key: 'value' }; 122 | * console.log(jsonc.stringify(obj)); // '{"key":"value"}' 123 | * 124 | * // pretty output with indents 125 | * let pretty = jsonc.stringify(obj, null, 2); 126 | * // equivalent to: 127 | * pretty = jsonc.stringify(obj, { reviver: null, space: 2 }); 128 | * if (!err) console.log(pretty); 129 | * // { 130 | * // "key": "value" 131 | * // } 132 | */ 133 | static stringify(value: any, optionsOrReplacer?: IStringifyOptions | Replacer, space?: string | number): string; 134 | /** 135 | * Specifies whether the given string has well-formed JSON structure. 136 | * 137 | * Note that, not all JSON-parsable strings are considered well-formed JSON 138 | * structures. JSON is built on two structures; a collection of name/value 139 | * pairs (object) or an ordered list of values (array). 140 | * 141 | * For example, `JSON.parse('true')` will parse successfully but 142 | * `jsonc.isJSON('true')` will return `false` since it has no object or 143 | * array structure. 144 | * 145 | * @param {string} str - String to be validated. 146 | * @param {boolean} [allowComments=false] - Whether comments should be 147 | * considered valid. 148 | * 149 | * @returns {boolean} 150 | * 151 | * @example 152 | * jsonc.isJSON('{"x":1}'); // true 153 | * jsonc.isJSON('true'); // false 154 | * jsonc.isJSON('[1, false, null]'); // true 155 | * jsonc.isJSON('string'); // false 156 | * jsonc.isJSON('null'); // false 157 | */ 158 | static isJSON(str: string, allowComments?: boolean): boolean; 159 | /** 160 | * Strips comments from the given JSON string. 161 | * 162 | * @param {string} str - JSON string. 163 | * @param {boolean} [whitespace=false] - Whether to replace comments with 164 | * whitespace instead of stripping them entirely. 165 | * 166 | * @returns {string} - Valid JSON string. 167 | * 168 | * @example 169 | * const str = jsonc.stripComments('// comments\n{"key":"value"}'); 170 | * console.log(str); // '\n{"key":"value"}' 171 | */ 172 | static stripComments(str: string, whitespace?: boolean): string; 173 | /** 174 | * Uglifies the given JSON string. 175 | * 176 | * @param {string} str - JSON string to be uglified. 177 | * @returns {string} - Uglified JSON string. 178 | * 179 | * @example 180 | * const pretty = ` 181 | * { 182 | * // comments... 183 | * "key": "value" 184 | * } 185 | * `; 186 | * const ugly = jsonc.uglify(pretty); 187 | * console.log(ugly); // '{"key":"value"}' 188 | */ 189 | static uglify(str: string): string; 190 | /** 191 | * Beautifies the given JSON string. Note that this will remove comments, 192 | * if any. 193 | * 194 | * @param {string} str - JSON string to be beautified. 195 | * @param {string|number} [space=2] Specifies the indentation of nested 196 | * structures. If it is omitted, the text will be packed without extra 197 | * whitespace. If it is a number, it will specify the number of spaces to 198 | * indent at each level. If it is a string (such as "\t" or " "), it 199 | * contains the characters used to indent at each level. 200 | * 201 | * @returns {string} - Beautified JSON string. 202 | * 203 | * @example 204 | * const ugly = '{"key":"value"}'; 205 | * const pretty = jsonc.beautify(ugly); 206 | * console.log(pretty); 207 | * // { 208 | * // "key": "value" 209 | * // } 210 | */ 211 | static beautify(str: string, space?: string | number): string; 212 | /** 213 | * Normalizes the given value by stringifying and parsing it back to a 214 | * Javascript object. 215 | * 216 | * @param {any} value 217 | * @param {Replacer} [replacer] - Determines how object values are 218 | * normalized for objects. It can be a function or an array of strings. 219 | * 220 | * @returns {any} - Normalized object. 221 | * 222 | * @example 223 | * const c = new SomeClass(); 224 | * console.log(c.constructor.name); // "SomeClass" 225 | * const normalized = jsonc.normalize(c); 226 | * console.log(normalized.constructor.name); // "Object" 227 | */ 228 | static normalize(value: any, replacer?: Replacer): any; 229 | /** 230 | * Asynchronously reads a JSON file, strips comments and UTF-8 BOM and 231 | * parses the JSON content. 232 | * 233 | * @param {string} filePath - Path to JSON file. 234 | * @param {Function|IReadOptions} [options] - Read options. 235 | * @param {Function} [options.reviver] - A function that can filter and 236 | * transform the results. It receives each of the keys and values, and its 237 | * return value is used instead of the original value. If it returns what 238 | * it received, then the structure is not modified. If it returns undefined 239 | * then the member is deleted. 240 | * @param {boolean} [options.stripComments=true] - Whether to strip 241 | * comments from the JSON string. Note that it will throw if this is set to 242 | * `false` and the string includes comments. 243 | * 244 | * @returns {Promise} - Promise of the parsed JSON content as a 245 | * JavaScript object. 246 | * 247 | * @example Using async/await (async () => {try {const 248 | * obj = await jsonc.read('path/to/file.json'); console.log(typeof obj); // 249 | * "object"} catch (err) {console.log('Failed to read JSON file'); 250 | * } 251 | * })(); 252 | * 253 | * @example Using promises 254 | * jsonc.read('path/to/file.json') .then(obj => {console.log(typeof obj); 255 | * // "object" 256 | * }) 257 | * .catch(err => { 258 | * console.log('Failed to read JSON file'); 259 | * }); 260 | */ 261 | static read(filePath: string, options?: IReadOptions): Promise; 262 | /** 263 | * Synchronously reads a JSON file, strips UTF-8 BOM and parses the JSON 264 | * content. 265 | * 266 | * @param {string} filePath - Path to JSON file. 267 | * @param {Function|IReadOptions} [options] - Read options. 268 | * @param {Function} [options.reviver] - A function that can filter and 269 | * transform the results. It receives each of the keys and values, and its 270 | * return value is used instead of the original value. If it returns what 271 | * it received, then the structure is not modified. If it returns undefined 272 | * then the member is deleted. 273 | * @param {boolean} [options.stripComments=true] - Whether to strip 274 | * comments from the JSON string. Note that it will throw if this is set to 275 | * `false` and the string includes comments. 276 | * 277 | * @returns {any} - Parsed JSON content as a JavaScript object. 278 | * 279 | * @example 280 | * const obj = jsonc.readSync('path/to/file.json'); 281 | * // use try/catch block to handle errors. or better, use the safe version. 282 | * console.log(typeof obj); // "object" 283 | */ 284 | static readSync(filePath: string, options?: IReadOptions): any; 285 | /** 286 | * Asynchronously writes a JSON file from the given JavaScript object. 287 | * 288 | * @param {string} filePath - Path to JSON file to be written. 289 | * @param {any} data - Data to be stringified into JSON. 290 | * @param {IWriteOptions} [options] - Write options. 291 | * @param {Replacer} [options.replacer] - Determines how object values are 292 | * stringified for objects. It can be a function or an array of strings. 293 | * @param {string|number} [options.space] - Specifies the indentation of 294 | * nested structures. If it is omitted, the text will be packed without 295 | * extra whitespace. If it is a number, it will specify the number of 296 | * spaces to indent at each level. If it is a string (such as "\t" or 297 | * " "), it contains the characters used to indent at each level. 298 | * @param {number} [options.mode=438] - FileSystem permission mode to be used when 299 | * writing the file. Default is `438` (`0666` in octal). 300 | * @param {boolean} [options.autoPath=true] - Specifies whether to create path 301 | * directories if they don't exist. This will throw if set to `false` and 302 | * path does not exist. 303 | * 304 | * @returns {Promise} - Always resolves with `true`, if no errors occur. 305 | * 306 | * @example Using async/await 307 | * (async () => { 308 | * try { 309 | * await jsonc.write('path/to/file.json', data); 310 | * console.log('Successfully wrote JSON file'); 311 | * } catch (err) { 312 | * console.log('Failed to write JSON file'); 313 | * } 314 | * })(); 315 | * 316 | * @example Using promises 317 | * jsonc.write('path/to/file.json', data) 318 | * .then(success => { 319 | * console.log('Successfully wrote JSON file'); 320 | * }) 321 | * .catch(err => { 322 | * console.log('Failed to write JSON file'); 323 | * }); 324 | */ 325 | static write(filePath: string, data: any, options?: IWriteOptions): Promise; 326 | /** 327 | * Synchronously writes a JSON file from the given JavaScript object. 328 | * 329 | * @param {string} filePath - Path to JSON file to be written. 330 | * @param {any} data - Data to be stringified into JSON. 331 | * @param {IWriteOptions} [options] - Write options. 332 | * @param {Replacer} [options.replacer] - Determines how object values are 333 | * stringified for objects. It can be a function or an array of strings. 334 | * @param {string|number} [options.space] - Specifies the indentation of 335 | * nested structures. If it is omitted, the text will be packed without 336 | * extra whitespace. If it is a number, it will specify the number of 337 | * spaces to indent at each level. If it is a string (such as "\t" or 338 | * " "), it contains the characters used to indent at each level. 339 | * @param {number} [options.mode=438] - FileSystem permission mode to be used when 340 | * writing the file. Default is `438` (`0666` in octal). 341 | * @param {boolean} [options.autoPath=true] - Specifies whether to create path 342 | * directories if they don't exist. This will throw if set to `false` and 343 | * path does not exist. 344 | * 345 | * @returns {boolean} - Always returns `true`, if no errors occur. 346 | * 347 | * @example 348 | * const success = jsonc.writeSync('path/to/file.json'); 349 | * // this will always return true. use try/catch block to handle errors. or better, use the safe version. 350 | * console.log('Successfully wrote JSON file'); 351 | */ 352 | static writeSync(filePath: string, data: any, options?: IWriteOptions): boolean; 353 | } 354 | declare namespace jsonc { 355 | const safe: typeof jsoncSafe; 356 | } 357 | export { jsonc }; 358 | -------------------------------------------------------------------------------- /lib/jsonc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* tslint:disable:class-name no-require-imports no-default-export max-line-length interface-name max-classes-per-file max-file-line-count */ 3 | var __assign = (this && this.__assign) || function () { 4 | __assign = Object.assign || function(t) { 5 | for (var s, i = 1, n = arguments.length; i < n; i++) { 6 | s = arguments[i]; 7 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 8 | t[p] = s[p]; 9 | } 10 | return t; 11 | }; 12 | return __assign.apply(this, arguments); 13 | }; 14 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 15 | return new (P || (P = Promise))(function (resolve, reject) { 16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 18 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 19 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 20 | }); 21 | }; 22 | var __generator = (this && this.__generator) || function (thisArg, body) { 23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 25 | function verb(n) { return function (v) { return step([n, v]); }; } 26 | function step(op) { 27 | if (f) throw new TypeError("Generator is already executing."); 28 | while (_) try { 29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 30 | if (y = 0, t) op = [op[0] & 2, t.value]; 31 | switch (op[0]) { 32 | case 0: case 1: t = op; break; 33 | case 4: _.label++; return { value: op[1], done: false }; 34 | case 5: _.label++; y = op[1]; op = [0]; continue; 35 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 36 | default: 37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 41 | if (t[2]) _.ops.pop(); 42 | _.trys.pop(); continue; 43 | } 44 | op = body.call(thisArg, _); 45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 47 | } 48 | }; 49 | Object.defineProperty(exports, "__esModule", { value: true }); 50 | // core modules 51 | var path = require("path"); 52 | // dep modules 53 | var fast_safe_stringify_1 = require("fast-safe-stringify"); 54 | var parseJson = require("parse-json"); 55 | var stripBOM = require("strip-bom"); 56 | var stripJsonComments = require("strip-json-comments"); 57 | // own modules 58 | var helper_1 = require("./helper"); 59 | var jsonc_safe_1 = require("./jsonc.safe"); 60 | // constants, variables 61 | var fs = helper_1.helper.fs, mkdirp = helper_1.helper.mkdirp, promise = helper_1.helper.promise; 62 | /** 63 | * JSON utility class that can handle comments and circular references; and 64 | * other extra functionality. 65 | * @class 66 | * @author Onur Yıldırım 67 | * @license MIT 68 | * @see {@link https://github.com/onury/jsonc|GitHub Repo} 69 | * @see {@link https://github.com/onury/jsonc#related-modules|Related Modules} 70 | * 71 | * @example 72 | * const jsonc = require('jsonc'); 73 | * // or 74 | * import { jsonc } from 'jsonc'; 75 | * 76 | * const result = jsonc.parse('// comments\n{ "key": "value" }'); 77 | * console.log(result); // { key: "value" } 78 | */ 79 | var jsonc = /** @class */ (function () { 80 | function jsonc() { 81 | } 82 | /** 83 | * Configures `jsonc` object. 84 | * 85 | * @param {IConfig} cfg - Configurations. 86 | * @param {NodeJS.WriteStream} [stream] - Stream to write logs to. This is 87 | * used with `.log()` and `.logp()` methods. 88 | * @param {NodeJS.WriteStream} [streamErr] - Stream to write error logs to. 89 | * This is used with `.log()` and `.logp()` methods. 90 | * 91 | * @example 92 | * // Output logs to stdout but logs containing errors to a file. 93 | * jsonc.config({ 94 | * stream: process.stdout, 95 | * streamErr: fs.createWriteStream('path/to/log.txt') 96 | * }); 97 | * jsonc.log({ info: 'this is logged to console' }); 98 | * jsonc.log(new Error('this is logged to file')); 99 | */ 100 | jsonc.config = function (cfg) { 101 | var conf = __assign({ stream: process.stdout, streamErr: process.stderr }, (cfg || {})); 102 | jsonc._ = { 103 | logger: helper_1.helper.getLogger(conf, false), 104 | prettyLogger: helper_1.helper.getLogger(conf, true) 105 | }; 106 | }; 107 | /** 108 | * Stringifies and logs the given arguments to console. This will 109 | * automatically handle circular references; so it won't throw. 110 | * 111 | * If an `Error` instance is passed, it will log the `.stack` property on 112 | * the instance, without stringifying the object. 113 | * 114 | * @param {...any[]} [args] - Arguments to be logged. 115 | * @returns {void} 116 | */ 117 | jsonc.log = function () { 118 | var _a; 119 | var args = []; 120 | for (var _i = 0; _i < arguments.length; _i++) { 121 | args[_i] = arguments[_i]; 122 | } 123 | (_a = jsonc._).logger.apply(_a, args); 124 | }; 125 | /** 126 | * Pretty version of `log()` method. Stringifies and logs the given 127 | * arguments to console, with indents. This will automatically handle 128 | * circular references; so it won't throw. 129 | * 130 | * If an `Error` instance is passed, it will log the `.stack` property on 131 | * the instance, without stringifying the object. 132 | * 133 | * @param {...any[]} [args] - Arguments to be logged. 134 | * @returns {void} 135 | */ 136 | jsonc.logp = function () { 137 | var _a; 138 | var args = []; 139 | for (var _i = 0; _i < arguments.length; _i++) { 140 | args[_i] = arguments[_i]; 141 | } 142 | (_a = jsonc._).prettyLogger.apply(_a, args); 143 | }; 144 | /** 145 | * Parses the given JSON string into a JavaScript object. The input string 146 | * can include comments. 147 | * 148 | * @param {string} str - JSON string to be parsed. 149 | * @param {IParseOptions|Reviver} [options] - Either a parse options 150 | * object or a reviver function. 151 | * @param {Reviver} [options.reviver] - A function that can filter 152 | * and transform the results. It receives each of the keys and values, and 153 | * its return value is used instead of the original value. If it returns 154 | * what it received, then the structure is not modified. If it returns 155 | * `undefined` then the member is deleted. 156 | * @param {Boolean} [options.stripComments=true] - Whether to strip 157 | * comments from the JSON string. Note that it will throw if this is set to 158 | * `false` and the string includes comments. 159 | * 160 | * @returns {any} - Parsed value. 161 | * 162 | * @throws {JSONError} - If JSON string is not valid. Note that any 163 | * comments within JSON are removed by default; so this will not throw for 164 | * comments unless you explicitly set `stripComments` to `false`. 165 | * 166 | * @example 167 | * const parsed = jsonc.parse('// comments\n{"success":true}\n'); 168 | * console.log(parsed); // { success: true } 169 | */ 170 | jsonc.parse = function (str, options) { 171 | var opts = typeof options === 'function' 172 | ? { reviver: options } 173 | : (options || {}); 174 | if (opts.stripComments !== false) 175 | str = stripJsonComments(str, { whitespace: false }); 176 | return parseJson(str, opts.reviver); 177 | }; 178 | /** 179 | * Outputs a JSON string from the given JavaScript object. 180 | * 181 | * @param {*} value - JavaScript value to be stringified. 182 | * @param {IStringifyOptions|Replacer} [options] - Stringify options or a 183 | * replacer. 184 | * @param {Replacer} [options.replacer] - Determines how object values are 185 | * stringified for objects. It can be a function or an array of strings or 186 | * numbers. 187 | * @param {string|number} [options.space] - Specifies the indentation of 188 | * nested structures. If it is omitted, the text will be packed without 189 | * extra whitespace. If it is a number, it will specify the number of 190 | * spaces to indent at each level. If it is a string (such as `"\t"` or 191 | * `" "`), it contains the characters used to indent at each level. 192 | * @param {string|number} [space] - This takes effect if second argument is 193 | * the `replacer` or a falsy value. This is for supporting the signature of 194 | * native `JSON.stringify()` method. 195 | * @param {boolean} [options.handleCircular=true] - Whether to handle 196 | * circular references (if any) by replacing their values with the string 197 | * `"[Circular]"`. You can also use a replacer function to replace or 198 | * remove circular references instead. 199 | * 200 | * @returns {string} - JSON string. 201 | * 202 | * @throws {Error} - If there are any circular references within the 203 | * original input. In this case, use `jsonc.safe.stringify()` method 204 | * instead. 205 | * 206 | * @example 207 | * const obj = { key: 'value' }; 208 | * console.log(jsonc.stringify(obj)); // '{"key":"value"}' 209 | * 210 | * // pretty output with indents 211 | * let pretty = jsonc.stringify(obj, null, 2); 212 | * // equivalent to: 213 | * pretty = jsonc.stringify(obj, { reviver: null, space: 2 }); 214 | * if (!err) console.log(pretty); 215 | * // { 216 | * // "key": "value" 217 | * // } 218 | */ 219 | jsonc.stringify = function (value, optionsOrReplacer, space) { 220 | var opts = helper_1.helper.getStringifyOptions(optionsOrReplacer, space); 221 | return opts.handleCircular 222 | ? fast_safe_stringify_1.default(value, opts.replacer, opts.space) 223 | : JSON.stringify(value, opts.replacer, opts.space); 224 | }; 225 | /** 226 | * Specifies whether the given string has well-formed JSON structure. 227 | * 228 | * Note that, not all JSON-parsable strings are considered well-formed JSON 229 | * structures. JSON is built on two structures; a collection of name/value 230 | * pairs (object) or an ordered list of values (array). 231 | * 232 | * For example, `JSON.parse('true')` will parse successfully but 233 | * `jsonc.isJSON('true')` will return `false` since it has no object or 234 | * array structure. 235 | * 236 | * @param {string} str - String to be validated. 237 | * @param {boolean} [allowComments=false] - Whether comments should be 238 | * considered valid. 239 | * 240 | * @returns {boolean} 241 | * 242 | * @example 243 | * jsonc.isJSON('{"x":1}'); // true 244 | * jsonc.isJSON('true'); // false 245 | * jsonc.isJSON('[1, false, null]'); // true 246 | * jsonc.isJSON('string'); // false 247 | * jsonc.isJSON('null'); // false 248 | */ 249 | jsonc.isJSON = function (str, allowComments) { 250 | if (allowComments === void 0) { allowComments = false; } 251 | if (typeof str !== 'string') 252 | return false; 253 | var _a = jsonc.safe.parse(str, { stripComments: allowComments }), err = _a[0], result = _a[1]; 254 | return !err && (helper_1.helper.isObject(result) || Array.isArray(result)); 255 | }; 256 | /** 257 | * Strips comments from the given JSON string. 258 | * 259 | * @param {string} str - JSON string. 260 | * @param {boolean} [whitespace=false] - Whether to replace comments with 261 | * whitespace instead of stripping them entirely. 262 | * 263 | * @returns {string} - Valid JSON string. 264 | * 265 | * @example 266 | * const str = jsonc.stripComments('// comments\n{"key":"value"}'); 267 | * console.log(str); // '\n{"key":"value"}' 268 | */ 269 | jsonc.stripComments = function (str, whitespace) { 270 | if (whitespace === void 0) { whitespace = false; } 271 | return stripJsonComments(str, { whitespace: whitespace }); 272 | }; 273 | /** 274 | * Uglifies the given JSON string. 275 | * 276 | * @param {string} str - JSON string to be uglified. 277 | * @returns {string} - Uglified JSON string. 278 | * 279 | * @example 280 | * const pretty = ` 281 | * { 282 | * // comments... 283 | * "key": "value" 284 | * } 285 | * `; 286 | * const ugly = jsonc.uglify(pretty); 287 | * console.log(ugly); // '{"key":"value"}' 288 | */ 289 | jsonc.uglify = function (str) { 290 | return jsonc.stringify(jsonc.parse(str, { stripComments: true })); 291 | }; 292 | /** 293 | * Beautifies the given JSON string. Note that this will remove comments, 294 | * if any. 295 | * 296 | * @param {string} str - JSON string to be beautified. 297 | * @param {string|number} [space=2] Specifies the indentation of nested 298 | * structures. If it is omitted, the text will be packed without extra 299 | * whitespace. If it is a number, it will specify the number of spaces to 300 | * indent at each level. If it is a string (such as "\t" or " "), it 301 | * contains the characters used to indent at each level. 302 | * 303 | * @returns {string} - Beautified JSON string. 304 | * 305 | * @example 306 | * const ugly = '{"key":"value"}'; 307 | * const pretty = jsonc.beautify(ugly); 308 | * console.log(pretty); 309 | * // { 310 | * // "key": "value" 311 | * // } 312 | */ 313 | jsonc.beautify = function (str, space) { 314 | if (space === void 0) { space = 2; } 315 | if (!space) 316 | space = 2; 317 | return jsonc.stringify(jsonc.parse(str), { space: space }); 318 | }; 319 | /** 320 | * Normalizes the given value by stringifying and parsing it back to a 321 | * Javascript object. 322 | * 323 | * @param {any} value 324 | * @param {Replacer} [replacer] - Determines how object values are 325 | * normalized for objects. It can be a function or an array of strings. 326 | * 327 | * @returns {any} - Normalized object. 328 | * 329 | * @example 330 | * const c = new SomeClass(); 331 | * console.log(c.constructor.name); // "SomeClass" 332 | * const normalized = jsonc.normalize(c); 333 | * console.log(normalized.constructor.name); // "Object" 334 | */ 335 | jsonc.normalize = function (value, replacer) { 336 | return jsonc.parse(jsonc.stringify(value, { replacer: replacer })); 337 | }; 338 | /** 339 | * Asynchronously reads a JSON file, strips comments and UTF-8 BOM and 340 | * parses the JSON content. 341 | * 342 | * @param {string} filePath - Path to JSON file. 343 | * @param {Function|IReadOptions} [options] - Read options. 344 | * @param {Function} [options.reviver] - A function that can filter and 345 | * transform the results. It receives each of the keys and values, and its 346 | * return value is used instead of the original value. If it returns what 347 | * it received, then the structure is not modified. If it returns undefined 348 | * then the member is deleted. 349 | * @param {boolean} [options.stripComments=true] - Whether to strip 350 | * comments from the JSON string. Note that it will throw if this is set to 351 | * `false` and the string includes comments. 352 | * 353 | * @returns {Promise} - Promise of the parsed JSON content as a 354 | * JavaScript object. 355 | * 356 | * @example Using async/await (async () => {try {const 357 | * obj = await jsonc.read('path/to/file.json'); console.log(typeof obj); // 358 | * "object"} catch (err) {console.log('Failed to read JSON file'); 359 | * } 360 | * })(); 361 | * 362 | * @example Using promises 363 | * jsonc.read('path/to/file.json') .then(obj => {console.log(typeof obj); 364 | * // "object" 365 | * }) 366 | * .catch(err => { 367 | * console.log('Failed to read JSON file'); 368 | * }); 369 | */ 370 | jsonc.read = function (filePath, options) { 371 | return __awaiter(this, void 0, void 0, function () { 372 | var opts, data; 373 | return __generator(this, function (_a) { 374 | switch (_a.label) { 375 | case 0: 376 | opts = __assign({ reviver: null, stripComments: true }, (options || {})); 377 | return [4 /*yield*/, promise.readFile(filePath, 'utf8')]; 378 | case 1: 379 | data = _a.sent(); 380 | if (opts.stripComments !== false) 381 | data = stripJsonComments(data); 382 | return [2 /*return*/, parseJson(stripBOM(data), opts.reviver, filePath)]; 383 | } 384 | }); 385 | }); 386 | }; 387 | /** 388 | * Synchronously reads a JSON file, strips UTF-8 BOM and parses the JSON 389 | * content. 390 | * 391 | * @param {string} filePath - Path to JSON file. 392 | * @param {Function|IReadOptions} [options] - Read options. 393 | * @param {Function} [options.reviver] - A function that can filter and 394 | * transform the results. It receives each of the keys and values, and its 395 | * return value is used instead of the original value. If it returns what 396 | * it received, then the structure is not modified. If it returns undefined 397 | * then the member is deleted. 398 | * @param {boolean} [options.stripComments=true] - Whether to strip 399 | * comments from the JSON string. Note that it will throw if this is set to 400 | * `false` and the string includes comments. 401 | * 402 | * @returns {any} - Parsed JSON content as a JavaScript object. 403 | * 404 | * @example 405 | * const obj = jsonc.readSync('path/to/file.json'); 406 | * // use try/catch block to handle errors. or better, use the safe version. 407 | * console.log(typeof obj); // "object" 408 | */ 409 | jsonc.readSync = function (filePath, options) { 410 | var opts = __assign({ reviver: null, stripComments: true }, (options || {})); 411 | var data = fs.readFileSync(filePath, 'utf8'); 412 | if (opts.stripComments !== false) 413 | data = stripJsonComments(data); 414 | return parseJson(stripBOM(data), opts.reviver, filePath); 415 | }; 416 | /** 417 | * Asynchronously writes a JSON file from the given JavaScript object. 418 | * 419 | * @param {string} filePath - Path to JSON file to be written. 420 | * @param {any} data - Data to be stringified into JSON. 421 | * @param {IWriteOptions} [options] - Write options. 422 | * @param {Replacer} [options.replacer] - Determines how object values are 423 | * stringified for objects. It can be a function or an array of strings. 424 | * @param {string|number} [options.space] - Specifies the indentation of 425 | * nested structures. If it is omitted, the text will be packed without 426 | * extra whitespace. If it is a number, it will specify the number of 427 | * spaces to indent at each level. If it is a string (such as "\t" or 428 | * " "), it contains the characters used to indent at each level. 429 | * @param {number} [options.mode=438] - FileSystem permission mode to be used when 430 | * writing the file. Default is `438` (`0666` in octal). 431 | * @param {boolean} [options.autoPath=true] - Specifies whether to create path 432 | * directories if they don't exist. This will throw if set to `false` and 433 | * path does not exist. 434 | * 435 | * @returns {Promise} - Always resolves with `true`, if no errors occur. 436 | * 437 | * @example Using async/await 438 | * (async () => { 439 | * try { 440 | * await jsonc.write('path/to/file.json', data); 441 | * console.log('Successfully wrote JSON file'); 442 | * } catch (err) { 443 | * console.log('Failed to write JSON file'); 444 | * } 445 | * })(); 446 | * 447 | * @example Using promises 448 | * jsonc.write('path/to/file.json', data) 449 | * .then(success => { 450 | * console.log('Successfully wrote JSON file'); 451 | * }) 452 | * .catch(err => { 453 | * console.log('Failed to write JSON file'); 454 | * }); 455 | */ 456 | jsonc.write = function (filePath, data, options) { 457 | return __awaiter(this, void 0, void 0, function () { 458 | var opts, content; 459 | return __generator(this, function (_a) { 460 | switch (_a.label) { 461 | case 0: 462 | opts = __assign({ replacer: null, space: 0, mode: 438, autoPath: true }, (options || {})); 463 | if (!opts.autoPath) return [3 /*break*/, 2]; 464 | return [4 /*yield*/, promise.mkdirp(path.dirname(filePath), { fs: fs })]; 465 | case 1: 466 | _a.sent(); 467 | _a.label = 2; 468 | case 2: 469 | content = JSON.stringify(data, opts.replacer, opts.space); 470 | return [4 /*yield*/, promise.writeFile(filePath, content + "\n", { 471 | mode: opts.mode, 472 | encoding: 'utf8' 473 | })]; 474 | case 3: 475 | _a.sent(); 476 | return [2 /*return*/, true]; 477 | } 478 | }); 479 | }); 480 | }; 481 | /** 482 | * Synchronously writes a JSON file from the given JavaScript object. 483 | * 484 | * @param {string} filePath - Path to JSON file to be written. 485 | * @param {any} data - Data to be stringified into JSON. 486 | * @param {IWriteOptions} [options] - Write options. 487 | * @param {Replacer} [options.replacer] - Determines how object values are 488 | * stringified for objects. It can be a function or an array of strings. 489 | * @param {string|number} [options.space] - Specifies the indentation of 490 | * nested structures. If it is omitted, the text will be packed without 491 | * extra whitespace. If it is a number, it will specify the number of 492 | * spaces to indent at each level. If it is a string (such as "\t" or 493 | * " "), it contains the characters used to indent at each level. 494 | * @param {number} [options.mode=438] - FileSystem permission mode to be used when 495 | * writing the file. Default is `438` (`0666` in octal). 496 | * @param {boolean} [options.autoPath=true] - Specifies whether to create path 497 | * directories if they don't exist. This will throw if set to `false` and 498 | * path does not exist. 499 | * 500 | * @returns {boolean} - Always returns `true`, if no errors occur. 501 | * 502 | * @example 503 | * const success = jsonc.writeSync('path/to/file.json'); 504 | * // this will always return true. use try/catch block to handle errors. or better, use the safe version. 505 | * console.log('Successfully wrote JSON file'); 506 | */ 507 | jsonc.writeSync = function (filePath, data, options) { 508 | var opts = __assign({ replacer: null, space: 0, mode: 438, autoPath: true }, (options || {})); 509 | if (opts.autoPath) 510 | mkdirp.sync(path.dirname(filePath), { fs: fs }); 511 | var content = JSON.stringify(data, opts.replacer, opts.space); 512 | fs.writeFileSync(filePath, content + "\n", { 513 | mode: opts.mode, 514 | encoding: 'utf8' 515 | }); 516 | return true; 517 | }; 518 | return jsonc; 519 | }()); 520 | exports.jsonc = jsonc; 521 | // default configuration 522 | jsonc.config(null); 523 | /* istanbul ignore next */ 524 | (function (jsonc) { 525 | jsonc.safe = jsonc_safe_1.jsoncSafe; 526 | })(jsonc || (jsonc = {})); 527 | exports.jsonc = jsonc; 528 | -------------------------------------------------------------------------------- /lib/jsonc.safe.d.ts: -------------------------------------------------------------------------------- 1 | import { IConfig, IParseOptions, IReadOptions, IStringifyOptions, IWriteOptions, Replacer, Reviver } from './interfaces'; 2 | /** 3 | * Class that provides safe versions of `jsonc` methods. Safe methods provide a 4 | * way to easily handle errors without throwing; so that you don't need to use 5 | * try/catch blocks. 6 | * 7 | * Each method (except a few such as `.isJSON`), will return an array with the 8 | * first item being the `Error` instance caught. If successful, second item 9 | * will be the result. 10 | * @name jsonc.safe 11 | * @class 12 | * 13 | * @example 14 | * const { safe } = require('jsonc'); 15 | * // or 16 | * import { safe as jsonc } from 'jsonc'; 17 | * 18 | * const [err, result] = jsonc.parse('[invalid JSON}'); 19 | * if (err) { 20 | * console.log(`Failed to parse JSON: ${err.message}`); 21 | * } else { 22 | * console.log(result); 23 | * } 24 | */ 25 | declare class jsoncSafe { 26 | /** 27 | * Configures `jsonc` object. 28 | * 29 | *
This method is added for convenience. Works the same as `jsonc.config()`.
30 | * 31 | * @name jsonc.safe.config 32 | * @function 33 | * 34 | * @param {IConfig} cfg - Configurations. 35 | * @param {NodeJS.WriteStream} [stream] - Stream to write logs to. This is 36 | * used with `.log()` and `.logp()` methods. 37 | * @param {NodeJS.WriteStream} [streamErr] - Stream to write error logs to. 38 | * This is used with `.log()` and `.logp()` methods. 39 | * 40 | * @example 41 | * import { safe as jsonc } from 'jsonc'; 42 | * // Output logs to stdout but logs containing errors to a file. 43 | * jsonc.config({ 44 | * stream: process.stdout, 45 | * streamErr: fs.createWriteStream('path/to/log.txt') 46 | * }); 47 | * jsonc.log({ info: 'this is logged to console' }); 48 | * jsonc.log(new Error('this is logged to file')); 49 | */ 50 | static config(cfg: IConfig): void; 51 | /** 52 | * Stringifies and logs the given arguments to console. This will 53 | * automatically handle circular references; so it won't throw. 54 | * 55 | * If an `Error` instance is passed, it will log the `.stack` property on 56 | * the instance, without stringifying the object. 57 | * 58 | *
This method is added for convenience. Works the same as `jsonc.log()`.
59 | * @name jsonc.safe.log 60 | * @function 61 | * 62 | * @param {...any[]} [args] - Arguments to be logged. 63 | * @returns {void} 64 | */ 65 | static log(...args: any[]): void; 66 | /** 67 | * Pretty version of `log()` method. Stringifies and logs the given 68 | * arguments to console, with indents. This will automatically handle 69 | * circular references; so it won't throw. 70 | * 71 | * If an `Error` instance is passed, it will log the `.stack` property on 72 | * the instance, without stringifying the object. 73 | * 74 | *
This method is added for convenience. Works the same as `jsonc.logp()`.
75 | * @name jsonc.safe.logp 76 | * @function 77 | * 78 | * @param {...any[]} [args] - Arguments to be logged. 79 | * @returns {void} 80 | */ 81 | static logp(...args: any[]): void; 82 | /** 83 | * Safe version of `jsonc.parse()`. Parses the given string into a 84 | * JavaScript object. 85 | * @name jsonc.safe.parse 86 | * @function 87 | * 88 | * @param {string} str - JSON string to be parsed. 89 | * @param {IParseOptions|Reviver} [options] - Either a parse options 90 | * object or a reviver function. 91 | * @param {Reviver} [options.reviver] - A function that can filter and 92 | * transform the results. It receives each of the keys and values, and 93 | * its return value is used instead of the original value. If it 94 | * returns what it received, then the structure is not modified. If it 95 | * returns `undefined` then the member is deleted. 96 | * @param {boolean} [options.stripComments=true] - Whether to strip 97 | * comments from the JSON string. Note that it will return the first 98 | * parameter as an error if this is set to `false` and the string 99 | * includes comments. 100 | * 101 | * @returns {Array} - Safe methods return an array with the 102 | * first item being the `Error` instance caught. If successful, second 103 | * item will be the result: `[Error, any]` 104 | * 105 | * @example 106 | * import { safe as jsonc } from 'jsonc'; 107 | * const [err, result] = jsonc.parse('--invalid JSON--'); 108 | * if (err) { 109 | * console.log('Failed to parse JSON: ' + err.message); 110 | * } else { 111 | * console.log(result); 112 | * } 113 | */ 114 | static parse(str: string, options?: IParseOptions | Reviver): [Error, undefined] | [null, any]; 115 | /** 116 | * Safe version of `jsonc.stringify()`. Stringifies the given 117 | * JavaScript object. The input object can have circular references 118 | * which will return the string `"[Circular]"` for each circular 119 | * reference, by default. You can use a replacer function to replace or 120 | * remove circular references instead. 121 | * @name jsonc.safe.stringify 122 | * @function 123 | * 124 | * @param {*} value - JavaScript value to be stringified. 125 | * @param {IStringifyOptions|Replacer} [options] - Stringify options or 126 | * a replacer. 127 | * @param {Replacer} [options.replacer] - Determines how object values 128 | * are stringified for objects. It can be an array of strings or 129 | * numbers; or a function with the following signature: `(key: string, 130 | * value: any) => any`. 131 | * @param {string|number} [options.space] - Specifies the indentation 132 | * of nested structures. If it is omitted, the text will be packed 133 | * without extra whitespace. If it is a number, it will specify the 134 | * number of spaces to indent at each level. If it is a string (such as 135 | * `"\t"` or `" "`), it contains the characters used to indent at 136 | * each level. 137 | * @param {boolean} [options.handleCircular=true] - Whether to handle 138 | * circular references (if any) by replacing their values with the 139 | * string `"[Circular]"`. You can also use a replacer function to 140 | * replace or remove circular references instead. 141 | * @param {string|number} [space] - This takes effect if second 142 | * argument is the `replacer` or a falsy value. Included for supporting 143 | * the signature of native `JSON.stringify()` method. 144 | * 145 | * @returns {Array} - Safe methods return an array with the 146 | * first item being the `Error` instance caught. If successful, second 147 | * item will be the result: `[Error, string]` 148 | * 149 | * @example 150 | * import { safe as jsonc } from 'jsonc'; 151 | * const obj = { key: 'value' }; 152 | * let [err, str] = jsonc.stringify(obj); 153 | * if (!err) console.log(str); // '{"key":"value"}' 154 | * 155 | * // pretty output with indents 156 | * let [err, pretty] = jsonc.stringify(obj, null, 2); 157 | * // equivalent to: 158 | * [err, pretty] = jsonc.stringify(obj, { reviver: null, space: 2 }); 159 | * if (!err) console.log(pretty); 160 | * // { 161 | * // "key": "value" 162 | * // } 163 | */ 164 | static stringify(value: any, option?: IStringifyOptions): [Error, undefined] | [null, string]; 165 | static stringify(value: any, replacer: Replacer, space?: string | number): [Error, undefined] | [null, string]; 166 | /** 167 | * Specifies whether the given string has well-formed JSON structure. 168 | * 169 | * Note that, not all JSON-parsable strings are considered well-formed JSON 170 | * structures. JSON is built on two structures; a collection of name/value 171 | * pairs (object) or an ordered list of values (array). 172 | * 173 | * For example, `JSON.parse('true')` will parse successfully but 174 | * `jsonc.isJSON('true')` will return `false` since it has no object or 175 | * array structure. 176 | * 177 | *
This method is added for convenience. Works the same as 178 | * `jsonc.isJSON()`.
179 | * @name jsonc.safe.isJSON 180 | * @function 181 | * 182 | * @param {string} str - String to be validated. 183 | * @param {boolean} [allowComments=false] - Whether comments should be 184 | * considered valid. 185 | * 186 | * @returns {boolean} 187 | * 188 | * @example 189 | * import { safe as jsonc } from 'jsonc'; 190 | * jsonc.isJSON('{"x":1}'); // true 191 | * jsonc.isJSON('true'); // false 192 | * jsonc.isJSON('[1, false, null]'); // true 193 | * jsonc.isJSON('string'); // false 194 | * jsonc.isJSON('null'); // false 195 | */ 196 | static isJSON(str: string, allowComments?: boolean): boolean; 197 | /** 198 | * Strips comments from the given JSON string. 199 | * @name jsonc.safe.stripComments 200 | * @function 201 | * 202 | * @param {string} str - JSON string. 203 | * @param {boolean} [whitespace=false] - Whether to replace comments 204 | * with whitespace instead of stripping them entirely. 205 | * 206 | * @returns {Array} - Safe methods return an array with the 207 | * first item being the `Error` instance caught. If successful, second 208 | * item will be the result: `[Error, string]` 209 | * 210 | * @example 211 | * import { safe as jsonc } from 'jsonc'; 212 | * const [err, str] = jsonc.stripComments('// comments\n{"key":"value"}'); 213 | * if (!err) console.log(str); // '\n{"key":"value"}' 214 | */ 215 | static stripComments(str: string, whitespace?: boolean): [Error, undefined] | [null, string]; 216 | /** 217 | * Safe version of `jsonc.uglify()`. Uglifies the given JSON string. 218 | * @name jsonc.safe.uglify 219 | * @function 220 | * 221 | * @param {string} str - JSON string to be uglified. 222 | * 223 | * @returns {Array} - Safe methods return an array with the 224 | * first item being the `Error` instance caught. If successful, second 225 | * item will be the result: `[Error, string]` 226 | * 227 | * @example 228 | * import { safe as jsonc } from 'jsonc'; 229 | * const pretty = ` 230 | * { 231 | * // comments... 232 | * "key": "value" 233 | * } 234 | * `; 235 | * const [err, ugly] = jsonc.uglify(pretty); 236 | * if (!err) console.log(ugly); // '{"key":"value"}' 237 | */ 238 | static uglify(str: string): [Error, undefined] | [null, string]; 239 | /** 240 | * Safe version of `jsonc.beautify()`. Beautifies the given JSON 241 | * string. Note that this will remove comments, if any. 242 | * @name jsonc.safe.beautify 243 | * @function 244 | * 245 | * @param {string} str - JSON string to be beautified. 246 | * @param {string|number} [space=2] Specifies the indentation of nested 247 | * structures. If it is omitted, the text will be packed without extra 248 | * whitespace. If it is a number, it will specify the number of spaces 249 | * to indent at each level. If it is a string (such as "\t" or 250 | * " "), it contains the characters used to indent at each level. 251 | * 252 | * @returns {Array} - Safe methods return an array with the 253 | * first item being the `Error` instance caught. If successful, second 254 | * item will be the result: `[Error, string]` 255 | * 256 | * @example 257 | * import { safe as jsonc } from 'jsonc'; 258 | * const ugly = '{"key":"value"}'; 259 | * const [err, pretty] = jsonc.beautify(ugly); 260 | * if (!err) console.log(pretty); 261 | * // { 262 | * // "key": "value" 263 | * // } 264 | */ 265 | static beautify(str: string, space?: string | number): [Error, undefined] | [null, string]; 266 | /** 267 | * Safe version of `jsonc.normalize()`. Normalizes the given value by 268 | * stringifying and parsing it back to a Javascript object. 269 | * @name jsonc.safe.normalize 270 | * @function 271 | * 272 | * @param {any} value 273 | * @param {Replacer} [replacer] - Determines how object values are 274 | * normalized for objects. It can be a function or an array of strings. 275 | * 276 | * @returns {Array} - Safe methods return an array with the 277 | * first item being the `Error` instance caught. If successful, second 278 | * item will be the result: `[Error, any]` 279 | * 280 | * @example 281 | * import { safe as jsonc } from 'jsonc'; 282 | * const c = new SomeClass(); 283 | * console.log(c.constructor.name); // "SomeClass" 284 | * const [err, normalized] = jsonc.normalize(c); 285 | * if (err) { 286 | * console.log('Failed to normalize: ' + err.message); 287 | * } else { 288 | * console.log(normalized.constructor.name); // "Object" 289 | * } 290 | */ 291 | static normalize(value: any, replacer?: Replacer): [Error, undefined] | [null, any]; 292 | /** 293 | * Safe version of `jsonc.read()`. Asynchronously reads a JSON file, 294 | * strips comments and UTF-8 BOM and parses the JSON content. 295 | * @name jsonc.safe.read 296 | * @function 297 | * 298 | * @param {string} filePath - Path to JSON file. 299 | * @param {Function|IReadOptions} [options] - Read options. 300 | * @param {Function} [options.reviver] - A function that can filter and 301 | * transform the results. It receives each of the keys and values, and 302 | * its return value is used instead of the original value. If it 303 | * returns what it received, then the structure is not modified. If it 304 | * returns undefined then the member is deleted. 305 | * @param {boolean} [options.stripComments=true] - Whether to strip 306 | * comments from the JSON string. Note that it will fail if this is 307 | * set to `false` and the string includes comments. 308 | * 309 | * @returns {Promise} - Safe methods return an array with 310 | * the first item being the `Error` instance caught. If successful, 311 | * second item will be the result: `Promise<[Error, any]>` 312 | * 313 | * @example Using async/await (recommended) 314 | * import { safe as jsonc } from 'jsonc'; 315 | * (async () => { 316 | * const [err, obj] = await jsonc.read('path/to/file.json'); 317 | * if (err) { 318 | * console.log('Failed to read JSON file'); 319 | * } catch (err) { 320 | * console.log(typeof obj); // "object" 321 | * } 322 | * })(); 323 | * 324 | * @example Using promises 325 | * import { safe as jsonc } from 'jsonc'; 326 | * jsonc.read('path/to/file.json') 327 | * .then([err, obj] => { 328 | * if (err) { 329 | * console.log('Failed to read JSON file'); 330 | * } else { 331 | * console.log(typeof obj); // "object" 332 | * } 333 | * }) 334 | * // .catch(err => {}); // this is never invoked when safe version is used. 335 | */ 336 | static read(filePath: string, options?: IReadOptions): Promise<[Error, undefined] | [null, any]>; 337 | /** 338 | * Safe version of `jsonc.readSync()`. Synchronously reads a JSON file, 339 | * strips UTF-8 BOM and parses the JSON content. 340 | * @name jsonc.safe.readSync 341 | * @function 342 | * 343 | * @param {string} filePath - Path to JSON file. 344 | * @param {Function|IReadOptions} [options] - Read options. 345 | * @param {Function} [options.reviver] - A function that can filter and 346 | * transform the results. It receives each of the keys and values, and 347 | * its return value is used instead of the original value. If it 348 | * returns what it received, then the structure is not modified. If it 349 | * returns undefined then the member is deleted. 350 | * @param {boolean} [options.stripComments=true] - Whether to strip 351 | * comments from the JSON string. Note that it will fail if this is 352 | * set to `false` and the string includes comments. 353 | * 354 | * @returns {Array} - Safe methods return an array with 355 | * the first item being the `Error` instance caught. If successful, 356 | * second item will be the result: `[Error, any]` 357 | * 358 | * @example 359 | * import { safe as jsonc } from 'jsonc'; 360 | * const [err, obj] = jsonc.readSync('path/to/file.json'); 361 | * if (!err) console.log(typeof obj); // "object" 362 | */ 363 | static readSync(filePath: string, options?: IReadOptions): [Error, undefined] | [null, any]; 364 | /** 365 | * Safe version of `jsonc.write()`. Asynchronously writes a JSON file 366 | * from the given JavaScript object. 367 | * @name jsonc.safe.write 368 | * @function 369 | * 370 | * @param {string} filePath - Path to JSON file to be written. 371 | * @param {any} data - Data to be stringified into JSON. 372 | * @param {IWriteOptions} [options] - Write options. 373 | * @param {Replacer} [options.replacer] - Determines how object values 374 | * are stringified for objects. It can be a function or an array of 375 | * strings. 376 | * @param {string|number} [options.space] - Specifies the indentation 377 | * of nested structures. If it is omitted, the text will be packed 378 | * without extra whitespace. If it is a number, it will specify the 379 | * number of spaces to indent at each level. If it is a string (such as 380 | * "\t" or " "), it contains the characters used to indent at each 381 | * level. 382 | * @param {number} [options.mode=438] - FileSystem permission mode to 383 | * be used when writing the file. Default is `438` (`0666` in octal). 384 | * @param {boolean} [options.autoPath=true] - Specifies whether to 385 | * create path directories if they don't exist. This will throw if set 386 | * to `false` and path does not exist. 387 | * 388 | * @returns {Promise} - Safe methods return an array with the 389 | * first item being the `Error` instance caught. If successful, 390 | * second item will be the result: `Promise<[Error, boolean]>` 391 | * 392 | * @example Using async/await (recommended) 393 | * import { safe as jsonc } from 'jsonc'; 394 | * (async () => { 395 | * const [err, success] = await jsonc.write('path/to/file.json', data); 396 | * if (err) { 397 | * console.log('Failed to read JSON file'); 398 | * } else { 399 | * console.log('Successfully wrote JSON file'); 400 | * } 401 | * })(); 402 | * 403 | * @example Using promises 404 | * import { safe as jsonc } from 'jsonc'; 405 | * jsonc.write('path/to/file.json', data) 406 | * .then([err, obj] => { 407 | * if (err) { 408 | * console.log('Failed to read JSON file'); 409 | * } else { 410 | * console.log('Successfully wrote JSON file'); 411 | * } 412 | * }) 413 | * // .catch(err => {}); // this is never invoked when safe version is used. 414 | */ 415 | static write(filePath: string, data: any, options?: IWriteOptions): Promise<[Error, undefined] | [null, boolean]>; 416 | /** 417 | * Safe version of `jsonc.writeSync()`. Synchronously writes a JSON 418 | * file from the given JavaScript object. 419 | * @name jsonc.safe.writeSync 420 | * @function 421 | * 422 | * @param {string} filePath - Path to JSON file to be written. 423 | * @param {any} data - Data to be stringified into JSON. 424 | * @param {IWriteOptions} [options] - Write options. 425 | * @param {Replacer} [options.replacer] - Determines how object values 426 | * are stringified for objects. It can be a function or an array of 427 | * strings. 428 | * @param {string|number} [options.space] - Specifies the indentation 429 | * of nested structures. If it is omitted, the text will be packed 430 | * without extra whitespace. If it is a number, it will specify the 431 | * number of spaces to indent at each level. If it is a string (such as 432 | * "\t" or " "), it contains the characters used to indent at each 433 | * level. 434 | * @param {number} [options.mode=438] - FileSystem permission mode to 435 | * be used when writing the file. Default is `438` (`0666` in octal). 436 | * @param {boolean} [options.autoPath=true] - Specifies whether to 437 | * create path directories if they don't exist. This will throw if set 438 | * to `false` and path does not exist. 439 | * 440 | * @returns {Array} - Safe methods return an array with the 441 | * first item being the `Error` instance caught. If successful, second 442 | * item will be the result: `[Error, boolean]` 443 | * 444 | * @example 445 | * import { safe as jsonc } from 'jsonc'; 446 | * const [err, obj] = jsonc.writeSync('path/to/file.json'); 447 | * if (!err) console.log(typeof obj); // "object" 448 | */ 449 | static writeSync(filePath: string, data: any, options?: IWriteOptions): [Error, undefined] | [null, boolean]; 450 | } 451 | export { jsoncSafe }; 452 | -------------------------------------------------------------------------------- /lib/jsonc.safe.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* tslint:disable:class-name no-require-imports no-default-export max-line-length interface-name max-classes-per-file max-file-line-count */ 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | // core modules 5 | // dep modules 6 | var fast_safe_stringify_1 = require("fast-safe-stringify"); 7 | var stripJsonComments = require("strip-json-comments"); 8 | // own modules 9 | var helper_1 = require("./helper"); 10 | var jsonc_1 = require("./jsonc"); 11 | // constants, variables 12 | var safeSync = helper_1.helper.safeSync, safeAsync = helper_1.helper.safeAsync; 13 | /** 14 | * Class that provides safe versions of `jsonc` methods. Safe methods provide a 15 | * way to easily handle errors without throwing; so that you don't need to use 16 | * try/catch blocks. 17 | * 18 | * Each method (except a few such as `.isJSON`), will return an array with the 19 | * first item being the `Error` instance caught. If successful, second item 20 | * will be the result. 21 | * @name jsonc.safe 22 | * @class 23 | * 24 | * @example 25 | * const { safe } = require('jsonc'); 26 | * // or 27 | * import { safe as jsonc } from 'jsonc'; 28 | * 29 | * const [err, result] = jsonc.parse('[invalid JSON}'); 30 | * if (err) { 31 | * console.log(`Failed to parse JSON: ${err.message}`); 32 | * } else { 33 | * console.log(result); 34 | * } 35 | */ 36 | var jsoncSafe = /** @class */ (function () { 37 | function jsoncSafe() { 38 | } 39 | /** 40 | * Configures `jsonc` object. 41 | * 42 | *
This method is added for convenience. Works the same as `jsonc.config()`.
43 | * 44 | * @name jsonc.safe.config 45 | * @function 46 | * 47 | * @param {IConfig} cfg - Configurations. 48 | * @param {NodeJS.WriteStream} [stream] - Stream to write logs to. This is 49 | * used with `.log()` and `.logp()` methods. 50 | * @param {NodeJS.WriteStream} [streamErr] - Stream to write error logs to. 51 | * This is used with `.log()` and `.logp()` methods. 52 | * 53 | * @example 54 | * import { safe as jsonc } from 'jsonc'; 55 | * // Output logs to stdout but logs containing errors to a file. 56 | * jsonc.config({ 57 | * stream: process.stdout, 58 | * streamErr: fs.createWriteStream('path/to/log.txt') 59 | * }); 60 | * jsonc.log({ info: 'this is logged to console' }); 61 | * jsonc.log(new Error('this is logged to file')); 62 | */ 63 | jsoncSafe.config = function (cfg) { 64 | jsonc_1.jsonc.config(cfg); 65 | }; 66 | /** 67 | * Stringifies and logs the given arguments to console. This will 68 | * automatically handle circular references; so it won't throw. 69 | * 70 | * If an `Error` instance is passed, it will log the `.stack` property on 71 | * the instance, without stringifying the object. 72 | * 73 | *
This method is added for convenience. Works the same as `jsonc.log()`.
74 | * @name jsonc.safe.log 75 | * @function 76 | * 77 | * @param {...any[]} [args] - Arguments to be logged. 78 | * @returns {void} 79 | */ 80 | jsoncSafe.log = function () { 81 | var args = []; 82 | for (var _i = 0; _i < arguments.length; _i++) { 83 | args[_i] = arguments[_i]; 84 | } 85 | jsonc_1.jsonc.log.apply(jsonc_1.jsonc, args); 86 | }; 87 | /** 88 | * Pretty version of `log()` method. Stringifies and logs the given 89 | * arguments to console, with indents. This will automatically handle 90 | * circular references; so it won't throw. 91 | * 92 | * If an `Error` instance is passed, it will log the `.stack` property on 93 | * the instance, without stringifying the object. 94 | * 95 | *
This method is added for convenience. Works the same as `jsonc.logp()`.
96 | * @name jsonc.safe.logp 97 | * @function 98 | * 99 | * @param {...any[]} [args] - Arguments to be logged. 100 | * @returns {void} 101 | */ 102 | jsoncSafe.logp = function () { 103 | var args = []; 104 | for (var _i = 0; _i < arguments.length; _i++) { 105 | args[_i] = arguments[_i]; 106 | } 107 | jsonc_1.jsonc.logp.apply(jsonc_1.jsonc, args); 108 | }; 109 | /** 110 | * Safe version of `jsonc.parse()`. Parses the given string into a 111 | * JavaScript object. 112 | * @name jsonc.safe.parse 113 | * @function 114 | * 115 | * @param {string} str - JSON string to be parsed. 116 | * @param {IParseOptions|Reviver} [options] - Either a parse options 117 | * object or a reviver function. 118 | * @param {Reviver} [options.reviver] - A function that can filter and 119 | * transform the results. It receives each of the keys and values, and 120 | * its return value is used instead of the original value. If it 121 | * returns what it received, then the structure is not modified. If it 122 | * returns `undefined` then the member is deleted. 123 | * @param {boolean} [options.stripComments=true] - Whether to strip 124 | * comments from the JSON string. Note that it will return the first 125 | * parameter as an error if this is set to `false` and the string 126 | * includes comments. 127 | * 128 | * @returns {Array} - Safe methods return an array with the 129 | * first item being the `Error` instance caught. If successful, second 130 | * item will be the result: `[Error, any]` 131 | * 132 | * @example 133 | * import { safe as jsonc } from 'jsonc'; 134 | * const [err, result] = jsonc.parse('--invalid JSON--'); 135 | * if (err) { 136 | * console.log('Failed to parse JSON: ' + err.message); 137 | * } else { 138 | * console.log(result); 139 | * } 140 | */ 141 | jsoncSafe.parse = function (str, options) { 142 | return safeSync(jsonc_1.jsonc.parse)(str, options); 143 | }; 144 | jsoncSafe.stringify = function (value, optionsOrReplacer, space) { 145 | var opts = helper_1.helper.getStringifyOptions(optionsOrReplacer, space); 146 | try { 147 | return [null, fast_safe_stringify_1.default(value, opts.replacer, opts.space)]; 148 | } 149 | catch (err) { 150 | return [err, undefined]; 151 | } 152 | }; 153 | /** 154 | * Specifies whether the given string has well-formed JSON structure. 155 | * 156 | * Note that, not all JSON-parsable strings are considered well-formed JSON 157 | * structures. JSON is built on two structures; a collection of name/value 158 | * pairs (object) or an ordered list of values (array). 159 | * 160 | * For example, `JSON.parse('true')` will parse successfully but 161 | * `jsonc.isJSON('true')` will return `false` since it has no object or 162 | * array structure. 163 | * 164 | *
This method is added for convenience. Works the same as 165 | * `jsonc.isJSON()`.
166 | * @name jsonc.safe.isJSON 167 | * @function 168 | * 169 | * @param {string} str - String to be validated. 170 | * @param {boolean} [allowComments=false] - Whether comments should be 171 | * considered valid. 172 | * 173 | * @returns {boolean} 174 | * 175 | * @example 176 | * import { safe as jsonc } from 'jsonc'; 177 | * jsonc.isJSON('{"x":1}'); // true 178 | * jsonc.isJSON('true'); // false 179 | * jsonc.isJSON('[1, false, null]'); // true 180 | * jsonc.isJSON('string'); // false 181 | * jsonc.isJSON('null'); // false 182 | */ 183 | jsoncSafe.isJSON = function (str, allowComments) { 184 | if (allowComments === void 0) { allowComments = false; } 185 | return jsonc_1.jsonc.isJSON(str, allowComments); 186 | }; 187 | /** 188 | * Strips comments from the given JSON string. 189 | * @name jsonc.safe.stripComments 190 | * @function 191 | * 192 | * @param {string} str - JSON string. 193 | * @param {boolean} [whitespace=false] - Whether to replace comments 194 | * with whitespace instead of stripping them entirely. 195 | * 196 | * @returns {Array} - Safe methods return an array with the 197 | * first item being the `Error` instance caught. If successful, second 198 | * item will be the result: `[Error, string]` 199 | * 200 | * @example 201 | * import { safe as jsonc } from 'jsonc'; 202 | * const [err, str] = jsonc.stripComments('// comments\n{"key":"value"}'); 203 | * if (!err) console.log(str); // '\n{"key":"value"}' 204 | */ 205 | jsoncSafe.stripComments = function (str, whitespace) { 206 | if (whitespace === void 0) { whitespace = false; } 207 | return safeSync(stripJsonComments)(str, { whitespace: whitespace }); 208 | }; 209 | /** 210 | * Safe version of `jsonc.uglify()`. Uglifies the given JSON string. 211 | * @name jsonc.safe.uglify 212 | * @function 213 | * 214 | * @param {string} str - JSON string to be uglified. 215 | * 216 | * @returns {Array} - Safe methods return an array with the 217 | * first item being the `Error` instance caught. If successful, second 218 | * item will be the result: `[Error, string]` 219 | * 220 | * @example 221 | * import { safe as jsonc } from 'jsonc'; 222 | * const pretty = ` 223 | * { 224 | * // comments... 225 | * "key": "value" 226 | * } 227 | * `; 228 | * const [err, ugly] = jsonc.uglify(pretty); 229 | * if (!err) console.log(ugly); // '{"key":"value"}' 230 | */ 231 | jsoncSafe.uglify = function (str) { 232 | return safeSync(jsonc_1.jsonc.uglify)(str); 233 | }; 234 | /** 235 | * Safe version of `jsonc.beautify()`. Beautifies the given JSON 236 | * string. Note that this will remove comments, if any. 237 | * @name jsonc.safe.beautify 238 | * @function 239 | * 240 | * @param {string} str - JSON string to be beautified. 241 | * @param {string|number} [space=2] Specifies the indentation of nested 242 | * structures. If it is omitted, the text will be packed without extra 243 | * whitespace. If it is a number, it will specify the number of spaces 244 | * to indent at each level. If it is a string (such as "\t" or 245 | * " "), it contains the characters used to indent at each level. 246 | * 247 | * @returns {Array} - Safe methods return an array with the 248 | * first item being the `Error` instance caught. If successful, second 249 | * item will be the result: `[Error, string]` 250 | * 251 | * @example 252 | * import { safe as jsonc } from 'jsonc'; 253 | * const ugly = '{"key":"value"}'; 254 | * const [err, pretty] = jsonc.beautify(ugly); 255 | * if (!err) console.log(pretty); 256 | * // { 257 | * // "key": "value" 258 | * // } 259 | */ 260 | jsoncSafe.beautify = function (str, space) { 261 | if (space === void 0) { space = 2; } 262 | return safeSync(jsonc_1.jsonc.beautify)(str, space); 263 | }; 264 | /** 265 | * Safe version of `jsonc.normalize()`. Normalizes the given value by 266 | * stringifying and parsing it back to a Javascript object. 267 | * @name jsonc.safe.normalize 268 | * @function 269 | * 270 | * @param {any} value 271 | * @param {Replacer} [replacer] - Determines how object values are 272 | * normalized for objects. It can be a function or an array of strings. 273 | * 274 | * @returns {Array} - Safe methods return an array with the 275 | * first item being the `Error` instance caught. If successful, second 276 | * item will be the result: `[Error, any]` 277 | * 278 | * @example 279 | * import { safe as jsonc } from 'jsonc'; 280 | * const c = new SomeClass(); 281 | * console.log(c.constructor.name); // "SomeClass" 282 | * const [err, normalized] = jsonc.normalize(c); 283 | * if (err) { 284 | * console.log('Failed to normalize: ' + err.message); 285 | * } else { 286 | * console.log(normalized.constructor.name); // "Object" 287 | * } 288 | */ 289 | jsoncSafe.normalize = function (value, replacer) { 290 | return safeSync(jsonc_1.jsonc.normalize)(value, replacer); 291 | }; 292 | /** 293 | * Safe version of `jsonc.read()`. Asynchronously reads a JSON file, 294 | * strips comments and UTF-8 BOM and parses the JSON content. 295 | * @name jsonc.safe.read 296 | * @function 297 | * 298 | * @param {string} filePath - Path to JSON file. 299 | * @param {Function|IReadOptions} [options] - Read options. 300 | * @param {Function} [options.reviver] - A function that can filter and 301 | * transform the results. It receives each of the keys and values, and 302 | * its return value is used instead of the original value. If it 303 | * returns what it received, then the structure is not modified. If it 304 | * returns undefined then the member is deleted. 305 | * @param {boolean} [options.stripComments=true] - Whether to strip 306 | * comments from the JSON string. Note that it will fail if this is 307 | * set to `false` and the string includes comments. 308 | * 309 | * @returns {Promise} - Safe methods return an array with 310 | * the first item being the `Error` instance caught. If successful, 311 | * second item will be the result: `Promise<[Error, any]>` 312 | * 313 | * @example Using async/await (recommended) 314 | * import { safe as jsonc } from 'jsonc'; 315 | * (async () => { 316 | * const [err, obj] = await jsonc.read('path/to/file.json'); 317 | * if (err) { 318 | * console.log('Failed to read JSON file'); 319 | * } catch (err) { 320 | * console.log(typeof obj); // "object" 321 | * } 322 | * })(); 323 | * 324 | * @example Using promises 325 | * import { safe as jsonc } from 'jsonc'; 326 | * jsonc.read('path/to/file.json') 327 | * .then([err, obj] => { 328 | * if (err) { 329 | * console.log('Failed to read JSON file'); 330 | * } else { 331 | * console.log(typeof obj); // "object" 332 | * } 333 | * }) 334 | * // .catch(err => {}); // this is never invoked when safe version is used. 335 | */ 336 | jsoncSafe.read = function (filePath, options) { 337 | return safeAsync(jsonc_1.jsonc.read(filePath, options)); 338 | }; 339 | /** 340 | * Safe version of `jsonc.readSync()`. Synchronously reads a JSON file, 341 | * strips UTF-8 BOM and parses the JSON content. 342 | * @name jsonc.safe.readSync 343 | * @function 344 | * 345 | * @param {string} filePath - Path to JSON file. 346 | * @param {Function|IReadOptions} [options] - Read options. 347 | * @param {Function} [options.reviver] - A function that can filter and 348 | * transform the results. It receives each of the keys and values, and 349 | * its return value is used instead of the original value. If it 350 | * returns what it received, then the structure is not modified. If it 351 | * returns undefined then the member is deleted. 352 | * @param {boolean} [options.stripComments=true] - Whether to strip 353 | * comments from the JSON string. Note that it will fail if this is 354 | * set to `false` and the string includes comments. 355 | * 356 | * @returns {Array} - Safe methods return an array with 357 | * the first item being the `Error` instance caught. If successful, 358 | * second item will be the result: `[Error, any]` 359 | * 360 | * @example 361 | * import { safe as jsonc } from 'jsonc'; 362 | * const [err, obj] = jsonc.readSync('path/to/file.json'); 363 | * if (!err) console.log(typeof obj); // "object" 364 | */ 365 | jsoncSafe.readSync = function (filePath, options) { 366 | return safeSync(jsonc_1.jsonc.readSync)(filePath, options); 367 | }; 368 | /** 369 | * Safe version of `jsonc.write()`. Asynchronously writes a JSON file 370 | * from the given JavaScript object. 371 | * @name jsonc.safe.write 372 | * @function 373 | * 374 | * @param {string} filePath - Path to JSON file to be written. 375 | * @param {any} data - Data to be stringified into JSON. 376 | * @param {IWriteOptions} [options] - Write options. 377 | * @param {Replacer} [options.replacer] - Determines how object values 378 | * are stringified for objects. It can be a function or an array of 379 | * strings. 380 | * @param {string|number} [options.space] - Specifies the indentation 381 | * of nested structures. If it is omitted, the text will be packed 382 | * without extra whitespace. If it is a number, it will specify the 383 | * number of spaces to indent at each level. If it is a string (such as 384 | * "\t" or " "), it contains the characters used to indent at each 385 | * level. 386 | * @param {number} [options.mode=438] - FileSystem permission mode to 387 | * be used when writing the file. Default is `438` (`0666` in octal). 388 | * @param {boolean} [options.autoPath=true] - Specifies whether to 389 | * create path directories if they don't exist. This will throw if set 390 | * to `false` and path does not exist. 391 | * 392 | * @returns {Promise} - Safe methods return an array with the 393 | * first item being the `Error` instance caught. If successful, 394 | * second item will be the result: `Promise<[Error, boolean]>` 395 | * 396 | * @example Using async/await (recommended) 397 | * import { safe as jsonc } from 'jsonc'; 398 | * (async () => { 399 | * const [err, success] = await jsonc.write('path/to/file.json', data); 400 | * if (err) { 401 | * console.log('Failed to read JSON file'); 402 | * } else { 403 | * console.log('Successfully wrote JSON file'); 404 | * } 405 | * })(); 406 | * 407 | * @example Using promises 408 | * import { safe as jsonc } from 'jsonc'; 409 | * jsonc.write('path/to/file.json', data) 410 | * .then([err, obj] => { 411 | * if (err) { 412 | * console.log('Failed to read JSON file'); 413 | * } else { 414 | * console.log('Successfully wrote JSON file'); 415 | * } 416 | * }) 417 | * // .catch(err => {}); // this is never invoked when safe version is used. 418 | */ 419 | jsoncSafe.write = function (filePath, data, options) { 420 | return safeAsync(jsonc_1.jsonc.write(filePath, data, options)); 421 | }; 422 | /** 423 | * Safe version of `jsonc.writeSync()`. Synchronously writes a JSON 424 | * file from the given JavaScript object. 425 | * @name jsonc.safe.writeSync 426 | * @function 427 | * 428 | * @param {string} filePath - Path to JSON file to be written. 429 | * @param {any} data - Data to be stringified into JSON. 430 | * @param {IWriteOptions} [options] - Write options. 431 | * @param {Replacer} [options.replacer] - Determines how object values 432 | * are stringified for objects. It can be a function or an array of 433 | * strings. 434 | * @param {string|number} [options.space] - Specifies the indentation 435 | * of nested structures. If it is omitted, the text will be packed 436 | * without extra whitespace. If it is a number, it will specify the 437 | * number of spaces to indent at each level. If it is a string (such as 438 | * "\t" or " "), it contains the characters used to indent at each 439 | * level. 440 | * @param {number} [options.mode=438] - FileSystem permission mode to 441 | * be used when writing the file. Default is `438` (`0666` in octal). 442 | * @param {boolean} [options.autoPath=true] - Specifies whether to 443 | * create path directories if they don't exist. This will throw if set 444 | * to `false` and path does not exist. 445 | * 446 | * @returns {Array} - Safe methods return an array with the 447 | * first item being the `Error` instance caught. If successful, second 448 | * item will be the result: `[Error, boolean]` 449 | * 450 | * @example 451 | * import { safe as jsonc } from 'jsonc'; 452 | * const [err, obj] = jsonc.writeSync('path/to/file.json'); 453 | * if (!err) console.log(typeof obj); // "object" 454 | */ 455 | jsoncSafe.writeSync = function (filePath, data, options) { 456 | return safeSync(jsonc_1.jsonc.writeSync)(filePath, data, options); 457 | }; 458 | return jsoncSafe; 459 | }()); 460 | exports.jsoncSafe = jsoncSafe; 461 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonc", 3 | "version": "2.0.0", 4 | "description": "Everything you need in JSON land. Parse JSON with comments, stringify objects with circular references, etc...", 5 | "author": "Onur Yıldırım ", 6 | "license": "MIT", 7 | "homepage": "https://onury.io/jsonc", 8 | "repository": "onury/jsonc", 9 | "main": "index.js", 10 | "files": [ 11 | "lib", 12 | "index.js", 13 | "LICENSE" 14 | ], 15 | "types": "./lib/jsonc.d.ts", 16 | "engines": { 17 | "node": ">=8" 18 | }, 19 | "scripts": { 20 | "clean": "rimraf ./lib", 21 | "build": "npm run clean && npm run cover && mkdirp ./lib && tsc", 22 | "test": "jest --verbose --no-cache", 23 | "cover": "jest --coverage --verbose --no-cache", 24 | "coveralls": "cat ./test/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js -v", 25 | "report": "open ./test/coverage/lcov-report/index.html", 26 | "docs": "docma -c ./docma.json" 27 | }, 28 | "jest": { 29 | "testEnvironment": "node", 30 | "globals": { 31 | "ts-jest": { 32 | "diagnostics": false 33 | } 34 | }, 35 | "roots": [ 36 | "/src", 37 | "/lib", 38 | "/test" 39 | ], 40 | "transform": { 41 | "^.+\\.tsx?$": "ts-jest" 42 | }, 43 | "testRegex": "(/test/.*|(\\.|/)(test|spec))\\.tsx?$", 44 | "moduleFileExtensions": [ 45 | "ts", 46 | "tsx", 47 | "js", 48 | "json" 49 | ], 50 | "testPathIgnorePatterns": [ 51 | "/backup/", 52 | "/helpers/", 53 | "/coverage/" 54 | ], 55 | "collectCoverageFrom": [ 56 | "src/**/*.ts", 57 | "!src/interfaces.ts" 58 | ], 59 | "coverageDirectory": "./test/coverage" 60 | }, 61 | "keywords": [ 62 | "json", 63 | "object", 64 | "notation", 65 | "safe", 66 | "stringify", 67 | "parse", 68 | "read-file", 69 | "write-file", 70 | "comments", 71 | "circular", 72 | "cyclic", 73 | "console", 74 | "log", 75 | "uglify", 76 | "beautify", 77 | "typescript" 78 | ], 79 | "dependencies": { 80 | "fast-safe-stringify": "^2.0.6", 81 | "graceful-fs": "^4.1.15", 82 | "mkdirp": "^0.5.1", 83 | "parse-json": "^4.0.0", 84 | "strip-bom": "^4.0.0", 85 | "strip-json-comments": "^3.0.1" 86 | }, 87 | "devDependencies": { 88 | "@types/jest": "^24.0.15", 89 | "@types/mkdirp": "^0.5.2", 90 | "@types/node": "^12.0.8", 91 | "@types/parse-json": "^4.0.0", 92 | "@types/strip-bom": "^4.0.1", 93 | "@types/strip-json-comments": "0.0.30", 94 | "coveralls": "^3.0.4", 95 | "docma": "^3.2.2", 96 | "jest": "^24.8.0", 97 | "jest-cli": "^24.8.0", 98 | "rimraf": "^2.6.3", 99 | "ts-jest": "^24.0.2", 100 | "typescript": "^3.5.2" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | // dep modules 2 | import fastSafeStringify from 'fast-safe-stringify'; 3 | import * as fs from 'graceful-fs'; 4 | import * as mkdirp from 'mkdirp'; 5 | 6 | // own modules 7 | import { IConfig, IStringifyOptions, Replacer } from './interfaces'; 8 | 9 | // vars 10 | const oproto = Object.prototype; 11 | 12 | // simple promisification. this won't work for callbacks with more than 2 13 | // args. 14 | function promisify(fn: Function): any { 15 | return (...args) => { 16 | return new Promise((resolve, reject) => { 17 | fn(...args, (err, result) => { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(result); 22 | } 23 | }); 24 | }); 25 | }; 26 | } 27 | 28 | const defaultStringifyOpts: IStringifyOptions = { 29 | replacer: null, 30 | space: 0, 31 | handleCircular: true 32 | }; 33 | 34 | const helper = { 35 | 36 | isObject(o: any): boolean { 37 | return oproto.toString.call(o) === '[object Object]'; 38 | }, 39 | 40 | isPrimitive(value: any): boolean { 41 | const t = typeof value; 42 | return value === null 43 | || value === undefined 44 | || (t !== 'function' && t !== 'object'); 45 | }, 46 | 47 | strLog(value: any, pretty: boolean): string { 48 | if (helper.isPrimitive(value)) return value; 49 | const s = pretty ? ' ' : null; 50 | return fastSafeStringify(value, null, s); 51 | }, 52 | 53 | getLogger(config: IConfig, pretty: boolean): Function { 54 | return (...args: any[]): void => { 55 | let stream = config.stream; 56 | const msg: string = args.map(arg => { 57 | if (arg instanceof Error) { 58 | stream = config.streamErr; 59 | return arg.stack 60 | /* istanbul ignore next */ 61 | || arg.message 62 | /* istanbul ignore next */ 63 | || String(arg); 64 | } 65 | return helper.strLog(arg, pretty); 66 | }).join(' '); 67 | stream.write(msg + '\n'); 68 | }; 69 | }, 70 | 71 | getStringifyOptions(options: IStringifyOptions | Replacer, space: string | number): IStringifyOptions { 72 | if (helper.isObject(options)) { 73 | return { 74 | ...defaultStringifyOpts, 75 | ...options 76 | }; // as IStringifyOptions 77 | } 78 | 79 | if (typeof options === 'function' || Array.isArray(options)) { 80 | return { 81 | ...defaultStringifyOpts, 82 | replacer: options as Replacer, 83 | space 84 | }; 85 | } 86 | 87 | return { 88 | ...defaultStringifyOpts, 89 | space 90 | }; 91 | }, 92 | 93 | fs, 94 | mkdirp, 95 | 96 | promise: { 97 | readFile: promisify(fs.readFile), 98 | writeFile: promisify(fs.writeFile), 99 | mkdirp: promisify(mkdirp) 100 | }, 101 | 102 | safeSync(fn: (...args: any[]) => T): (...args: any[]) => [U | null, T | undefined] { 103 | return (...args: any[]): [U | null, T | undefined] => { 104 | try { 105 | return [null, fn(...args) as T]; 106 | } catch (err) { 107 | return [err, undefined] as [U, undefined]; 108 | } 109 | }; 110 | }, 111 | 112 | safeAsync(promise: Promise): Promise<[U | null, T | undefined]> { 113 | return promise 114 | .then<[null, T]>((data: T) => [null, data]) 115 | .catch<[U, undefined]>(err => [err, undefined]); 116 | } 117 | 118 | }; 119 | 120 | export { helper }; 121 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export type Reviver = (key: string, value: any) => any; 2 | export type Replacer = (key: string, value: any) => any | string[] | number[]; 3 | 4 | export interface IParseOptions { 5 | reviver?: Reviver; 6 | stripComments?: boolean; 7 | } 8 | 9 | export interface IStringifyOptions { 10 | replacer?: Replacer; 11 | space?: string | number; 12 | handleCircular?: boolean; 13 | } 14 | 15 | export interface IReadOptions extends IParseOptions {} 16 | 17 | export interface IWriteOptions { 18 | mode?: number; 19 | autoPath?: boolean; 20 | replacer?: Replacer; 21 | space?: string | number; 22 | } 23 | 24 | export interface IConfig { 25 | stream?: NodeJS.WriteStream; 26 | streamErr?: NodeJS.WriteStream; 27 | } 28 | -------------------------------------------------------------------------------- /src/jsonc.safe.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:class-name no-require-imports no-default-export max-line-length interface-name max-classes-per-file max-file-line-count */ 2 | 3 | // core modules 4 | 5 | // dep modules 6 | import fastSafeStringify from 'fast-safe-stringify'; 7 | import * as stripJsonComments from 'strip-json-comments'; 8 | 9 | // own modules 10 | import { helper } from './helper'; 11 | import { 12 | IConfig, IParseOptions, IReadOptions, IStringifyOptions, IWriteOptions, Replacer, Reviver 13 | } from './interfaces'; 14 | import { jsonc } from './jsonc'; 15 | 16 | // constants, variables 17 | const { safeSync, safeAsync } = helper; 18 | 19 | /** 20 | * Class that provides safe versions of `jsonc` methods. Safe methods provide a 21 | * way to easily handle errors without throwing; so that you don't need to use 22 | * try/catch blocks. 23 | * 24 | * Each method (except a few such as `.isJSON`), will return an array with the 25 | * first item being the `Error` instance caught. If successful, second item 26 | * will be the result. 27 | * @name jsonc.safe 28 | * @class 29 | * 30 | * @example 31 | * const { safe } = require('jsonc'); 32 | * // or 33 | * import { safe as jsonc } from 'jsonc'; 34 | * 35 | * const [err, result] = jsonc.parse('[invalid JSON}'); 36 | * if (err) { 37 | * console.log(`Failed to parse JSON: ${err.message}`); 38 | * } else { 39 | * console.log(result); 40 | * } 41 | */ 42 | class jsoncSafe { 43 | 44 | /** 45 | * Configures `jsonc` object. 46 | * 47 | *
This method is added for convenience. Works the same as `jsonc.config()`.
48 | * 49 | * @name jsonc.safe.config 50 | * @function 51 | * 52 | * @param {IConfig} cfg - Configurations. 53 | * @param {NodeJS.WriteStream} [stream] - Stream to write logs to. This is 54 | * used with `.log()` and `.logp()` methods. 55 | * @param {NodeJS.WriteStream} [streamErr] - Stream to write error logs to. 56 | * This is used with `.log()` and `.logp()` methods. 57 | * 58 | * @example 59 | * import { safe as jsonc } from 'jsonc'; 60 | * // Output logs to stdout but logs containing errors to a file. 61 | * jsonc.config({ 62 | * stream: process.stdout, 63 | * streamErr: fs.createWriteStream('path/to/log.txt') 64 | * }); 65 | * jsonc.log({ info: 'this is logged to console' }); 66 | * jsonc.log(new Error('this is logged to file')); 67 | */ 68 | static config(cfg: IConfig): void { 69 | jsonc.config(cfg); 70 | } 71 | 72 | /** 73 | * Stringifies and logs the given arguments to console. This will 74 | * automatically handle circular references; so it won't throw. 75 | * 76 | * If an `Error` instance is passed, it will log the `.stack` property on 77 | * the instance, without stringifying the object. 78 | * 79 | *
This method is added for convenience. Works the same as `jsonc.log()`.
80 | * @name jsonc.safe.log 81 | * @function 82 | * 83 | * @param {...any[]} [args] - Arguments to be logged. 84 | * @returns {void} 85 | */ 86 | static log(...args: any[]): void { 87 | jsonc.log(...args); 88 | } 89 | 90 | /** 91 | * Pretty version of `log()` method. Stringifies and logs the given 92 | * arguments to console, with indents. This will automatically handle 93 | * circular references; so it won't throw. 94 | * 95 | * If an `Error` instance is passed, it will log the `.stack` property on 96 | * the instance, without stringifying the object. 97 | * 98 | *
This method is added for convenience. Works the same as `jsonc.logp()`.
99 | * @name jsonc.safe.logp 100 | * @function 101 | * 102 | * @param {...any[]} [args] - Arguments to be logged. 103 | * @returns {void} 104 | */ 105 | static logp(...args: any[]): void { 106 | jsonc.logp(...args); 107 | } 108 | 109 | /** 110 | * Safe version of `jsonc.parse()`. Parses the given string into a 111 | * JavaScript object. 112 | * @name jsonc.safe.parse 113 | * @function 114 | * 115 | * @param {string} str - JSON string to be parsed. 116 | * @param {IParseOptions|Reviver} [options] - Either a parse options 117 | * object or a reviver function. 118 | * @param {Reviver} [options.reviver] - A function that can filter and 119 | * transform the results. It receives each of the keys and values, and 120 | * its return value is used instead of the original value. If it 121 | * returns what it received, then the structure is not modified. If it 122 | * returns `undefined` then the member is deleted. 123 | * @param {boolean} [options.stripComments=true] - Whether to strip 124 | * comments from the JSON string. Note that it will return the first 125 | * parameter as an error if this is set to `false` and the string 126 | * includes comments. 127 | * 128 | * @returns {Array} - Safe methods return an array with the 129 | * first item being the `Error` instance caught. If successful, second 130 | * item will be the result: `[Error, any]` 131 | * 132 | * @example 133 | * import { safe as jsonc } from 'jsonc'; 134 | * const [err, result] = jsonc.parse('--invalid JSON--'); 135 | * if (err) { 136 | * console.log('Failed to parse JSON: ' + err.message); 137 | * } else { 138 | * console.log(result); 139 | * } 140 | */ 141 | static parse(str: string, options?: IParseOptions | Reviver): [Error, undefined] | [null, any] { 142 | return safeSync(jsonc.parse)(str, options); 143 | } 144 | 145 | /** 146 | * Safe version of `jsonc.stringify()`. Stringifies the given 147 | * JavaScript object. The input object can have circular references 148 | * which will return the string `"[Circular]"` for each circular 149 | * reference, by default. You can use a replacer function to replace or 150 | * remove circular references instead. 151 | * @name jsonc.safe.stringify 152 | * @function 153 | * 154 | * @param {*} value - JavaScript value to be stringified. 155 | * @param {IStringifyOptions|Replacer} [options] - Stringify options or 156 | * a replacer. 157 | * @param {Replacer} [options.replacer] - Determines how object values 158 | * are stringified for objects. It can be an array of strings or 159 | * numbers; or a function with the following signature: `(key: string, 160 | * value: any) => any`. 161 | * @param {string|number} [options.space] - Specifies the indentation 162 | * of nested structures. If it is omitted, the text will be packed 163 | * without extra whitespace. If it is a number, it will specify the 164 | * number of spaces to indent at each level. If it is a string (such as 165 | * `"\t"` or `" "`), it contains the characters used to indent at 166 | * each level. 167 | * @param {boolean} [options.handleCircular=true] - Whether to handle 168 | * circular references (if any) by replacing their values with the 169 | * string `"[Circular]"`. You can also use a replacer function to 170 | * replace or remove circular references instead. 171 | * @param {string|number} [space] - This takes effect if second 172 | * argument is the `replacer` or a falsy value. Included for supporting 173 | * the signature of native `JSON.stringify()` method. 174 | * 175 | * @returns {Array} - Safe methods return an array with the 176 | * first item being the `Error` instance caught. If successful, second 177 | * item will be the result: `[Error, string]` 178 | * 179 | * @example 180 | * import { safe as jsonc } from 'jsonc'; 181 | * const obj = { key: 'value' }; 182 | * let [err, str] = jsonc.stringify(obj); 183 | * if (!err) console.log(str); // '{"key":"value"}' 184 | * 185 | * // pretty output with indents 186 | * let [err, pretty] = jsonc.stringify(obj, null, 2); 187 | * // equivalent to: 188 | * [err, pretty] = jsonc.stringify(obj, { reviver: null, space: 2 }); 189 | * if (!err) console.log(pretty); 190 | * // { 191 | * // "key": "value" 192 | * // } 193 | */ 194 | static stringify(value: any, option?: IStringifyOptions): [Error, undefined] | [null, string]; 195 | static stringify(value: any, replacer: Replacer, space?: string | number): [Error, undefined] | [null, string]; 196 | static stringify(value: any, optionsOrReplacer?: IStringifyOptions | Replacer, space?: string | number): [Error, undefined] | [null, string] { 197 | const opts = helper.getStringifyOptions(optionsOrReplacer, space); 198 | try { 199 | return [null, fastSafeStringify(value, opts.replacer, opts.space)]; 200 | } catch (err) { 201 | return [err, undefined]; 202 | } 203 | } 204 | 205 | /** 206 | * Specifies whether the given string has well-formed JSON structure. 207 | * 208 | * Note that, not all JSON-parsable strings are considered well-formed JSON 209 | * structures. JSON is built on two structures; a collection of name/value 210 | * pairs (object) or an ordered list of values (array). 211 | * 212 | * For example, `JSON.parse('true')` will parse successfully but 213 | * `jsonc.isJSON('true')` will return `false` since it has no object or 214 | * array structure. 215 | * 216 | *
This method is added for convenience. Works the same as 217 | * `jsonc.isJSON()`.
218 | * @name jsonc.safe.isJSON 219 | * @function 220 | * 221 | * @param {string} str - String to be validated. 222 | * @param {boolean} [allowComments=false] - Whether comments should be 223 | * considered valid. 224 | * 225 | * @returns {boolean} 226 | * 227 | * @example 228 | * import { safe as jsonc } from 'jsonc'; 229 | * jsonc.isJSON('{"x":1}'); // true 230 | * jsonc.isJSON('true'); // false 231 | * jsonc.isJSON('[1, false, null]'); // true 232 | * jsonc.isJSON('string'); // false 233 | * jsonc.isJSON('null'); // false 234 | */ 235 | static isJSON(str: string, allowComments: boolean = false): boolean { 236 | return jsonc.isJSON(str, allowComments); 237 | } 238 | 239 | /** 240 | * Strips comments from the given JSON string. 241 | * @name jsonc.safe.stripComments 242 | * @function 243 | * 244 | * @param {string} str - JSON string. 245 | * @param {boolean} [whitespace=false] - Whether to replace comments 246 | * with whitespace instead of stripping them entirely. 247 | * 248 | * @returns {Array} - Safe methods return an array with the 249 | * first item being the `Error` instance caught. If successful, second 250 | * item will be the result: `[Error, string]` 251 | * 252 | * @example 253 | * import { safe as jsonc } from 'jsonc'; 254 | * const [err, str] = jsonc.stripComments('// comments\n{"key":"value"}'); 255 | * if (!err) console.log(str); // '\n{"key":"value"}' 256 | */ 257 | static stripComments(str: string, whitespace: boolean = false): [Error, undefined] | [null, string] { 258 | return safeSync(stripJsonComments)(str, { whitespace }); 259 | } 260 | 261 | /** 262 | * Safe version of `jsonc.uglify()`. Uglifies the given JSON string. 263 | * @name jsonc.safe.uglify 264 | * @function 265 | * 266 | * @param {string} str - JSON string to be uglified. 267 | * 268 | * @returns {Array} - Safe methods return an array with the 269 | * first item being the `Error` instance caught. If successful, second 270 | * item will be the result: `[Error, string]` 271 | * 272 | * @example 273 | * import { safe as jsonc } from 'jsonc'; 274 | * const pretty = ` 275 | * { 276 | * // comments... 277 | * "key": "value" 278 | * } 279 | * `; 280 | * const [err, ugly] = jsonc.uglify(pretty); 281 | * if (!err) console.log(ugly); // '{"key":"value"}' 282 | */ 283 | static uglify(str: string): [Error, undefined] | [null, string] { 284 | return safeSync(jsonc.uglify)(str); 285 | } 286 | 287 | /** 288 | * Safe version of `jsonc.beautify()`. Beautifies the given JSON 289 | * string. Note that this will remove comments, if any. 290 | * @name jsonc.safe.beautify 291 | * @function 292 | * 293 | * @param {string} str - JSON string to be beautified. 294 | * @param {string|number} [space=2] Specifies the indentation of nested 295 | * structures. If it is omitted, the text will be packed without extra 296 | * whitespace. If it is a number, it will specify the number of spaces 297 | * to indent at each level. If it is a string (such as "\t" or 298 | * " "), it contains the characters used to indent at each level. 299 | * 300 | * @returns {Array} - Safe methods return an array with the 301 | * first item being the `Error` instance caught. If successful, second 302 | * item will be the result: `[Error, string]` 303 | * 304 | * @example 305 | * import { safe as jsonc } from 'jsonc'; 306 | * const ugly = '{"key":"value"}'; 307 | * const [err, pretty] = jsonc.beautify(ugly); 308 | * if (!err) console.log(pretty); 309 | * // { 310 | * // "key": "value" 311 | * // } 312 | */ 313 | static beautify(str: string, space: string | number = 2): [Error, undefined] | [null, string] { 314 | return safeSync(jsonc.beautify)(str, space); 315 | } 316 | 317 | /** 318 | * Safe version of `jsonc.normalize()`. Normalizes the given value by 319 | * stringifying and parsing it back to a Javascript object. 320 | * @name jsonc.safe.normalize 321 | * @function 322 | * 323 | * @param {any} value 324 | * @param {Replacer} [replacer] - Determines how object values are 325 | * normalized for objects. It can be a function or an array of strings. 326 | * 327 | * @returns {Array} - Safe methods return an array with the 328 | * first item being the `Error` instance caught. If successful, second 329 | * item will be the result: `[Error, any]` 330 | * 331 | * @example 332 | * import { safe as jsonc } from 'jsonc'; 333 | * const c = new SomeClass(); 334 | * console.log(c.constructor.name); // "SomeClass" 335 | * const [err, normalized] = jsonc.normalize(c); 336 | * if (err) { 337 | * console.log('Failed to normalize: ' + err.message); 338 | * } else { 339 | * console.log(normalized.constructor.name); // "Object" 340 | * } 341 | */ 342 | static normalize(value: any, replacer?: Replacer): [Error, undefined] | [null, any] { 343 | return safeSync(jsonc.normalize)(value, replacer); 344 | } 345 | 346 | /** 347 | * Safe version of `jsonc.read()`. Asynchronously reads a JSON file, 348 | * strips comments and UTF-8 BOM and parses the JSON content. 349 | * @name jsonc.safe.read 350 | * @function 351 | * 352 | * @param {string} filePath - Path to JSON file. 353 | * @param {Function|IReadOptions} [options] - Read options. 354 | * @param {Function} [options.reviver] - A function that can filter and 355 | * transform the results. It receives each of the keys and values, and 356 | * its return value is used instead of the original value. If it 357 | * returns what it received, then the structure is not modified. If it 358 | * returns undefined then the member is deleted. 359 | * @param {boolean} [options.stripComments=true] - Whether to strip 360 | * comments from the JSON string. Note that it will fail if this is 361 | * set to `false` and the string includes comments. 362 | * 363 | * @returns {Promise} - Safe methods return an array with 364 | * the first item being the `Error` instance caught. If successful, 365 | * second item will be the result: `Promise<[Error, any]>` 366 | * 367 | * @example Using async/await (recommended) 368 | * import { safe as jsonc } from 'jsonc'; 369 | * (async () => { 370 | * const [err, obj] = await jsonc.read('path/to/file.json'); 371 | * if (err) { 372 | * console.log('Failed to read JSON file'); 373 | * } catch (err) { 374 | * console.log(typeof obj); // "object" 375 | * } 376 | * })(); 377 | * 378 | * @example Using promises 379 | * import { safe as jsonc } from 'jsonc'; 380 | * jsonc.read('path/to/file.json') 381 | * .then([err, obj] => { 382 | * if (err) { 383 | * console.log('Failed to read JSON file'); 384 | * } else { 385 | * console.log(typeof obj); // "object" 386 | * } 387 | * }) 388 | * // .catch(err => {}); // this is never invoked when safe version is used. 389 | */ 390 | static read(filePath: string, options?: IReadOptions): Promise<[Error, undefined] | [null, any]> { 391 | return safeAsync(jsonc.read(filePath, options)); 392 | } 393 | 394 | /** 395 | * Safe version of `jsonc.readSync()`. Synchronously reads a JSON file, 396 | * strips UTF-8 BOM and parses the JSON content. 397 | * @name jsonc.safe.readSync 398 | * @function 399 | * 400 | * @param {string} filePath - Path to JSON file. 401 | * @param {Function|IReadOptions} [options] - Read options. 402 | * @param {Function} [options.reviver] - A function that can filter and 403 | * transform the results. It receives each of the keys and values, and 404 | * its return value is used instead of the original value. If it 405 | * returns what it received, then the structure is not modified. If it 406 | * returns undefined then the member is deleted. 407 | * @param {boolean} [options.stripComments=true] - Whether to strip 408 | * comments from the JSON string. Note that it will fail if this is 409 | * set to `false` and the string includes comments. 410 | * 411 | * @returns {Array} - Safe methods return an array with 412 | * the first item being the `Error` instance caught. If successful, 413 | * second item will be the result: `[Error, any]` 414 | * 415 | * @example 416 | * import { safe as jsonc } from 'jsonc'; 417 | * const [err, obj] = jsonc.readSync('path/to/file.json'); 418 | * if (!err) console.log(typeof obj); // "object" 419 | */ 420 | static readSync(filePath: string, options?: IReadOptions): [Error, undefined] | [null, any] { 421 | return safeSync(jsonc.readSync)(filePath, options); 422 | } 423 | 424 | /** 425 | * Safe version of `jsonc.write()`. Asynchronously writes a JSON file 426 | * from the given JavaScript object. 427 | * @name jsonc.safe.write 428 | * @function 429 | * 430 | * @param {string} filePath - Path to JSON file to be written. 431 | * @param {any} data - Data to be stringified into JSON. 432 | * @param {IWriteOptions} [options] - Write options. 433 | * @param {Replacer} [options.replacer] - Determines how object values 434 | * are stringified for objects. It can be a function or an array of 435 | * strings. 436 | * @param {string|number} [options.space] - Specifies the indentation 437 | * of nested structures. If it is omitted, the text will be packed 438 | * without extra whitespace. If it is a number, it will specify the 439 | * number of spaces to indent at each level. If it is a string (such as 440 | * "\t" or " "), it contains the characters used to indent at each 441 | * level. 442 | * @param {number} [options.mode=438] - FileSystem permission mode to 443 | * be used when writing the file. Default is `438` (`0666` in octal). 444 | * @param {boolean} [options.autoPath=true] - Specifies whether to 445 | * create path directories if they don't exist. This will throw if set 446 | * to `false` and path does not exist. 447 | * 448 | * @returns {Promise} - Safe methods return an array with the 449 | * first item being the `Error` instance caught. If successful, 450 | * second item will be the result: `Promise<[Error, boolean]>` 451 | * 452 | * @example Using async/await (recommended) 453 | * import { safe as jsonc } from 'jsonc'; 454 | * (async () => { 455 | * const [err, success] = await jsonc.write('path/to/file.json', data); 456 | * if (err) { 457 | * console.log('Failed to read JSON file'); 458 | * } else { 459 | * console.log('Successfully wrote JSON file'); 460 | * } 461 | * })(); 462 | * 463 | * @example Using promises 464 | * import { safe as jsonc } from 'jsonc'; 465 | * jsonc.write('path/to/file.json', data) 466 | * .then([err, obj] => { 467 | * if (err) { 468 | * console.log('Failed to read JSON file'); 469 | * } else { 470 | * console.log('Successfully wrote JSON file'); 471 | * } 472 | * }) 473 | * // .catch(err => {}); // this is never invoked when safe version is used. 474 | */ 475 | static write(filePath: string, data: any, options?: IWriteOptions): Promise<[Error, undefined] | [null, boolean]> { 476 | return safeAsync(jsonc.write(filePath, data, options)); 477 | } 478 | 479 | /** 480 | * Safe version of `jsonc.writeSync()`. Synchronously writes a JSON 481 | * file from the given JavaScript object. 482 | * @name jsonc.safe.writeSync 483 | * @function 484 | * 485 | * @param {string} filePath - Path to JSON file to be written. 486 | * @param {any} data - Data to be stringified into JSON. 487 | * @param {IWriteOptions} [options] - Write options. 488 | * @param {Replacer} [options.replacer] - Determines how object values 489 | * are stringified for objects. It can be a function or an array of 490 | * strings. 491 | * @param {string|number} [options.space] - Specifies the indentation 492 | * of nested structures. If it is omitted, the text will be packed 493 | * without extra whitespace. If it is a number, it will specify the 494 | * number of spaces to indent at each level. If it is a string (such as 495 | * "\t" or " "), it contains the characters used to indent at each 496 | * level. 497 | * @param {number} [options.mode=438] - FileSystem permission mode to 498 | * be used when writing the file. Default is `438` (`0666` in octal). 499 | * @param {boolean} [options.autoPath=true] - Specifies whether to 500 | * create path directories if they don't exist. This will throw if set 501 | * to `false` and path does not exist. 502 | * 503 | * @returns {Array} - Safe methods return an array with the 504 | * first item being the `Error` instance caught. If successful, second 505 | * item will be the result: `[Error, boolean]` 506 | * 507 | * @example 508 | * import { safe as jsonc } from 'jsonc'; 509 | * const [err, obj] = jsonc.writeSync('path/to/file.json'); 510 | * if (!err) console.log(typeof obj); // "object" 511 | */ 512 | static writeSync(filePath: string, data: any, options?: IWriteOptions): [Error, undefined] | [null, boolean] { 513 | return safeSync(jsonc.writeSync)(filePath, data, options); 514 | } 515 | } 516 | 517 | // Export 518 | 519 | export { jsoncSafe }; 520 | -------------------------------------------------------------------------------- /src/jsonc.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:class-name no-require-imports no-default-export max-line-length interface-name max-classes-per-file max-file-line-count */ 2 | 3 | // core modules 4 | import * as path from 'path'; 5 | 6 | // dep modules 7 | import fastSafeStringify from 'fast-safe-stringify'; 8 | import parseJson = require('parse-json'); 9 | import stripBOM = require('strip-bom'); 10 | import * as stripJsonComments from 'strip-json-comments'; 11 | 12 | // own modules 13 | import { helper } from './helper'; 14 | import { 15 | IConfig, IParseOptions, IReadOptions, IStringifyOptions, IWriteOptions, Replacer, Reviver 16 | } from './interfaces'; 17 | import { jsoncSafe } from './jsonc.safe'; 18 | 19 | // constants, variables 20 | const { fs, mkdirp, promise } = helper; 21 | 22 | /** 23 | * JSON utility class that can handle comments and circular references; and 24 | * other extra functionality. 25 | * @class 26 | * @author Onur Yıldırım 27 | * @license MIT 28 | * @see {@link https://github.com/onury/jsonc|GitHub Repo} 29 | * @see {@link https://github.com/onury/jsonc#related-modules|Related Modules} 30 | * 31 | * @example 32 | * const jsonc = require('jsonc'); 33 | * // or 34 | * import { jsonc } from 'jsonc'; 35 | * 36 | * const result = jsonc.parse('// comments\n{ "key": "value" }'); 37 | * console.log(result); // { key: "value" } 38 | */ 39 | class jsonc { 40 | 41 | /** @private */ 42 | private static _: any; 43 | 44 | /** 45 | * Configures `jsonc` object. 46 | * 47 | * @param {IConfig} cfg - Configurations. 48 | * @param {NodeJS.WriteStream} [stream] - Stream to write logs to. This is 49 | * used with `.log()` and `.logp()` methods. 50 | * @param {NodeJS.WriteStream} [streamErr] - Stream to write error logs to. 51 | * This is used with `.log()` and `.logp()` methods. 52 | * 53 | * @example 54 | * // Output logs to stdout but logs containing errors to a file. 55 | * jsonc.config({ 56 | * stream: process.stdout, 57 | * streamErr: fs.createWriteStream('path/to/log.txt') 58 | * }); 59 | * jsonc.log({ info: 'this is logged to console' }); 60 | * jsonc.log(new Error('this is logged to file')); 61 | */ 62 | static config(cfg: IConfig): void { 63 | const conf = { 64 | stream: process.stdout, 65 | streamErr: process.stderr, 66 | ...(cfg || {}) 67 | }; 68 | jsonc._ = { 69 | logger: helper.getLogger(conf, false), 70 | prettyLogger: helper.getLogger(conf, true) 71 | }; 72 | } 73 | 74 | /** 75 | * Stringifies and logs the given arguments to console. This will 76 | * automatically handle circular references; so it won't throw. 77 | * 78 | * If an `Error` instance is passed, it will log the `.stack` property on 79 | * the instance, without stringifying the object. 80 | * 81 | * @param {...any[]} [args] - Arguments to be logged. 82 | * @returns {void} 83 | */ 84 | static log(...args: any[]): void { 85 | jsonc._.logger(...args); 86 | } 87 | 88 | /** 89 | * Pretty version of `log()` method. Stringifies and logs the given 90 | * arguments to console, with indents. This will automatically handle 91 | * circular references; so it won't throw. 92 | * 93 | * If an `Error` instance is passed, it will log the `.stack` property on 94 | * the instance, without stringifying the object. 95 | * 96 | * @param {...any[]} [args] - Arguments to be logged. 97 | * @returns {void} 98 | */ 99 | static logp(...args: any[]): void { 100 | jsonc._.prettyLogger(...args); 101 | } 102 | 103 | /** 104 | * Parses the given JSON string into a JavaScript object. The input string 105 | * can include comments. 106 | * 107 | * @param {string} str - JSON string to be parsed. 108 | * @param {IParseOptions|Reviver} [options] - Either a parse options 109 | * object or a reviver function. 110 | * @param {Reviver} [options.reviver] - A function that can filter 111 | * and transform the results. It receives each of the keys and values, and 112 | * its return value is used instead of the original value. If it returns 113 | * what it received, then the structure is not modified. If it returns 114 | * `undefined` then the member is deleted. 115 | * @param {Boolean} [options.stripComments=true] - Whether to strip 116 | * comments from the JSON string. Note that it will throw if this is set to 117 | * `false` and the string includes comments. 118 | * 119 | * @returns {any} - Parsed value. 120 | * 121 | * @throws {JSONError} - If JSON string is not valid. Note that any 122 | * comments within JSON are removed by default; so this will not throw for 123 | * comments unless you explicitly set `stripComments` to `false`. 124 | * 125 | * @example 126 | * const parsed = jsonc.parse('// comments\n{"success":true}\n'); 127 | * console.log(parsed); // { success: true } 128 | */ 129 | static parse(str: string, options ?: IParseOptions | Reviver): any { 130 | const opts: IParseOptions = typeof options === 'function' 131 | ? { reviver: options } 132 | : (options || {}); 133 | if (opts.stripComments !== false) str = stripJsonComments(str, { whitespace: false }); 134 | return parseJson(str, opts.reviver); 135 | } 136 | 137 | /** 138 | * Outputs a JSON string from the given JavaScript object. 139 | * 140 | * @param {*} value - JavaScript value to be stringified. 141 | * @param {IStringifyOptions|Replacer} [options] - Stringify options or a 142 | * replacer. 143 | * @param {Replacer} [options.replacer] - Determines how object values are 144 | * stringified for objects. It can be a function or an array of strings or 145 | * numbers. 146 | * @param {string|number} [options.space] - Specifies the indentation of 147 | * nested structures. If it is omitted, the text will be packed without 148 | * extra whitespace. If it is a number, it will specify the number of 149 | * spaces to indent at each level. If it is a string (such as `"\t"` or 150 | * `" "`), it contains the characters used to indent at each level. 151 | * @param {string|number} [space] - This takes effect if second argument is 152 | * the `replacer` or a falsy value. This is for supporting the signature of 153 | * native `JSON.stringify()` method. 154 | * @param {boolean} [options.handleCircular=true] - Whether to handle 155 | * circular references (if any) by replacing their values with the string 156 | * `"[Circular]"`. You can also use a replacer function to replace or 157 | * remove circular references instead. 158 | * 159 | * @returns {string} - JSON string. 160 | * 161 | * @throws {Error} - If there are any circular references within the 162 | * original input. In this case, use `jsonc.safe.stringify()` method 163 | * instead. 164 | * 165 | * @example 166 | * const obj = { key: 'value' }; 167 | * console.log(jsonc.stringify(obj)); // '{"key":"value"}' 168 | * 169 | * // pretty output with indents 170 | * let pretty = jsonc.stringify(obj, null, 2); 171 | * // equivalent to: 172 | * pretty = jsonc.stringify(obj, { reviver: null, space: 2 }); 173 | * if (!err) console.log(pretty); 174 | * // { 175 | * // "key": "value" 176 | * // } 177 | */ 178 | static stringify(value: any, optionsOrReplacer?: IStringifyOptions | Replacer, space?: string | number): string { 179 | const opts = helper.getStringifyOptions(optionsOrReplacer, space); 180 | return opts.handleCircular 181 | ? fastSafeStringify(value, opts.replacer, opts.space) 182 | : JSON.stringify(value, opts.replacer, opts.space); 183 | } 184 | 185 | /** 186 | * Specifies whether the given string has well-formed JSON structure. 187 | * 188 | * Note that, not all JSON-parsable strings are considered well-formed JSON 189 | * structures. JSON is built on two structures; a collection of name/value 190 | * pairs (object) or an ordered list of values (array). 191 | * 192 | * For example, `JSON.parse('true')` will parse successfully but 193 | * `jsonc.isJSON('true')` will return `false` since it has no object or 194 | * array structure. 195 | * 196 | * @param {string} str - String to be validated. 197 | * @param {boolean} [allowComments=false] - Whether comments should be 198 | * considered valid. 199 | * 200 | * @returns {boolean} 201 | * 202 | * @example 203 | * jsonc.isJSON('{"x":1}'); // true 204 | * jsonc.isJSON('true'); // false 205 | * jsonc.isJSON('[1, false, null]'); // true 206 | * jsonc.isJSON('string'); // false 207 | * jsonc.isJSON('null'); // false 208 | */ 209 | static isJSON(str: string, allowComments: boolean = false): boolean { 210 | if (typeof str !== 'string') return false; 211 | const [err, result] = jsonc.safe.parse(str, { stripComments: allowComments }); 212 | return !err && (helper.isObject(result) || Array.isArray(result)); 213 | } 214 | 215 | /** 216 | * Strips comments from the given JSON string. 217 | * 218 | * @param {string} str - JSON string. 219 | * @param {boolean} [whitespace=false] - Whether to replace comments with 220 | * whitespace instead of stripping them entirely. 221 | * 222 | * @returns {string} - Valid JSON string. 223 | * 224 | * @example 225 | * const str = jsonc.stripComments('// comments\n{"key":"value"}'); 226 | * console.log(str); // '\n{"key":"value"}' 227 | */ 228 | static stripComments(str: string, whitespace: boolean = false): string { 229 | return stripJsonComments(str, { whitespace }); 230 | } 231 | 232 | /** 233 | * Uglifies the given JSON string. 234 | * 235 | * @param {string} str - JSON string to be uglified. 236 | * @returns {string} - Uglified JSON string. 237 | * 238 | * @example 239 | * const pretty = ` 240 | * { 241 | * // comments... 242 | * "key": "value" 243 | * } 244 | * `; 245 | * const ugly = jsonc.uglify(pretty); 246 | * console.log(ugly); // '{"key":"value"}' 247 | */ 248 | static uglify(str: string): string { 249 | return jsonc.stringify(jsonc.parse(str, { stripComments: true })); 250 | } 251 | 252 | /** 253 | * Beautifies the given JSON string. Note that this will remove comments, 254 | * if any. 255 | * 256 | * @param {string} str - JSON string to be beautified. 257 | * @param {string|number} [space=2] Specifies the indentation of nested 258 | * structures. If it is omitted, the text will be packed without extra 259 | * whitespace. If it is a number, it will specify the number of spaces to 260 | * indent at each level. If it is a string (such as "\t" or " "), it 261 | * contains the characters used to indent at each level. 262 | * 263 | * @returns {string} - Beautified JSON string. 264 | * 265 | * @example 266 | * const ugly = '{"key":"value"}'; 267 | * const pretty = jsonc.beautify(ugly); 268 | * console.log(pretty); 269 | * // { 270 | * // "key": "value" 271 | * // } 272 | */ 273 | static beautify(str: string, space: string | number = 2): string { 274 | if (!space) space = 2; 275 | return jsonc.stringify(jsonc.parse(str), { space }); 276 | } 277 | 278 | /** 279 | * Normalizes the given value by stringifying and parsing it back to a 280 | * Javascript object. 281 | * 282 | * @param {any} value 283 | * @param {Replacer} [replacer] - Determines how object values are 284 | * normalized for objects. It can be a function or an array of strings. 285 | * 286 | * @returns {any} - Normalized object. 287 | * 288 | * @example 289 | * const c = new SomeClass(); 290 | * console.log(c.constructor.name); // "SomeClass" 291 | * const normalized = jsonc.normalize(c); 292 | * console.log(normalized.constructor.name); // "Object" 293 | */ 294 | static normalize(value: any, replacer?: Replacer): any { 295 | return jsonc.parse(jsonc.stringify(value, { replacer })); 296 | } 297 | 298 | /** 299 | * Asynchronously reads a JSON file, strips comments and UTF-8 BOM and 300 | * parses the JSON content. 301 | * 302 | * @param {string} filePath - Path to JSON file. 303 | * @param {Function|IReadOptions} [options] - Read options. 304 | * @param {Function} [options.reviver] - A function that can filter and 305 | * transform the results. It receives each of the keys and values, and its 306 | * return value is used instead of the original value. If it returns what 307 | * it received, then the structure is not modified. If it returns undefined 308 | * then the member is deleted. 309 | * @param {boolean} [options.stripComments=true] - Whether to strip 310 | * comments from the JSON string. Note that it will throw if this is set to 311 | * `false` and the string includes comments. 312 | * 313 | * @returns {Promise} - Promise of the parsed JSON content as a 314 | * JavaScript object. 315 | * 316 | * @example Using async/await (async () => {try {const 317 | * obj = await jsonc.read('path/to/file.json'); console.log(typeof obj); // 318 | * "object"} catch (err) {console.log('Failed to read JSON file'); 319 | * } 320 | * })(); 321 | * 322 | * @example Using promises 323 | * jsonc.read('path/to/file.json') .then(obj => {console.log(typeof obj); 324 | * // "object" 325 | * }) 326 | * .catch(err => { 327 | * console.log('Failed to read JSON file'); 328 | * }); 329 | */ 330 | static async read(filePath: string, options?: IReadOptions): Promise { 331 | const opts: IReadOptions = { 332 | reviver: null, 333 | stripComments: true, 334 | ...(options || {}) 335 | }; 336 | let data: string = await promise.readFile(filePath, 'utf8'); 337 | if (opts.stripComments !== false) data = stripJsonComments(data); 338 | return parseJson(stripBOM(data), opts.reviver, filePath); 339 | } 340 | 341 | /** 342 | * Synchronously reads a JSON file, strips UTF-8 BOM and parses the JSON 343 | * content. 344 | * 345 | * @param {string} filePath - Path to JSON file. 346 | * @param {Function|IReadOptions} [options] - Read options. 347 | * @param {Function} [options.reviver] - A function that can filter and 348 | * transform the results. It receives each of the keys and values, and its 349 | * return value is used instead of the original value. If it returns what 350 | * it received, then the structure is not modified. If it returns undefined 351 | * then the member is deleted. 352 | * @param {boolean} [options.stripComments=true] - Whether to strip 353 | * comments from the JSON string. Note that it will throw if this is set to 354 | * `false` and the string includes comments. 355 | * 356 | * @returns {any} - Parsed JSON content as a JavaScript object. 357 | * 358 | * @example 359 | * const obj = jsonc.readSync('path/to/file.json'); 360 | * // use try/catch block to handle errors. or better, use the safe version. 361 | * console.log(typeof obj); // "object" 362 | */ 363 | static readSync(filePath: string, options?: IReadOptions): any { 364 | const opts: IReadOptions = { 365 | reviver: null, 366 | stripComments: true, 367 | ...(options || {}) 368 | }; 369 | let data: string = fs.readFileSync(filePath, 'utf8'); 370 | if (opts.stripComments !== false) data = stripJsonComments(data); 371 | return parseJson(stripBOM(data), opts.reviver, filePath); 372 | } 373 | 374 | /** 375 | * Asynchronously writes a JSON file from the given JavaScript object. 376 | * 377 | * @param {string} filePath - Path to JSON file to be written. 378 | * @param {any} data - Data to be stringified into JSON. 379 | * @param {IWriteOptions} [options] - Write options. 380 | * @param {Replacer} [options.replacer] - Determines how object values are 381 | * stringified for objects. It can be a function or an array of strings. 382 | * @param {string|number} [options.space] - Specifies the indentation of 383 | * nested structures. If it is omitted, the text will be packed without 384 | * extra whitespace. If it is a number, it will specify the number of 385 | * spaces to indent at each level. If it is a string (such as "\t" or 386 | * " "), it contains the characters used to indent at each level. 387 | * @param {number} [options.mode=438] - FileSystem permission mode to be used when 388 | * writing the file. Default is `438` (`0666` in octal). 389 | * @param {boolean} [options.autoPath=true] - Specifies whether to create path 390 | * directories if they don't exist. This will throw if set to `false` and 391 | * path does not exist. 392 | * 393 | * @returns {Promise} - Always resolves with `true`, if no errors occur. 394 | * 395 | * @example Using async/await 396 | * (async () => { 397 | * try { 398 | * await jsonc.write('path/to/file.json', data); 399 | * console.log('Successfully wrote JSON file'); 400 | * } catch (err) { 401 | * console.log('Failed to write JSON file'); 402 | * } 403 | * })(); 404 | * 405 | * @example Using promises 406 | * jsonc.write('path/to/file.json', data) 407 | * .then(success => { 408 | * console.log('Successfully wrote JSON file'); 409 | * }) 410 | * .catch(err => { 411 | * console.log('Failed to write JSON file'); 412 | * }); 413 | */ 414 | static async write(filePath: string, data: any, options?: IWriteOptions): Promise { 415 | const opts: IWriteOptions = { 416 | replacer: null, 417 | space: 0, 418 | mode: 438, 419 | autoPath: true, 420 | ...(options || {}) 421 | }; 422 | 423 | if (opts.autoPath) await promise.mkdirp(path.dirname(filePath), { fs }); 424 | const content = JSON.stringify(data, opts.replacer, opts.space); 425 | await promise.writeFile(filePath, `${content}\n`, { 426 | mode: opts.mode, 427 | encoding: 'utf8' 428 | }); 429 | return true; 430 | } 431 | 432 | /** 433 | * Synchronously writes a JSON file from the given JavaScript object. 434 | * 435 | * @param {string} filePath - Path to JSON file to be written. 436 | * @param {any} data - Data to be stringified into JSON. 437 | * @param {IWriteOptions} [options] - Write options. 438 | * @param {Replacer} [options.replacer] - Determines how object values are 439 | * stringified for objects. It can be a function or an array of strings. 440 | * @param {string|number} [options.space] - Specifies the indentation of 441 | * nested structures. If it is omitted, the text will be packed without 442 | * extra whitespace. If it is a number, it will specify the number of 443 | * spaces to indent at each level. If it is a string (such as "\t" or 444 | * " "), it contains the characters used to indent at each level. 445 | * @param {number} [options.mode=438] - FileSystem permission mode to be used when 446 | * writing the file. Default is `438` (`0666` in octal). 447 | * @param {boolean} [options.autoPath=true] - Specifies whether to create path 448 | * directories if they don't exist. This will throw if set to `false` and 449 | * path does not exist. 450 | * 451 | * @returns {boolean} - Always returns `true`, if no errors occur. 452 | * 453 | * @example 454 | * const success = jsonc.writeSync('path/to/file.json'); 455 | * // this will always return true. use try/catch block to handle errors. or better, use the safe version. 456 | * console.log('Successfully wrote JSON file'); 457 | */ 458 | static writeSync(filePath: string, data: any, options?: IWriteOptions): boolean { 459 | const opts: IWriteOptions = { 460 | replacer: null, 461 | space: 0, 462 | mode: 438, 463 | autoPath: true, 464 | ...(options || {}) 465 | }; 466 | 467 | if (opts.autoPath) mkdirp.sync(path.dirname(filePath), { fs }); 468 | const content = JSON.stringify(data, opts.replacer, opts.space); 469 | fs.writeFileSync(filePath, `${content}\n`, { 470 | mode: opts.mode, 471 | encoding: 'utf8' 472 | }); 473 | return true; 474 | } 475 | 476 | } 477 | 478 | // default configuration 479 | jsonc.config(null); 480 | 481 | /* istanbul ignore next */ 482 | namespace jsonc { 483 | export const safe = jsoncSafe; 484 | } 485 | 486 | // Export 487 | 488 | export { jsonc }; 489 | -------------------------------------------------------------------------------- /test/helpers/streamLog.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-require-imports */ 2 | 3 | // core modules 4 | import * as path from 'path'; 5 | 6 | // dep modules 7 | import * as fs from 'graceful-fs'; 8 | import * as mkdirp from 'mkdirp'; 9 | 10 | // @ts-ignore 11 | const rimraf = require('rimraf'); 12 | 13 | export const streamLog = (jsonc: any, logMethod: string, msgList: any[]) => { 14 | 15 | const tmpDir = path.join(__dirname, '..', 'tmp'); 16 | mkdirp.sync(tmpDir); 17 | const fpath = path.join(tmpDir, 'log-test.txt'); 18 | let writableStream: any = fs.createWriteStream(fpath); 19 | jsonc.config({ 20 | stream: writableStream, 21 | streamErr: writableStream 22 | }); 23 | 24 | return new Promise((resolve, reject) => { 25 | // eslint-disable-next-line prefer-const 26 | let readableStream; 27 | let data; 28 | 29 | function destroy(): void { 30 | try { 31 | if (readableStream) { 32 | readableStream.removeAllListeners(); 33 | readableStream.close(); 34 | readableStream = null; 35 | } 36 | if (writableStream) { 37 | writableStream.removeAllListeners(); 38 | writableStream.close(); 39 | writableStream = null; 40 | } 41 | rimraf.sync(tmpDir); 42 | } catch (err) { } 43 | } 44 | 45 | /* istanbul ignore next */ 46 | function onError(err: Error): void { 47 | destroy(); 48 | reject(err); 49 | } 50 | 51 | function onReadbleEnd(): void { 52 | destroy(); 53 | setTimeout(() => resolve(data), 300); 54 | } 55 | 56 | function onWritableReady(): void { 57 | jsonc[logMethod](...msgList); 58 | readableStream = fs.createReadStream(fpath) 59 | .on('error', onError) 60 | .on('readable', () => { 61 | let buffer; 62 | // tslint:disable-next-line:no-conditional-assignment 63 | while (buffer = readableStream.read()) { 64 | data = buffer.toString(); 65 | } 66 | readableStream.on('end', onReadbleEnd); 67 | }); 68 | } 69 | 70 | // 'ready' event is introduced in Node.js v9.11.0 so we use 'open' event. 71 | writableStream 72 | .on('error', onError) 73 | .on('open', onWritableReady); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /test/jsonc.spec.ts: -------------------------------------------------------------------------------- 1 | // core modules 2 | import * as path from 'path'; 3 | 4 | // dep modules 5 | import * as fs from 'graceful-fs'; 6 | 7 | // own modules 8 | import { IParseOptions, IReadOptions, IWriteOptions } from '../src/interfaces'; 9 | import { jsonc } from '../src/jsonc'; 10 | import { streamLog } from './helpers/streamLog'; 11 | 12 | function _removeDirRecursiveSync(dirPath: string): void { 13 | if (fs.existsSync(dirPath)) { 14 | fs.readdirSync(dirPath).forEach(file => { 15 | const curPath = path.join(dirPath, file); 16 | if (fs.statSync(curPath).isDirectory()) { 17 | _removeDirRecursiveSync(curPath); 18 | } else { 19 | fs.unlinkSync(curPath); 20 | } 21 | }); 22 | fs.rmdirSync(dirPath); 23 | } 24 | } 25 | 26 | describe('jsonc', () => { 27 | 28 | const validJsonWithComments = ` 29 | // comments will be stripped... 30 | { 31 | "some": /* special */ "property", 32 | "value": 1 // don't change this!!! 33 | } 34 | `; 35 | 36 | const invalidJson = '[invalid JSON}'; 37 | 38 | class MyClass { 39 | a: number; 40 | b: string; 41 | constructor() { 42 | this.a = 1; 43 | this.b = 'prop'; 44 | } 45 | } 46 | 47 | test('.parse()', () => { 48 | // throw on comments 49 | const opts: IParseOptions = { stripComments: false }; 50 | expect(() => jsonc.parse(validJsonWithComments, opts)).toThrow(); 51 | 52 | // strip comments and parse 53 | let result = jsonc.parse(validJsonWithComments); 54 | expect(typeof result).toEqual('object'); 55 | expect(result.value).toEqual(1); 56 | 57 | const reviver = (key: string, value: any) => { 58 | return key === 'some' ? 'modified ' + value : value; 59 | }; 60 | 61 | result = jsonc.parse(validJsonWithComments, reviver); 62 | expect(typeof result).toEqual('object'); 63 | expect(result.some).toEqual('modified property'); 64 | 65 | result = jsonc.parse(validJsonWithComments, { reviver }); 66 | expect(typeof result).toEqual('object'); 67 | expect(result.some).toEqual('modified property'); 68 | }); 69 | 70 | test('.safe.parse()', () => { 71 | let [err, result] = jsonc.safe.parse(validJsonWithComments); 72 | // jsonc.stringify() 73 | expect(typeof result).toEqual('object'); 74 | expect(result.value).toEqual(1); 75 | 76 | [err, result] = jsonc.safe.parse(invalidJson); 77 | expect(err instanceof Error).toEqual(true); 78 | expect(result).toBeUndefined(); 79 | 80 | expect(() => jsonc.parse(invalidJson)).toThrow(); 81 | }); 82 | 83 | test('.stringify()', () => { 84 | const o = { a: 1, b: 'text' }; 85 | expect(typeof jsonc.stringify(o)).toEqual('string'); 86 | 87 | const expected = '{\n "a": 1,\n "b": "text"\n}'; 88 | let result = jsonc.stringify(o, null, 2); 89 | expect(result).toEqual(expected); 90 | result = jsonc.stringify(o, { space: 2 }); 91 | expect(result).toEqual(expected); 92 | 93 | const replacer = (key: string, value: any) => { 94 | return key === 'b' ? 'modified ' + value : value; 95 | }; 96 | result = jsonc.stringify(o, replacer, 2); 97 | expect(result).toEqual('{\n "a": 1,\n "b": "modified text"\n}'); 98 | result = jsonc.stringify(o, { replacer, space: 2 }); 99 | expect(result).toEqual('{\n "a": 1,\n "b": "modified text"\n}'); 100 | 101 | const x = { a: 1, b: 'text' }; 102 | (x as any).y = x; // circular 103 | expect(() => jsonc.stringify(x, { handleCircular: false })).toThrow(); 104 | }); 105 | 106 | test('.safe.stringify()', () => { 107 | const x = { a: 1, b: 'text' }; 108 | (x as any).y = x; // circular 109 | expect(() => jsonc.safe.stringify(x)).not.toThrow(); 110 | expect(() => jsonc.safe.stringify(x, { handleCircular: false })).not.toThrow(); 111 | 112 | let [err, result] = jsonc.safe.stringify(x); 113 | expect(err).toEqual(null); 114 | expect(typeof result).toEqual('string'); 115 | 116 | const o = { 117 | get error(): any { 118 | throw new Error(); 119 | } 120 | }; 121 | [err, result] = jsonc.safe.stringify(o); 122 | expect(err instanceof Error).toEqual(true); 123 | }); 124 | 125 | test('.isJSON()', () => { 126 | expect(jsonc.isJSON(5 as any)).toEqual(false); 127 | expect(jsonc.isJSON({} as any)).toEqual(false); 128 | expect(jsonc.isJSON('true')).toEqual(false); 129 | expect(jsonc.isJSON('1')).toEqual(false); 130 | expect(jsonc.isJSON('null')).toEqual(false); 131 | expect(jsonc.isJSON('[null]')).toEqual(true); 132 | expect(jsonc.isJSON('{}')).toEqual(true); 133 | expect(jsonc.isJSON('// comments\n{"x":/*test*/1}')).toEqual(false); 134 | expect(jsonc.isJSON('// comments\n{"x":/*test*/1}', true)).toEqual(true); 135 | }); 136 | 137 | test('.safe.isJSON()', () => { 138 | expect(jsonc.safe.isJSON(5 as any)).toEqual(false); 139 | expect(jsonc.safe.isJSON({} as any)).toEqual(false); 140 | expect(jsonc.safe.isJSON('true')).toEqual(false); 141 | expect(jsonc.safe.isJSON('1')).toEqual(false); 142 | expect(jsonc.safe.isJSON('null')).toEqual(false); 143 | expect(jsonc.safe.isJSON('[null]')).toEqual(true); 144 | expect(jsonc.safe.isJSON('{}')).toEqual(true); 145 | expect(jsonc.safe.isJSON('// comments\n{"x":/*test*/1}')).toEqual(false); 146 | expect(jsonc.safe.isJSON('// comments\n{"x":/*test*/1}', true)).toEqual(true); 147 | }); 148 | 149 | test('.stripComments()', () => { 150 | expect(jsonc.stripComments('// comments\n{"x":/*test*/1}')).toEqual('\n{"x":1}'); 151 | expect(jsonc.stripComments('// comments\n{"x":/*test*/1}', true)).toEqual(' \n{"x": 1}'); 152 | }); 153 | 154 | test('.safe.stripComments()', () => { 155 | expect(jsonc.safe.stripComments('// comments\n{"x":/*test*/1}')[1]).toEqual('\n{"x":1}'); 156 | expect(jsonc.safe.stripComments('// comments\n{"x":/*test*/1}', true)[1]).toEqual(' \n{"x": 1}'); 157 | }); 158 | 159 | test('.uglify()', () => { 160 | expect(jsonc.uglify(validJsonWithComments)).toEqual('{"some":"property","value":1}'); 161 | }); 162 | 163 | test('.safe.uglify()', () => { 164 | expect(jsonc.safe.uglify(validJsonWithComments)[1]).toEqual('{"some":"property","value":1}'); 165 | }); 166 | 167 | test('.beautify()', () => { 168 | const ugly = jsonc.uglify(validJsonWithComments); 169 | const with2spaces = '{\n "some": "property",\n "value": 1\n}'; 170 | expect(jsonc.beautify(ugly)).toEqual(with2spaces); 171 | expect(jsonc.beautify(ugly, 0)).toEqual(with2spaces); 172 | expect(jsonc.beautify(ugly)).toEqual('{\n "some": "property",\n "value": 1\n}'); 173 | expect(jsonc.beautify(ugly, 4)).toEqual('{\n "some": "property",\n "value": 1\n}'); 174 | }); 175 | 176 | test('.safe.beautify()', () => { 177 | const [err, ugly] = jsonc.safe.uglify(validJsonWithComments); 178 | const with2spaces = '{\n "some": "property",\n "value": 1\n}'; 179 | expect(jsonc.safe.beautify(ugly as string)[1]).toEqual(with2spaces); 180 | expect(jsonc.safe.beautify(ugly as string, 0)[1]).toEqual(with2spaces); 181 | expect(jsonc.safe.beautify(ugly as string)[1]).toEqual('{\n "some": "property",\n "value": 1\n}'); 182 | expect(jsonc.safe.beautify(ugly as string, 4)[1]).toEqual('{\n "some": "property",\n "value": 1\n}'); 183 | }); 184 | 185 | test('.normalize()', () => { 186 | const mc = new MyClass(); 187 | let normalized = jsonc.normalize(mc); 188 | expect(mc.constructor.name).toEqual('MyClass'); // for convinience 189 | expect(normalized.constructor.name).toEqual('Object'); 190 | 191 | normalized = jsonc.normalize(mc, (key: string, value: any) => { 192 | return key === 'b' ? value + ' modified' : value; 193 | }); 194 | expect(normalized.constructor.name).toEqual('Object'); 195 | expect(normalized.b).toEqual('prop modified'); 196 | }); 197 | 198 | test('.safe.normalize()', () => { 199 | const mc = new MyClass(); 200 | let err; 201 | let normalized; 202 | [err, normalized] = jsonc.safe.normalize(mc); 203 | expect(mc.constructor.name).toEqual('MyClass'); // for convinience 204 | expect(normalized.constructor.name).toEqual('Object'); 205 | 206 | [err, normalized] = jsonc.safe.normalize(mc, (key: string, value: any) => { 207 | return key === 'b' ? value + ' modified' : value; 208 | }); 209 | expect(normalized.constructor.name).toEqual('Object'); 210 | expect(normalized.b).toEqual('prop modified'); 211 | }); 212 | 213 | const asyncFilePath = './test/tmp/test.json'; 214 | async function readWrite(data: any, writeOpts?: IWriteOptions, readOpts?: IReadOptions): Promise { 215 | await jsonc.write(asyncFilePath, data, writeOpts); 216 | return jsonc.read(asyncFilePath, readOpts); 217 | } 218 | 219 | test('.write() & .read()', async () => { 220 | expect.assertions(9); 221 | 222 | const data = { test: 'file', x: 1 }; 223 | 224 | let obj = await readWrite(data); 225 | expect(obj.test).toEqual('file'); 226 | expect(data.test).toEqual(obj.test); 227 | 228 | const readOpts: IReadOptions = { 229 | stripComments: false, 230 | reviver: (key: string, value: any) => key === 'x' ? 5 : value 231 | }; 232 | obj = await readWrite(data, null, readOpts); 233 | expect(obj.test).toEqual('file'); 234 | expect(obj.x).toEqual(5); 235 | expect(data.test).toEqual(obj.test); 236 | 237 | const writeOpts: IWriteOptions = { 238 | replacer: (key: string, value: any) => key === 'x' ? 5 : value, 239 | autoPath: true, 240 | space: 2 241 | }; 242 | obj = await readWrite(data, writeOpts); 243 | expect(obj.test).toEqual('file'); 244 | expect(obj.x).toEqual(5); 245 | expect(data.test).toEqual(obj.test); 246 | 247 | _removeDirRecursiveSync(path.dirname(asyncFilePath)); 248 | 249 | await expect(readWrite(data, { autoPath: false })).rejects.toThrow(); 250 | }); 251 | 252 | async function safeReadWrite(data: any, writeOpts?: IWriteOptions, readOpts?: IReadOptions): Promise { 253 | await jsonc.safe.write(asyncFilePath, data, writeOpts); 254 | return jsonc.safe.read(asyncFilePath, readOpts); 255 | } 256 | test('.safe.write() & .safe.read()', async () => { 257 | expect.assertions(12); 258 | 259 | const data = { test: 'file', x: 1 }; 260 | 261 | let [err, obj] = await safeReadWrite(data); 262 | expect(err).toEqual(null); 263 | expect(obj.test).toEqual('file'); 264 | expect(data.test).toEqual(obj.test); 265 | 266 | const readOpts: IReadOptions = { 267 | stripComments: false, 268 | reviver: (key: string, value: any) => key === 'x' ? 5 : value 269 | }; 270 | [err, obj] = await safeReadWrite(data, null, readOpts); 271 | expect(err).toEqual(null); 272 | expect(obj.test).toEqual('file'); 273 | expect(obj.x).toEqual(5); 274 | expect(data.test).toEqual(obj.test); 275 | 276 | const writeOpts: IWriteOptions = { 277 | replacer: (key: string, value: any) => key === 'x' ? 5 : value, 278 | autoPath: true, 279 | space: 2 280 | }; 281 | [err, obj] = await safeReadWrite(data, writeOpts); 282 | expect(err).toEqual(null); 283 | expect(obj.test).toEqual('file'); 284 | expect(obj.x).toEqual(5); 285 | expect(data.test).toEqual(obj.test); 286 | 287 | _removeDirRecursiveSync(path.dirname(asyncFilePath)); 288 | 289 | await expect(safeReadWrite(data, { autoPath: false })).resolves.not.toThrow(); 290 | }); 291 | 292 | const syncFilePath = './test/tmp/test-sync.json'; 293 | function readWriteSync(data: any, writeOpts?: IWriteOptions, readOpts?: IReadOptions): any { 294 | jsonc.writeSync(syncFilePath, data, writeOpts); 295 | return jsonc.readSync(syncFilePath, readOpts); 296 | } 297 | 298 | test('.writeSync() & .readSync()', () => { 299 | const data = { test: 'file', x: 1 }; 300 | 301 | let obj = readWriteSync(data); 302 | expect(obj.test).toEqual('file'); 303 | expect(data.test).toEqual(obj.test); 304 | 305 | const readOpts: IReadOptions = { 306 | stripComments: false, 307 | reviver: (key: string, value: any) => key === 'x' ? 5 : value 308 | }; 309 | obj = readWriteSync(data, null, readOpts); 310 | expect(obj.test).toEqual('file'); 311 | expect(obj.x).toEqual(5); 312 | expect(data.test).toEqual(obj.test); 313 | 314 | const writeOpts: IWriteOptions = { 315 | replacer: (key: string, value: any) => key === 'x' ? 5 : value, 316 | autoPath: true, 317 | space: 2 318 | }; 319 | obj = readWriteSync(data, writeOpts); 320 | expect(obj.test).toEqual('file'); 321 | expect(obj.x).toEqual(5); 322 | expect(data.test).toEqual(obj.test); 323 | 324 | _removeDirRecursiveSync(path.dirname(syncFilePath)); 325 | 326 | expect(() => readWriteSync(data, { autoPath: false })).toThrow(); 327 | }); 328 | 329 | function safeReadWriteSync(data: any, writeOpts?: IWriteOptions, readOpts?: IReadOptions): any { 330 | jsonc.safe.writeSync(syncFilePath, data, writeOpts); 331 | return jsonc.safe.readSync(syncFilePath, readOpts); 332 | } 333 | 334 | test('.safe.writeSync() & .safe.readSync()', () => { 335 | const data = { test: 'file', x: 1 }; 336 | 337 | let [err, obj] = safeReadWriteSync(data); 338 | expect(err).toEqual(null); 339 | expect(obj.test).toEqual('file'); 340 | expect(data.test).toEqual(obj.test); 341 | 342 | const readOpts: IReadOptions = { 343 | stripComments: false, 344 | reviver: (key: string, value: any) => key === 'x' ? 5 : value 345 | }; 346 | [err, obj] = safeReadWriteSync(data, null, readOpts); 347 | expect(err).toEqual(null); 348 | expect(obj.test).toEqual('file'); 349 | expect(obj.x).toEqual(5); 350 | expect(data.test).toEqual(obj.test); 351 | 352 | const writeOpts: IWriteOptions = { 353 | replacer: (key: string, value: any) => key === 'x' ? 5 : value, 354 | autoPath: true, 355 | space: 2 356 | }; 357 | [err, obj] = safeReadWriteSync(data, writeOpts); 358 | expect(err).toEqual(null); 359 | expect(obj.test).toEqual('file'); 360 | expect(obj.x).toEqual(5); 361 | expect(data.test).toEqual(obj.test); 362 | 363 | _removeDirRecursiveSync(path.dirname(syncFilePath)); 364 | 365 | expect(() => safeReadWriteSync(data, { autoPath: false })).not.toThrow(); 366 | }); 367 | 368 | test('.config() & .log() & .logp()', async () => { 369 | expect.assertions(8); 370 | 371 | expect(await streamLog(jsonc, 'log', [{ test: true }])).toMatch('{"test":true}'); // obj 372 | expect(await streamLog(jsonc, 'log', [[1, 2, 3]])).toMatch('[1,2,3]'); // array 373 | expect(await streamLog(jsonc, 'log', [true])).toMatch('true'); // primitive 374 | expect(await streamLog(jsonc, 'log', [new Error('logged error')])).toMatch('logged error'); // error 375 | 376 | expect(await streamLog(jsonc, 'logp', [{ test: true }])).toMatch('{\n "test": true\n}\n'); 377 | expect(await streamLog(jsonc, 'logp', [[1, 2, 3]])).toMatch('[\n 1,\n 2,\n 3\n]\n'); // array 378 | expect(await streamLog(jsonc, 'logp', [true])).toMatch('true'); // primitive 379 | expect(await streamLog(jsonc, 'logp', [new Error('logged error')])).toMatch('logged error'); // error 380 | }); 381 | 382 | test('.safe.config() & .safe.log() & .safe.logp()', async () => { 383 | expect.assertions(8); 384 | 385 | expect(await streamLog(jsonc.safe, 'log', [{ test: true }])).toMatch('{"test":true}'); // obj 386 | expect(await streamLog(jsonc.safe, 'log', [[1, 2, 3]])).toMatch('[1,2,3]'); // array 387 | expect(await streamLog(jsonc.safe, 'log', [true])).toMatch('true'); // primitive 388 | expect(await streamLog(jsonc.safe, 'log', [new Error('logged error')])).toMatch('logged error'); // error 389 | 390 | expect(await streamLog(jsonc.safe, 'logp', [{ test: true }])).toMatch('{\n "test": true\n}\n'); 391 | expect(await streamLog(jsonc.safe, 'logp', [[1, 2, 3]])).toMatch('[\n 1,\n 2,\n 3\n]\n'); // array 392 | expect(await streamLog(jsonc.safe, 'logp', [true])).toMatch('true'); // primitive 393 | expect(await streamLog(jsonc.safe, 'logp', [new Error('logged error')])).toMatch('logged error'); // error 394 | }); 395 | 396 | }); 397 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "./lib", 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "removeComments": false, 10 | "noLib": false, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "lib": ["es6", "es2015", "dom"], 14 | "sourceMap": false, 15 | "pretty": true, 16 | "allowUnreachableCode": false, 17 | "allowUnusedLabels": false, 18 | "allowJs": false, // cannot be true when decleration is true 19 | "noImplicitAny": false, 20 | "suppressImplicitAnyIndexErrors": true, 21 | "noImplicitReturns": true, 22 | "noImplicitUseStrict": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "typeRoots": [ 25 | "./node_modules/@types", 26 | "./node_modules" 27 | ], 28 | "types": [ 29 | "node" 30 | ], 31 | "outDir": "./lib" 32 | }, 33 | "include": [ 34 | "./src/**/*.ts" 35 | ], 36 | "exclude": [ 37 | "./node_modules", 38 | "./lib", 39 | "./dev", 40 | "./docs", 41 | "./test", 42 | "./tasks", 43 | "./backup" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | 4 | // ---------------------------- 5 | // TypeScript-specific 6 | // ---------------------------- 7 | 8 | "adjacent-overload-signatures": true, 9 | "ban-types": [true, ["Object", "Use {} instead."], ["String"]], 10 | "member-access": [true, "no-public"], 11 | "member-ordering": [true, {"order": "fields-first"}], 12 | "no-any": false, 13 | "no-empty-interface": false, 14 | "no-import-side-effect": true, 15 | "no-inferrable-types": [true, "ignore-params"], // "ignore-properties" 16 | "no-internal-module": true, 17 | "no-magic-numbers": [false], 18 | // "no-namespace": [true, "allow-declarations"], 19 | "no-non-null-assertion": true, 20 | "no-reference": true, 21 | // "no-unnecessary-type-assertion": true, // rule requires type information. 22 | "no-var-requires": true, 23 | "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], 24 | "prefer-for-of": true, 25 | // "promise-function-async": true, // rule requires type information 26 | "typedef": [true, "call-signature", "parameter", "member-variable-declaration"], 27 | "typedef-whitespace": [ 28 | true, 29 | { 30 | "call-signature": "nospace", 31 | "index-signature": "nospace", 32 | "parameter": "nospace", 33 | "property-declaration": "nospace", 34 | "variable-declaration": "nospace" 35 | }, 36 | { 37 | "call-signature": "onespace", 38 | "index-signature": "onespace", 39 | "parameter": "onespace", 40 | "property-declaration": "onespace", 41 | "variable-declaration": "onespace" 42 | } 43 | ], 44 | "unified-signatures": true, 45 | 46 | // ---------------------------- 47 | // Functionality 48 | // ---------------------------- 49 | 50 | // "await-promise": [true, "Thenable"], // rule requires type information 51 | // "ban": [], 52 | "curly": [true, "ignore-same-line"], 53 | "forin": true, 54 | // "import-blacklist": [true, "rxjs", "lodash"], 55 | "label-position": true, 56 | "no-arg": true, 57 | "no-bitwise": true, 58 | "no-conditional-assignment": true, 59 | // "no-console": [true, "log", "error"], 60 | "no-construct": true, 61 | "no-debugger": true, 62 | "no-duplicate-super": true, 63 | "no-duplicate-variable": [true, "check-parameters"], 64 | "no-empty": [true, "allow-empty-catch"], 65 | "no-eval": true, 66 | // "no-floating-promises": [true, "JQueryPromise"], // rule requires type information 67 | // "no-for-in-array": true, // rule requires type information 68 | // "no-inferred-empty-object-type": true, // rule requires type information 69 | "no-invalid-template-strings": true, 70 | "no-invalid-this": [true, "check-function-in-method"], 71 | "no-misused-new": true, 72 | "no-null-keyword": false, 73 | "no-object-literal-type-assertion": true, 74 | "no-shadowed-variable": true, 75 | "no-sparse-arrays": true, 76 | "no-string-literal": true, 77 | "no-string-throw": true, 78 | "no-switch-case-fall-through": true, 79 | "no-this-assignment": [true, {"allowed-names": ["^self$"], "allow-destructuring": true}], 80 | // "no-unbound-method": [true, "ignore-static"], // rule requires type information 81 | // "no-unsafe-any": true, // rule requires type information 82 | "no-unsafe-finally": true, 83 | "no-unused-expression": [true, "allow-fast-null-checks"], 84 | // "no-unused-variable": [true, {"ignore-pattern": "^_"}], // rule requires type information 85 | // "no-use-before-declare": true, // rule requires type information 86 | "no-var-keyword": true, 87 | // "no-void-expression": [true, "ignore-arrow-function-shorthand"], // rule requires type information 88 | "prefer-conditional-expression": true, 89 | "prefer-object-spread": true, 90 | "radix": true, 91 | // "restrict-plus-operands": true, // rule requires type information 92 | // "strict-boolean-expressions": [true, "allow-boolean-or-undefined"], // rule requires type information 93 | // "strict-type-predicates": true, // does not work without --strictNullChecks // rule requires type information 94 | "switch-default": true, 95 | "triple-equals": true, 96 | // "use-default-type-parameter": true, // rule requires type information 97 | "use-isnan": true, 98 | 99 | // ---------------------------- 100 | // Maintainability 101 | // ---------------------------- 102 | 103 | "cyclomatic-complexity": [true, 30], 104 | // "deprecation": true, // rule requires type information 105 | "eofline": true, 106 | "linebreak-style": [true, "LF"], 107 | "max-classes-per-file": [true, 1], 108 | "max-file-line-count": [true, 750], 109 | "max-line-length": [true, 135], 110 | "no-default-export": true, 111 | "no-mergeable-namespace": true, 112 | "no-require-imports": true, 113 | "object-literal-sort-keys": [false, "ignore-case"], 114 | "prefer-const": [true, {"destructuring": "all"}], 115 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], 116 | 117 | // ---------------------------- 118 | // Style 119 | // ---------------------------- 120 | 121 | "align": [true, "parameters", "statements"], 122 | "array-type": [true, "array"], 123 | "arrow-parens": [true, "ban-single-arg-parens"], 124 | "arrow-return-shorthand": [true], 125 | "binary-expression-operand-order": true, 126 | "callable-types": true, 127 | "class-name": true, 128 | "comment-format": [true, "check-space", {"ignore-words": ["TODO", "HACK", "NOTE"]}], 129 | // "completed-docs": [true, "enums", "enum-members", "functions", "methods", "properties", "interfaces", "classes"], // rule requires type information 130 | "encoding": true, 131 | // "file-header": [false], 132 | "import-spacing": true, 133 | "interface-name": [true, "always-prefix"], 134 | "interface-over-type-literal": true, 135 | "jsdoc-format": true, 136 | // "match-default-export-name": true, // rule requires type information 137 | // "newline-before-return": true, 138 | "new-parens": true, 139 | "no-angle-bracket-type-assertion": true, 140 | // "no-boolean-literal-compare": true, // rule requires type information 141 | "no-consecutive-blank-lines": [true, 1], 142 | "no-irregular-whitespace": true, 143 | "no-parameter-properties": true, 144 | "no-reference-import": true, 145 | "no-trailing-whitespace": [true, "ignore-jsdoc"], 146 | "no-unnecessary-callback-wrapper": true, 147 | "no-unnecessary-initializer": true, 148 | // "no-unnecessary-qualifier": true, // rule requires type information 149 | "number-literal-format": true, 150 | "object-literal-key-quotes": [true, "as-needed"], 151 | "object-literal-shorthand": true, 152 | "one-line": [false], 153 | "one-variable-per-declaration": [true, "ignore-for-loop"], 154 | "ordered-imports": [true], 155 | "prefer-function-over-method": [true, "allow-protected", "allow-public"], 156 | "prefer-method-signature": true, 157 | "prefer-switch": [true, {"min-cases": 4}], 158 | // "prefer-template": [true, "allow-single-concat"], 159 | "quotemark": [true, "single", "avoid-escape", "avoid-template"], 160 | // "return-undefined": true, // rule requires type information 161 | "semicolon": [true, "always"], 162 | "space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}], 163 | "switch-final-break": true, 164 | "type-literal-delimiter": true, 165 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 166 | "whitespace": [ 167 | true, 168 | "check-branch", 169 | "check-decl", 170 | "check-operator", 171 | "check-separator" 172 | ] 173 | } 174 | } 175 | --------------------------------------------------------------------------------