426 lines
17 KiB
JavaScript
426 lines
17 KiB
JavaScript
"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
|