@angular/cli
Version:
CLI tool for Angular
179 lines (178 loc) • 6.25 kB
JavaScript
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.JSONFile = void 0;
exports.readAndParseJson = readAndParseJson;
exports.parseJson = parseJson;
const jsonc_parser_1 = require("jsonc-parser");
const node_fs_1 = require("node:fs");
const eol_1 = require("./eol");
const error_1 = require("./error");
/**
* Represents a JSON file, allowing for reading, modifying, and saving.
* This class uses `jsonc-parser` to preserve comments and formatting, including
* indentation and end-of-line sequences.
* @internal
*/
class JSONFile {
/** The raw content of the JSON file. */
#content;
/** The end-of-line sequence used in the file. */
#eol;
/** Whether the file uses spaces for indentation. */
#insertSpaces = true;
/** The number of spaces or tabs used for indentation. */
#tabSize = 2;
/** The path to the JSON file. */
#path;
/** The parsed JSON abstract syntax tree. */
#jsonAst;
/** The raw content of the JSON file. */
get content() {
return this.#content;
}
/**
* Creates an instance of JSONFile.
* @param path The path to the JSON file.
*/
constructor(path) {
this.#path = path;
try {
this.#content = (0, node_fs_1.readFileSync)(this.#path, 'utf-8');
}
catch (e) {
(0, error_1.assertIsError)(e);
// We don't have to worry about ENOENT, since we'll be creating the file.
if (e.code !== 'ENOENT') {
throw e;
}
this.#content = '';
}
this.#eol = (0, eol_1.getEOL)(this.#content);
this.#detectIndentation();
}
/**
* Gets the parsed JSON abstract syntax tree.
* The AST is lazily parsed and cached.
*/
get JsonAst() {
if (this.#jsonAst) {
return this.#jsonAst;
}
const errors = [];
this.#jsonAst = (0, jsonc_parser_1.parseTree)(this.#content, errors, { allowTrailingComma: true });
if (errors.length) {
formatError(this.#path, errors);
}
return this.#jsonAst;
}
/**
* Gets a value from the JSON file at a specific path.
* @param jsonPath The path to the value.
* @returns The value at the given path, or `undefined` if not found.
*/
get(jsonPath) {
const jsonAstNode = this.JsonAst;
if (!jsonAstNode) {
return undefined;
}
if (jsonPath.length === 0) {
return (0, jsonc_parser_1.getNodeValue)(jsonAstNode);
}
const node = (0, jsonc_parser_1.findNodeAtLocation)(jsonAstNode, jsonPath);
return node === undefined ? undefined : (0, jsonc_parser_1.getNodeValue)(node);
}
/**
* Modifies a value in the JSON file.
* @param jsonPath The path to the value to modify.
* @param value The new value to insert.
* @param insertInOrder A function to determine the insertion index, or `false` to insert at the end.
* @returns `true` if the modification was successful, `false` otherwise.
*/
modify(jsonPath, value, insertInOrder) {
if (value === undefined && this.get(jsonPath) === undefined) {
// Cannot remove a value which doesn't exist.
return false;
}
let getInsertionIndex;
if (insertInOrder === undefined) {
const property = jsonPath.slice(-1)[0];
getInsertionIndex = (properties) => [...properties, property].sort().findIndex((p) => p === property);
}
else if (insertInOrder !== false) {
getInsertionIndex = insertInOrder;
}
const edits = (0, jsonc_parser_1.modify)(this.#content, jsonPath, value, {
getInsertionIndex,
formattingOptions: {
insertSpaces: this.#insertSpaces,
tabSize: this.#tabSize,
eol: this.#eol,
},
});
if (edits.length === 0) {
return false;
}
this.#content = (0, jsonc_parser_1.applyEdits)(this.#content, edits);
this.#jsonAst = undefined;
return true;
}
/**
* Deletes a value from the JSON file at a specific path.
* @param jsonPath The path to the value to delete.
* @returns `true` if the deletion was successful, `false` otherwise.
*/
delete(jsonPath) {
return this.modify(jsonPath, undefined);
}
/** Saves the modified content back to the file. */
save() {
(0, node_fs_1.writeFileSync)(this.#path, this.#content);
}
/** Detects the indentation of the file. */
#detectIndentation() {
// Find the first line that has indentation.
const match = this.#content.match(/^(?:( )+|\t+)\S/m);
if (match) {
this.#insertSpaces = !!match[1];
this.#tabSize = match[0].length - 1;
}
}
}
exports.JSONFile = JSONFile;
/**
* Reads and parses a JSON file, supporting comments and trailing commas.
* @param path The path to the JSON file.
* @returns The parsed JSON object.
*/
function readAndParseJson(path) {
const errors = [];
const content = (0, jsonc_parser_1.parse)((0, node_fs_1.readFileSync)(path, 'utf-8'), errors, { allowTrailingComma: true });
if (errors.length) {
formatError(path, errors);
}
return content;
}
/**
* Formats a JSON parsing error and throws an exception.
* @param path The path to the file that failed to parse.
* @param errors The list of parsing errors.
*/
function formatError(path, errors) {
const { error, offset } = errors[0];
throw new Error(`Failed to parse "${path}" as JSON AST Object. ${(0, jsonc_parser_1.printParseErrorCode)(error)} at location: ${offset}.`);
}
/**
* Parses a JSON string, supporting comments and trailing commas.
* @param content The JSON string to parse.
* @returns The parsed JSON object.
*/
function parseJson(content) {
return (0, jsonc_parser_1.parse)(content, undefined, { allowTrailingComma: true });
}