utils/node_modules/@microsoft/tsdoc-config/lib/TSDocConfigFile.js
2024-02-07 01:33:07 -05:00

683 lines
28 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (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.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TSDocConfigFile = void 0;
const tsdoc_1 = require("@microsoft/tsdoc");
const fs = __importStar(require("fs"));
const resolve = __importStar(require("resolve"));
const path = __importStar(require("path"));
const ajv_1 = __importDefault(require("ajv"));
const jju = __importStar(require("jju"));
const ajv = new ajv_1.default({ verbose: true });
function initializeSchemaValidator() {
const jsonSchemaPath = resolve.sync('@microsoft/tsdoc/schemas/tsdoc.schema.json', { basedir: __dirname });
const jsonSchemaContent = fs.readFileSync(jsonSchemaPath).toString();
const jsonSchema = jju.parse(jsonSchemaContent, { mode: 'cjson' });
return ajv.compile(jsonSchema);
}
// Warning: AJV has a fairly strange API. Each time this function is called, the function object's
// properties get overwritten with the results of the latest validation. Thus we need to be careful
// to read the properties before a subsequent call may occur.
const tsdocSchemaValidator = initializeSchemaValidator();
/**
* Represents an individual `tsdoc.json` file.
*
* @public
*/
class TSDocConfigFile {
constructor() {
this.log = new tsdoc_1.ParserMessageLog();
this._extendsFiles = [];
this._filePath = '';
this._fileNotFound = false;
this._hasErrors = false;
this._fileMTime = 0;
this._tsdocSchema = '';
this._extendsPaths = [];
this._noStandardTags = undefined;
this._tagDefinitions = [];
this._tagDefinitionNames = new Set();
this._supportForTags = new Map();
}
/**
* Other config files that this file extends from.
*/
get extendsFiles() {
return this._extendsFiles;
}
/**
* The full path of the file that was attempted to load, or an empty string if the configuration was
* loaded from a source that is not a file.
*/
get filePath() {
return this._filePath;
}
/**
* If true, then the TSDocConfigFile object contains an empty state, because the `tsdoc.json` file
* was not found by the loader.
*
* @remarks
* A missing "tsdoc.json" file is not considered an error. It simply means that the defaults will be used.
*/
get fileNotFound() {
return this._fileNotFound;
}
/**
* If true, then at least one error was encountered while loading this file or one of its "extends" files.
*
* @remarks
* You can use {@link TSDocConfigFile.getErrorSummary} to report these errors.
*
* The individual messages can be retrieved from the {@link TSDocConfigFile.log} property of each `TSDocConfigFile`
* object (including the {@link TSDocConfigFile.extendsFiles} tree).
*/
get hasErrors() {
return this._hasErrors;
}
/**
* The `$schema` field from the `tsdoc.json` file.
*/
get tsdocSchema() {
return this._tsdocSchema;
}
/**
* The `extends` field from the `tsdoc.json` file. For the parsed file contents,
* use the `extendsFiles` property instead.
*/
get extendsPaths() {
return this._extendsPaths;
}
/**
* By default, the config file loader will predefine all of the standardized TSDoc tags. To disable this and
* start with a completely empty configuration, set `noStandardTags` to true.
*
* @remarks
* If a config file uses `"extends"` to include settings from base config files, then its setting will
* override any settings from the base config files. If `"noStandardTags"` is not specified, then this
* property will be `undefined`. The config files are applied in the order they are processed (a depth-first
* traversal of the `"extends"` references), and files processed later can override earlier files.
* If no config file specifies `noStandardTags` then the default value is `false`.
*/
get noStandardTags() {
return this._noStandardTags;
}
set noStandardTags(value) {
this._noStandardTags = value;
}
get tagDefinitions() {
return this._tagDefinitions;
}
get supportForTags() {
return this._supportForTags;
}
get supportedHtmlElements() {
return this._supportedHtmlElements && Array.from(this._supportedHtmlElements);
}
get reportUnsupportedHtmlElements() {
return this._reportUnsupportedHtmlElements;
}
set reportUnsupportedHtmlElements(value) {
this._reportUnsupportedHtmlElements = value;
}
/**
* Removes all items from the `tagDefinitions` array.
*/
clearTagDefinitions() {
this._tagDefinitions.length = 0;
this._tagDefinitionNames.clear();
}
/**
* Adds a new item to the `tagDefinitions` array.
*/
addTagDefinition(parameters) {
// This validates the tag name
const tagDefinition = new tsdoc_1.TSDocTagDefinition(parameters);
if (this._tagDefinitionNames.has(tagDefinition.tagNameWithUpperCase)) {
throw new Error(`A tag definition was already added with the tag name "${parameters.tagName}"`);
}
this._tagDefinitionNames.add(tagDefinition.tagName);
this._tagDefinitions.push(tagDefinition);
}
// Similar to addTagDefinition() but reports errors using _reportError()
_addTagDefinitionForLoad(parameters) {
let tagDefinition;
try {
// This validates the tag name
tagDefinition = new tsdoc_1.TSDocTagDefinition(parameters);
}
catch (error) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileInvalidTagName,
messageText: error.message,
textRange: tsdoc_1.TextRange.empty,
});
return;
}
if (this._tagDefinitionNames.has(tagDefinition.tagNameWithUpperCase)) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileDuplicateTagName,
messageText: `The "tagDefinitions" field specifies more than one tag with the name "${parameters.tagName}"`,
textRange: tsdoc_1.TextRange.empty,
});
}
this._tagDefinitionNames.add(tagDefinition.tagNameWithUpperCase);
this._tagDefinitions.push(tagDefinition);
}
/**
* Adds a new item to the `supportedHtmlElements` array.
*/
addSupportedHtmlElement(htmlElement) {
if (!this._supportedHtmlElements) {
this._supportedHtmlElements = new Set();
}
this._supportedHtmlElements.add(htmlElement);
}
/**
* Removes the explicit list of allowed html elements.
*/
clearSupportedHtmlElements() {
this._supportedHtmlElements = undefined;
}
/**
* Removes all entries from the "supportForTags" map.
*/
clearSupportForTags() {
this._supportForTags.clear();
}
/**
* Sets an entry in the "supportForTags" map.
*/
setSupportForTag(tagName, supported) {
tsdoc_1.TSDocTagDefinition.validateTSDocTagName(tagName);
this._supportForTags.set(tagName, supported);
}
/**
* This can be used for cache eviction. It returns true if the modification timestamp has changed for
* any of the files that were read when loading this `TSDocConfigFile`, which indicates that the file should be
* reloaded. It does not consider cases where `TSDocConfigFile.fileNotFound` was `true`.
*
* @remarks
* This can be used for cache eviction. An example eviction strategy might be like this:
*
* - call `checkForModifiedFiles()` once per second, and reload the configuration if it returns true
*
* - otherwise, reload the configuration when it is more than 10 seconds old (to handle less common cases such
* as creation of a missing file, or creation of a file at an earlier location in the search path).
*/
checkForModifiedFiles() {
if (this._checkForModifiedFile()) {
return true;
}
for (const extendsFile of this.extendsFiles) {
if (extendsFile.checkForModifiedFiles()) {
return true;
}
}
return false;
}
/**
* Checks the last modification time for `TSDocConfigFile.filePath` and returns `true` if it has changed
* since the file was loaded. If the file is missing, this returns `false`. If the timestamp cannot be read,
* then this returns `true`.
*/
_checkForModifiedFile() {
if (this._fileNotFound || !this._filePath) {
return false;
}
try {
const mtimeMs = fs.statSync(this._filePath).mtimeMs;
return mtimeMs !== this._fileMTime;
}
catch (error) {
return true;
}
}
_reportError(parserMessageParameters) {
this.log.addMessage(new tsdoc_1.ParserMessage(parserMessageParameters));
this._hasErrors = true;
}
_loadJsonObject(configJson) {
if (configJson.$schema !== TSDocConfigFile.CURRENT_SCHEMA_URL) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileUnsupportedSchema,
messageText: `Unsupported JSON "$schema" value; expecting "${TSDocConfigFile.CURRENT_SCHEMA_URL}"`,
textRange: tsdoc_1.TextRange.empty,
});
return;
}
const success = tsdocSchemaValidator(configJson);
if (!success) {
const description = ajv.errorsText(tsdocSchemaValidator.errors);
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileSchemaError,
messageText: 'Error loading config file: ' + description,
textRange: tsdoc_1.TextRange.empty,
});
return;
}
this._tsdocSchema = configJson.$schema;
if (configJson.extends) {
this._extendsPaths.push(...configJson.extends);
}
this.noStandardTags = configJson.noStandardTags;
for (const jsonTagDefinition of configJson.tagDefinitions || []) {
let syntaxKind;
switch (jsonTagDefinition.syntaxKind) {
case 'inline':
syntaxKind = tsdoc_1.TSDocTagSyntaxKind.InlineTag;
break;
case 'block':
syntaxKind = tsdoc_1.TSDocTagSyntaxKind.BlockTag;
break;
case 'modifier':
syntaxKind = tsdoc_1.TSDocTagSyntaxKind.ModifierTag;
break;
default:
// The JSON schema should have caught this error
throw new Error('Unexpected tag kind');
}
this._addTagDefinitionForLoad({
tagName: jsonTagDefinition.tagName,
syntaxKind: syntaxKind,
allowMultiple: jsonTagDefinition.allowMultiple,
});
}
if (configJson.supportedHtmlElements) {
this._supportedHtmlElements = new Set();
for (const htmlElement of configJson.supportedHtmlElements) {
this.addSupportedHtmlElement(htmlElement);
}
}
this._reportUnsupportedHtmlElements = configJson.reportUnsupportedHtmlElements;
if (configJson.supportForTags) {
for (const tagName of Object.keys(configJson.supportForTags)) {
const supported = configJson.supportForTags[tagName];
this._supportForTags.set(tagName, supported);
}
}
}
_loadWithExtends(configFilePath, referencingConfigFile, alreadyVisitedPaths) {
// In case an exception is thrown, start by assuming that the file was not found; we'll revise
// this later upon success
this._fileNotFound = true;
if (!configFilePath) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileNotFound,
messageText: 'File not found',
textRange: tsdoc_1.TextRange.empty,
});
return;
}
this._filePath = path.resolve(configFilePath);
if (!fs.existsSync(this._filePath)) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileNotFound,
messageText: 'File not found',
textRange: tsdoc_1.TextRange.empty,
});
return;
}
const configJsonContent = fs.readFileSync(this._filePath).toString();
this._fileMTime = fs.statSync(this._filePath).mtimeMs;
this._fileNotFound = false;
const hashKey = fs.realpathSync(this._filePath);
if (referencingConfigFile && alreadyVisitedPaths.has(hashKey)) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileCyclicExtends,
messageText: `Circular reference encountered for "extends" field of "${referencingConfigFile.filePath}"`,
textRange: tsdoc_1.TextRange.empty,
});
return;
}
alreadyVisitedPaths.add(hashKey);
let configJson;
try {
configJson = jju.parse(configJsonContent, { mode: 'cjson' });
}
catch (e) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigInvalidJson,
messageText: 'Error parsing JSON input: ' + e.message,
textRange: tsdoc_1.TextRange.empty,
});
return;
}
this._loadJsonObject(configJson);
const configFileFolder = path.dirname(this.filePath);
for (const extendsField of this.extendsPaths) {
let resolvedExtendsPath;
try {
resolvedExtendsPath = resolve.sync(extendsField, { basedir: configFileFolder });
}
catch (e) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileUnresolvedExtends,
messageText: `Unable to resolve "extends" reference to "${extendsField}": ` + e.message,
textRange: tsdoc_1.TextRange.empty,
});
return;
}
const baseConfigFile = new TSDocConfigFile();
baseConfigFile._loadWithExtends(resolvedExtendsPath, this, alreadyVisitedPaths);
if (baseConfigFile.fileNotFound) {
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileUnresolvedExtends,
messageText: `Unable to resolve "extends" reference to "${extendsField}"`,
textRange: tsdoc_1.TextRange.empty,
});
}
this._extendsFiles.push(baseConfigFile);
if (baseConfigFile.hasErrors) {
this._hasErrors = true;
}
}
}
/**
* For the given folder, look for the relevant tsdoc.json file (if any), and return its path.
*
* @param folderPath - the path to a folder where the search should start
* @returns the (possibly relative) path to tsdoc.json, or an empty string if not found
*/
static findConfigPathForFolder(folderPath) {
if (folderPath) {
let foundFolder = folderPath;
for (;;) {
const tsconfigJsonPath = path.join(foundFolder, 'tsconfig.json');
if (fs.existsSync(tsconfigJsonPath)) {
// Stop when we reach a folder containing tsconfig.json
return path.join(foundFolder, TSDocConfigFile.FILENAME);
}
const packageJsonPath = path.join(foundFolder, 'package.json');
if (fs.existsSync(packageJsonPath)) {
// Stop when we reach a folder containing package.json; this avoids crawling out of the current package
return path.join(foundFolder, TSDocConfigFile.FILENAME);
}
const previousFolder = foundFolder;
foundFolder = path.dirname(foundFolder);
if (!foundFolder || foundFolder === previousFolder) {
// Give up if we reach the filesystem root directory
break;
}
}
}
return '';
}
/**
* Calls `TSDocConfigFile.findConfigPathForFolder()` to find the relevant tsdoc.json config file, if one exists.
* Then calls `TSDocConfigFile.findConfigPathForFolder()` to return the loaded result.
*
* @remarks
* This API does not report loading errors by throwing exceptions. Instead, the caller is expected to check
* for errors using {@link TSDocConfigFile.hasErrors}, {@link TSDocConfigFile.log},
* or {@link TSDocConfigFile.getErrorSummary}.
*
* @param folderPath - the path to a folder where the search should start
*/
static loadForFolder(folderPath) {
const rootConfigPath = TSDocConfigFile.findConfigPathForFolder(folderPath);
return TSDocConfigFile.loadFile(rootConfigPath);
}
/**
* Loads the specified tsdoc.json and any base files that it refers to using the "extends" option.
*
* @remarks
* This API does not report loading errors by throwing exceptions. Instead, the caller is expected to check
* for errors using {@link TSDocConfigFile.hasErrors}, {@link TSDocConfigFile.log},
* or {@link TSDocConfigFile.getErrorSummary}.
*
* @param tsdocJsonFilePath - the path to the tsdoc.json config file
*/
static loadFile(tsdocJsonFilePath) {
const configFile = new TSDocConfigFile();
const alreadyVisitedPaths = new Set();
configFile._loadWithExtends(tsdocJsonFilePath, undefined, alreadyVisitedPaths);
return configFile;
}
/**
* Loads the object state from a JSON-serializable object as produced by {@link TSDocConfigFile.saveToObject}.
*
* @remarks
* The serialized object has the same structure as `tsdoc.json`; however the `"extends"` field is not allowed.
*
* This API does not report loading errors by throwing exceptions. Instead, the caller is expected to check
* for errors using {@link TSDocConfigFile.hasErrors}, {@link TSDocConfigFile.log},
* or {@link TSDocConfigFile.getErrorSummary}.
*/
static loadFromObject(jsonObject) {
const configFile = new TSDocConfigFile();
configFile._loadJsonObject(jsonObject);
if (configFile.extendsPaths.length > 0) {
throw new Error('The "extends" field cannot be used with TSDocConfigFile.loadFromObject()');
}
return configFile;
}
/**
* Initializes a TSDocConfigFile object using the state from the provided `TSDocConfiguration` object.
*
* @remarks
* This API does not report loading errors by throwing exceptions. Instead, the caller is expected to check
* for errors using {@link TSDocConfigFile.hasErrors}, {@link TSDocConfigFile.log},
* or {@link TSDocConfigFile.getErrorSummary}.
*/
static loadFromParser(configuration) {
const configFile = new TSDocConfigFile();
// The standard tags will be mixed together with custom definitions,
// so set noStandardTags=true to avoid defining them twice.
configFile.noStandardTags = true;
for (const tagDefinition of configuration.tagDefinitions) {
configFile.addTagDefinition({
syntaxKind: tagDefinition.syntaxKind,
tagName: tagDefinition.tagName,
allowMultiple: tagDefinition.allowMultiple,
});
}
for (const tagDefinition of configuration.supportedTagDefinitions) {
configFile.setSupportForTag(tagDefinition.tagName, true);
}
for (const htmlElement of configuration.supportedHtmlElements) {
configFile.addSupportedHtmlElement(htmlElement);
}
configFile.reportUnsupportedHtmlElements = configuration.validation.reportUnsupportedHtmlElements;
return configFile;
}
/**
* Writes the config file content to a JSON file with the specified file path.
*/
saveFile(jsonFilePath) {
const jsonObject = this.saveToObject();
const jsonContent = JSON.stringify(jsonObject, undefined, 2);
fs.writeFileSync(jsonFilePath, jsonContent);
}
/**
* Writes the object state into a JSON-serializable object.
*/
saveToObject() {
const configJson = {
$schema: TSDocConfigFile.CURRENT_SCHEMA_URL,
};
if (this.noStandardTags !== undefined) {
configJson.noStandardTags = this.noStandardTags;
}
if (this.tagDefinitions.length > 0) {
configJson.tagDefinitions = [];
for (const tagDefinition of this.tagDefinitions) {
configJson.tagDefinitions.push(TSDocConfigFile._serializeTagDefinition(tagDefinition));
}
}
if (this.supportForTags.size > 0) {
configJson.supportForTags = {};
this.supportForTags.forEach((supported, tagName) => {
configJson.supportForTags[tagName] = supported;
});
}
if (this.supportedHtmlElements) {
configJson.supportedHtmlElements = [...this.supportedHtmlElements];
}
if (this._reportUnsupportedHtmlElements !== undefined) {
configJson.reportUnsupportedHtmlElements = this._reportUnsupportedHtmlElements;
}
return configJson;
}
static _serializeTagDefinition(tagDefinition) {
let syntaxKind;
switch (tagDefinition.syntaxKind) {
case tsdoc_1.TSDocTagSyntaxKind.InlineTag:
syntaxKind = 'inline';
break;
case tsdoc_1.TSDocTagSyntaxKind.BlockTag:
syntaxKind = 'block';
break;
case tsdoc_1.TSDocTagSyntaxKind.ModifierTag:
syntaxKind = 'modifier';
break;
default:
throw new Error('Unimplemented TSDocTagSyntaxKind');
}
const tagConfigJson = {
tagName: tagDefinition.tagName,
syntaxKind,
};
if (tagDefinition.allowMultiple) {
tagConfigJson.allowMultiple = true;
}
return tagConfigJson;
}
/**
* Returns a report of any errors that occurred while attempting to load this file or any files
* referenced via the "extends" field.
*
* @remarks
* Use {@link TSDocConfigFile.hasErrors} to determine whether any errors occurred.
*/
getErrorSummary() {
if (!this._hasErrors) {
return 'No errors.';
}
let result = '';
if (this.log.messages.length > 0) {
const errorNoun = this.log.messages.length > 1 ? 'Errors' : 'Error';
if (this.filePath) {
result += `${errorNoun} encountered for ${this.filePath}:\n`;
}
else {
result += `${errorNoun} encountered when loading TSDoc configuration:\n`;
}
for (const message of this.log.messages) {
result += ` ${message.text}\n`;
}
}
for (const extendsFile of this.extendsFiles) {
if (extendsFile.hasErrors) {
if (result !== '') {
result += '\n';
}
result += extendsFile.getErrorSummary();
}
}
return result;
}
/**
* Applies the settings from this config file to a TSDoc parser configuration.
* Any `extendsFile` settings will also applied.
*
* @remarks
* Additional validation is performed during this operation. The caller is expected to check for errors
* using {@link TSDocConfigFile.hasErrors}, {@link TSDocConfigFile.log}, or {@link TSDocConfigFile.getErrorSummary}.
*/
configureParser(configuration) {
if (this._getNoStandardTagsWithExtends()) {
// Do not define standard tags
configuration.clear(true);
}
else {
// Define standard tags (the default behavior)
configuration.clear(false);
}
this.updateParser(configuration);
}
/**
* This is the same as {@link configureParser}, but it preserves any previous state.
*
* @remarks
* Additional validation is performed during this operation. The caller is expected to check for errors
* using {@link TSDocConfigFile.hasErrors}, {@link TSDocConfigFile.log}, or {@link TSDocConfigFile.getErrorSummary}.
*/
updateParser(configuration) {
// First apply the base config files
for (const extendsFile of this.extendsFiles) {
extendsFile.updateParser(configuration);
}
// Then apply this one
for (const tagDefinition of this.tagDefinitions) {
configuration.addTagDefinition(tagDefinition);
}
this.supportForTags.forEach((supported, tagName) => {
const tagDefinition = configuration.tryGetTagDefinition(tagName);
if (tagDefinition) {
// Note that setSupportForTag() automatically enables configuration.validation.reportUnsupportedTags
configuration.setSupportForTag(tagDefinition, supported);
}
else {
// Note that this validation may depend partially on the preexisting state of the TSDocConfiguration
// object, so it cannot be performed during the TSConfigFile.loadFile() stage.
this._reportError({
messageId: tsdoc_1.TSDocMessageId.ConfigFileUndefinedTag,
messageText: `The "supportForTags" field refers to an undefined tag ${JSON.stringify(tagName)}.`,
textRange: tsdoc_1.TextRange.empty,
});
}
});
if (this.supportedHtmlElements) {
configuration.setSupportedHtmlElements([...this.supportedHtmlElements]);
}
if (this._reportUnsupportedHtmlElements === false) {
configuration.validation.reportUnsupportedHtmlElements = false;
}
else if (this._reportUnsupportedHtmlElements === true) {
configuration.validation.reportUnsupportedHtmlElements = true;
}
}
_getNoStandardTagsWithExtends() {
if (this.noStandardTags !== undefined) {
return this.noStandardTags;
}
// This config file does not specify "noStandardTags", so consider any base files referenced using "extends"
let result = undefined;
for (const extendsFile of this.extendsFiles) {
const extendedValue = extendsFile._getNoStandardTagsWithExtends();
if (extendedValue !== undefined) {
result = extendedValue;
}
}
if (result === undefined) {
// If no config file specifies noStandardTags, then it defaults to false
result = false;
}
return result;
}
}
exports.TSDocConfigFile = TSDocConfigFile;
TSDocConfigFile.FILENAME = 'tsdoc.json';
TSDocConfigFile.CURRENT_SCHEMA_URL = 'https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json';
//# sourceMappingURL=TSDocConfigFile.js.map