"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonFile = exports.JsonSyntax = void 0; const os = __importStar(require("os")); const jju = __importStar(require("jju")); const Text_1 = require("./Text"); const FileSystem_1 = require("./FileSystem"); /** * Specifies the variant of JSON syntax to be used. * * @public */ var JsonSyntax; (function (JsonSyntax) { /** * Specifies the exact RFC 8259 format as implemented by the `JSON.parse()` system API. * This format was designed for machine generated inputs such as an HTTP payload. * It is not a recommend choice for human-authored files, because it does not support * code comments. * * @remarks * * A well-known quote from Douglas Crockford, the inventor of JSON: * * "I removed comments from JSON because I saw people were using them to hold parsing directives, * a practice which would have destroyed interoperability. I know that the lack of comments makes * some people sad, but it shouldn't. Suppose you are using JSON to keep configuration files, * which you would like to annotate. Go ahead and insert all the comments you like. * Then pipe it through JSMin before handing it to your JSON parser." * * @see {@link https://datatracker.ietf.org/doc/html/rfc8259 | RFC 8259} */ JsonSyntax["Strict"] = "strict"; /** * `JsonSyntax.JsonWithComments` is the recommended format for human-authored config files. * It is a minimal extension to `JsonSyntax.Strict` adding support for code comments * using `//` and `/*`. * * @remarks * * VS Code calls this format `jsonc`, but it should not be confused with unrelated file formats * and libraries that also use the name "JSONC". * * To fix VS Code syntax highlighting, add this setting: * `"files.associations": { "*.json": "jsonc" }` * * To fix GitHub syntax highlighting, add this to your `.gitattributes`: * `*.json linguist-language=JSON-with-Comments` */ JsonSyntax["JsonWithComments"] = "jsonWithComments"; /** * JSON5 is a project that proposes a JSON-like format supplemented with ECMAScript 5.1 * notations for objects, numbers, comments, and more. * * @remarks * Files using this format should use the `.json5` file extension instead of `.json`. * * JSON5 has substantial differences from JSON: object keys may be unquoted, trailing commas * are allowed, and strings may span multiple lines. Whereas `JsonSyntax.JsonWithComments` can * be cheaply converted to standard JSON by stripping comments, parsing JSON5 requires a * nontrivial algorithm that may not be easily available in some contexts or programming languages. * * @see {@link https://json5.org/ | JSON5 project website} */ JsonSyntax["Json5"] = "json5"; })(JsonSyntax = exports.JsonSyntax || (exports.JsonSyntax = {})); const DEFAULT_ENCODING = 'utf8'; /** * Utilities for reading/writing JSON files. * @public */ class JsonFile { /** * Loads a JSON file. */ static load(jsonFilename, options) { try { const contents = FileSystem_1.FileSystem.readFile(jsonFilename); const parseOptions = JsonFile._buildJjuParseOptions(options); return jju.parse(contents, parseOptions); } catch (error) { if (FileSystem_1.FileSystem.isNotExistError(error)) { throw error; } else { throw new Error(`Error reading "${JsonFile._formatPathForError(jsonFilename)}":` + os.EOL + ` ${error.message}`); } } } /** * An async version of {@link JsonFile.load}. */ static async loadAsync(jsonFilename, options) { try { const contents = await FileSystem_1.FileSystem.readFileAsync(jsonFilename); const parseOptions = JsonFile._buildJjuParseOptions(options); return jju.parse(contents, parseOptions); } catch (error) { if (FileSystem_1.FileSystem.isNotExistError(error)) { throw error; } else { throw new Error(`Error reading "${JsonFile._formatPathForError(jsonFilename)}":` + os.EOL + ` ${error.message}`); } } } /** * Parses a JSON file's contents. */ static parseString(jsonContents, options) { const parseOptions = JsonFile._buildJjuParseOptions(options); return jju.parse(jsonContents, parseOptions); } /** * Loads a JSON file and validate its schema. */ static loadAndValidate(jsonFilename, jsonSchema, options) { const jsonObject = JsonFile.load(jsonFilename, options); jsonSchema.validateObject(jsonObject, jsonFilename, options); return jsonObject; } /** * An async version of {@link JsonFile.loadAndValidate}. */ static async loadAndValidateAsync(jsonFilename, jsonSchema, options) { const jsonObject = await JsonFile.loadAsync(jsonFilename, options); jsonSchema.validateObject(jsonObject, jsonFilename, options); return jsonObject; } /** * Loads a JSON file and validate its schema, reporting errors using a callback * @remarks * See JsonSchema.validateObjectWithCallback() for more info. */ static loadAndValidateWithCallback(jsonFilename, jsonSchema, errorCallback, options) { const jsonObject = JsonFile.load(jsonFilename, options); jsonSchema.validateObjectWithCallback(jsonObject, errorCallback); return jsonObject; } /** * An async version of {@link JsonFile.loadAndValidateWithCallback}. */ static async loadAndValidateWithCallbackAsync(jsonFilename, jsonSchema, errorCallback, options) { const jsonObject = await JsonFile.loadAsync(jsonFilename, options); jsonSchema.validateObjectWithCallback(jsonObject, errorCallback); return jsonObject; } /** * Serializes the specified JSON object to a string buffer. * @param jsonObject - the object to be serialized * @param options - other settings that control serialization * @returns a JSON string, with newlines, and indented with two spaces */ static stringify(jsonObject, options) { return JsonFile.updateString('', jsonObject, options); } /** * Serializes the specified JSON object to a string buffer. * @param previousJson - the previous JSON string, which will be updated * @param newJsonObject - the object to be serialized * @param options - other settings that control serialization * @returns a JSON string, with newlines, and indented with two spaces */ static updateString(previousJson, newJsonObject, options) { if (!options) { options = {}; } if (!options.ignoreUndefinedValues) { // Standard handling of `undefined` in JSON stringification is to discard the key. JsonFile.validateNoUndefinedMembers(newJsonObject); } let stringified; if (previousJson !== '') { // NOTE: We don't use mode=json here because comments aren't allowed by strict JSON stringified = jju.update(previousJson, newJsonObject, { mode: 'cjson', indent: 2 }); } else if (options.prettyFormatting) { stringified = jju.stringify(newJsonObject, { mode: 'json', indent: 2 }); if (options.headerComment !== undefined) { stringified = JsonFile._formatJsonHeaderComment(options.headerComment) + stringified; } } else { stringified = JSON.stringify(newJsonObject, undefined, 2); if (options.headerComment !== undefined) { stringified = JsonFile._formatJsonHeaderComment(options.headerComment) + stringified; } } // Add the trailing newline stringified = Text_1.Text.ensureTrailingNewline(stringified); if (options && options.newlineConversion) { stringified = Text_1.Text.convertTo(stringified, options.newlineConversion); } return stringified; } /** * Saves the file to disk. Returns false if nothing was written due to options.onlyIfChanged. * @param jsonObject - the object to be saved * @param jsonFilename - the file path to write * @param options - other settings that control how the file is saved * @returns false if ISaveJsonFileOptions.onlyIfChanged didn't save anything; true otherwise */ static save(jsonObject, jsonFilename, options) { if (!options) { options = {}; } // Do we need to read the previous file contents? let oldBuffer = undefined; if (options.updateExistingFile || options.onlyIfChanged) { try { oldBuffer = FileSystem_1.FileSystem.readFileToBuffer(jsonFilename); } catch (error) { if (!FileSystem_1.FileSystem.isNotExistError(error)) { throw error; } } } let jsonToUpdate = ''; if (options.updateExistingFile && oldBuffer) { jsonToUpdate = oldBuffer.toString(DEFAULT_ENCODING); } const newJson = JsonFile.updateString(jsonToUpdate, jsonObject, options); const newBuffer = Buffer.from(newJson, DEFAULT_ENCODING); if (options.onlyIfChanged) { // Has the file changed? if (oldBuffer && Buffer.compare(newBuffer, oldBuffer) === 0) { // Nothing has changed, so don't touch the file return false; } } FileSystem_1.FileSystem.writeFile(jsonFilename, newBuffer, { ensureFolderExists: options.ensureFolderExists }); // TEST CODE: Used to verify that onlyIfChanged isn't broken by a hidden transformation during saving. /* const oldBuffer2: Buffer = FileSystem.readFileToBuffer(jsonFilename); if (Buffer.compare(buffer, oldBuffer2) !== 0) { console.log('new:' + buffer.toString('hex')); console.log('old:' + oldBuffer2.toString('hex')); throw new Error('onlyIfChanged logic is broken'); } */ return true; } /** * An async version of {@link JsonFile.save}. */ static async saveAsync(jsonObject, jsonFilename, options) { if (!options) { options = {}; } // Do we need to read the previous file contents? let oldBuffer = undefined; if (options.updateExistingFile || options.onlyIfChanged) { try { oldBuffer = await FileSystem_1.FileSystem.readFileToBufferAsync(jsonFilename); } catch (error) { if (!FileSystem_1.FileSystem.isNotExistError(error)) { throw error; } } } let jsonToUpdate = ''; if (options.updateExistingFile && oldBuffer) { jsonToUpdate = oldBuffer.toString(DEFAULT_ENCODING); } const newJson = JsonFile.updateString(jsonToUpdate, jsonObject, options); const newBuffer = Buffer.from(newJson, DEFAULT_ENCODING); if (options.onlyIfChanged) { // Has the file changed? if (oldBuffer && Buffer.compare(newBuffer, oldBuffer) === 0) { // Nothing has changed, so don't touch the file return false; } } await FileSystem_1.FileSystem.writeFileAsync(jsonFilename, newBuffer, { ensureFolderExists: options.ensureFolderExists }); // TEST CODE: Used to verify that onlyIfChanged isn't broken by a hidden transformation during saving. /* const oldBuffer2: Buffer = await FileSystem.readFileToBufferAsync(jsonFilename); if (Buffer.compare(buffer, oldBuffer2) !== 0) { console.log('new:' + buffer.toString('hex')); console.log('old:' + oldBuffer2.toString('hex')); throw new Error('onlyIfChanged logic is broken'); } */ return true; } /** * Used to validate a data structure before writing. Reports an error if there * are any undefined members. */ static validateNoUndefinedMembers(jsonObject) { return JsonFile._validateNoUndefinedMembers(jsonObject, []); } // Private implementation of validateNoUndefinedMembers() static _validateNoUndefinedMembers(jsonObject, keyPath) { if (!jsonObject) { return; } if (typeof jsonObject === 'object') { for (const key of Object.keys(jsonObject)) { keyPath.push(key); // eslint-disable-next-line @typescript-eslint/no-explicit-any const value = jsonObject[key]; if (value === undefined) { const fullPath = JsonFile._formatKeyPath(keyPath); throw new Error(`The value for ${fullPath} is "undefined" and cannot be serialized as JSON`); } JsonFile._validateNoUndefinedMembers(value, keyPath); keyPath.pop(); } } } // Given this input: ['items', '4', 'syntax', 'parameters', 'string "with" symbols", 'type'] // Return this string: items[4].syntax.parameters["string \"with\" symbols"].type static _formatKeyPath(keyPath) { let result = ''; for (const key of keyPath) { if (/^[0-9]+$/.test(key)) { // It's an integer, so display like this: parent[123] result += `[${key}]`; } else if (/^[a-z_][a-z_0-9]*$/i.test(key)) { // It's an alphanumeric identifier, so display like this: parent.name if (result) { result += '.'; } result += `${key}`; } else { // It's a freeform string, so display like this: parent["A path: \"C:\\file\""] // Convert this: A path: "C:\file" // To this: A path: \"C:\\file\" const escapedKey = key .replace(/[\\]/g, '\\\\') // escape backslashes .replace(/["]/g, '\\'); // escape quotes result += `["${escapedKey}"]`; } } return result; } static _formatJsonHeaderComment(headerComment) { if (headerComment === '') { return ''; } const lines = headerComment.split('\n'); const result = []; for (const line of lines) { if (!/^\s*$/.test(line) && !/^\s*\/\//.test(line)) { throw new Error('The headerComment lines must be blank or start with the "//" prefix.\n' + 'Invalid line' + JSON.stringify(line)); } result.push(Text_1.Text.replaceAll(line, '\r', '')); } return lines.join('\n') + '\n'; } static _buildJjuParseOptions(options) { if (!options) { options = {}; } const parseOptions = {}; switch (options.jsonSyntax) { case JsonSyntax.Strict: parseOptions.mode = 'json'; break; case JsonSyntax.JsonWithComments: parseOptions.mode = 'cjson'; break; case JsonSyntax.Json5: default: parseOptions.mode = 'json5'; break; } return parseOptions; } } /** * @internal */ JsonFile._formatPathForError = (path) => path; exports.JsonFile = JsonFile; //# sourceMappingURL=JsonFile.js.map