utils/node_modules/@rushstack/node-core-library/lib/PackageJsonLookup.js
2024-02-07 01:33:07 -05:00

327 lines
14 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.PackageJsonLookup = void 0;
const path = __importStar(require("path"));
const JsonFile_1 = require("./JsonFile");
const Constants_1 = require("./Constants");
const FileSystem_1 = require("./FileSystem");
/**
* This class provides methods for finding the nearest "package.json" for a folder
* and retrieving the name of the package. The results are cached.
*
* @public
*/
class PackageJsonLookup {
/**
* A singleton instance of `PackageJsonLookup`, which is useful for short-lived processes
* that can reasonably assume that the file system will not be modified after the cache
* is populated.
*
* @remarks
* For long-running processes that need to clear the cache at appropriate times,
* it is recommended to create your own instance of `PackageJsonLookup` instead
* of relying on this instance.
*/
static get instance() {
if (!PackageJsonLookup._instance) {
PackageJsonLookup._instance = new PackageJsonLookup({ loadExtraFields: true });
}
return PackageJsonLookup._instance;
}
constructor(parameters) {
this._loadExtraFields = false;
if (parameters) {
if (parameters.loadExtraFields) {
this._loadExtraFields = parameters.loadExtraFields;
}
}
this.clearCache();
}
/**
* A helper for loading the caller's own package.json file.
*
* @remarks
*
* This function provides a concise and efficient way for an NPM package to report metadata about itself.
* For example, a tool might want to report its version.
*
* The `loadOwnPackageJson()` probes upwards from the caller's folder, expecting to find a package.json file,
* which is assumed to be the caller's package. The result is cached, under the assumption that a tool's
* own package.json (and intermediary folders) will never change during the lifetime of the process.
*
* @example
* ```ts
* // Report the version of our NPM package
* const myPackageVersion: string = PackageJsonLookup.loadOwnPackageJson(__dirname).version;
* console.log(`Cool Tool - Version ${myPackageVersion}`);
* ```
*
* @param dirnameOfCaller - The NodeJS `__dirname` macro for the caller.
* @returns This function always returns a valid `IPackageJson` object. If any problems are encountered during
* loading, an exception will be thrown instead.
*/
static loadOwnPackageJson(dirnameOfCaller) {
const packageJson = PackageJsonLookup.instance.tryLoadPackageJsonFor(dirnameOfCaller);
if (packageJson === undefined) {
throw new Error(`PackageJsonLookup.loadOwnPackageJson() failed to find the caller's package.json.` +
` The __dirname was: ${dirnameOfCaller}`);
}
if (packageJson.version !== undefined) {
return packageJson;
}
const errorPath = PackageJsonLookup.instance.tryGetPackageJsonFilePathFor(dirnameOfCaller) || 'package.json';
throw new Error(`PackageJsonLookup.loadOwnPackageJson() failed because the "version" field is missing in` +
` ${errorPath}`);
}
/**
* Clears the internal file cache.
* @remarks
* Call this method if changes have been made to the package.json files on disk.
*/
clearCache() {
this._packageFolderCache = new Map();
this._packageJsonCache = new Map();
}
/**
* Returns the absolute path of a folder containing a package.json file, by looking
* upwards from the specified fileOrFolderPath. If no package.json can be found,
* undefined is returned.
*
* @remarks
* The fileOrFolderPath is not required to actually exist on disk.
* The fileOrFolderPath itself can be the return value, if it is a folder containing
* a package.json file.
* Both positive and negative lookup results are cached.
*
* @param fileOrFolderPath - a relative or absolute path to a source file or folder
* that may be part of a package
* @returns an absolute path to a folder containing a package.json file
*/
tryGetPackageFolderFor(fileOrFolderPath) {
// Convert it to an absolute path
const resolvedFileOrFolderPath = path.resolve(fileOrFolderPath);
// Optimistically hope that the starting string is already in the cache,
// in which case we can avoid disk access entirely.
//
// (Two lookups are required, because get() cannot distinguish the undefined value
// versus a missing key.)
if (this._packageFolderCache.has(resolvedFileOrFolderPath)) {
return this._packageFolderCache.get(resolvedFileOrFolderPath);
}
// Now call the recursive part of the algorithm
return this._tryGetPackageFolderFor(resolvedFileOrFolderPath);
}
/**
* If the specified file or folder is part of a package, this returns the absolute path
* to the associated package.json file.
*
* @remarks
* The package folder is determined using the same algorithm
* as {@link PackageJsonLookup.tryGetPackageFolderFor}.
*
* @param fileOrFolderPath - a relative or absolute path to a source file or folder
* that may be part of a package
* @returns an absolute path to * package.json file
*/
tryGetPackageJsonFilePathFor(fileOrFolderPath) {
const packageJsonFolder = this.tryGetPackageFolderFor(fileOrFolderPath);
if (!packageJsonFolder) {
return undefined;
}
return path.join(packageJsonFolder, Constants_1.FileConstants.PackageJson);
}
/**
* If the specified file or folder is part of a package, this loads and returns the
* associated package.json file.
*
* @remarks
* The package folder is determined using the same algorithm
* as {@link PackageJsonLookup.tryGetPackageFolderFor}.
*
* @param fileOrFolderPath - a relative or absolute path to a source file or folder
* that may be part of a package
* @returns an IPackageJson object, or undefined if the fileOrFolderPath does not
* belong to a package
*/
tryLoadPackageJsonFor(fileOrFolderPath) {
const packageJsonFilePath = this.tryGetPackageJsonFilePathFor(fileOrFolderPath);
if (!packageJsonFilePath) {
return undefined;
}
return this.loadPackageJson(packageJsonFilePath);
}
/**
* This function is similar to {@link PackageJsonLookup.tryLoadPackageJsonFor}, except that it does not report
* an error if the `version` field is missing from the package.json file.
*/
tryLoadNodePackageJsonFor(fileOrFolderPath) {
const packageJsonFilePath = this.tryGetPackageJsonFilePathFor(fileOrFolderPath);
if (!packageJsonFilePath) {
return undefined;
}
return this.loadNodePackageJson(packageJsonFilePath);
}
/**
* Loads the specified package.json file, if it is not already present in the cache.
*
* @remarks
* Unless {@link IPackageJsonLookupParameters.loadExtraFields} was specified,
* the returned IPackageJson object will contain a subset of essential fields.
* The returned object should be considered to be immutable; the caller must never
* modify it.
*
* @param jsonFilename - a relative or absolute path to a package.json file
*/
loadPackageJson(jsonFilename) {
const packageJson = this.loadNodePackageJson(jsonFilename);
if (!packageJson.version) {
throw new Error(`Error reading "${jsonFilename}":\n The required field "version" was not found`);
}
return packageJson;
}
/**
* This function is similar to {@link PackageJsonLookup.loadPackageJson}, except that it does not report an error
* if the `version` field is missing from the package.json file.
*/
loadNodePackageJson(jsonFilename) {
return this._loadPackageJsonInner(jsonFilename);
}
_loadPackageJsonInner(jsonFilename, errorsToIgnore) {
const loadResult = this._tryLoadNodePackageJsonInner(jsonFilename);
if (loadResult.error && (errorsToIgnore === null || errorsToIgnore === void 0 ? void 0 : errorsToIgnore.has(loadResult.error))) {
return undefined;
}
switch (loadResult.error) {
case 'FILE_NOT_FOUND': {
throw new Error(`Input file not found: ${jsonFilename}`);
}
case 'MISSING_NAME_FIELD': {
throw new Error(`Error reading "${jsonFilename}":\n The required field "name" was not found`);
}
case 'OTHER_ERROR': {
throw loadResult.errorObject;
}
default: {
return loadResult.packageJson;
}
}
}
/**
* Try to load a package.json file as an INodePackageJson,
* returning undefined if the found file does not contain a `name` field.
*/
_tryLoadNodePackageJsonInner(jsonFilename) {
// Since this will be a cache key, follow any symlinks and get an absolute path
// to minimize duplication. (Note that duplication can still occur due to e.g. character case.)
let normalizedFilePath;
try {
normalizedFilePath = FileSystem_1.FileSystem.getRealPath(jsonFilename);
}
catch (e) {
if (FileSystem_1.FileSystem.isNotExistError(e)) {
return {
error: 'FILE_NOT_FOUND'
};
}
else {
return {
error: 'OTHER_ERROR',
errorObject: e
};
}
}
let packageJson = this._packageJsonCache.get(normalizedFilePath);
if (!packageJson) {
const loadedPackageJson = JsonFile_1.JsonFile.load(normalizedFilePath);
// Make sure this is really a package.json file. CommonJS has fairly strict requirements,
// but NPM only requires "name" and "version"
if (!loadedPackageJson.name) {
return {
error: 'MISSING_NAME_FIELD'
};
}
if (this._loadExtraFields) {
packageJson = loadedPackageJson;
}
else {
packageJson = {};
// Unless "loadExtraFields" was requested, copy over the essential fields only
packageJson.bin = loadedPackageJson.bin;
packageJson.dependencies = loadedPackageJson.dependencies;
packageJson.description = loadedPackageJson.description;
packageJson.devDependencies = loadedPackageJson.devDependencies;
packageJson.homepage = loadedPackageJson.homepage;
packageJson.license = loadedPackageJson.license;
packageJson.main = loadedPackageJson.main;
packageJson.name = loadedPackageJson.name;
packageJson.optionalDependencies = loadedPackageJson.optionalDependencies;
packageJson.peerDependencies = loadedPackageJson.peerDependencies;
packageJson.private = loadedPackageJson.private;
packageJson.scripts = loadedPackageJson.scripts;
packageJson.typings = loadedPackageJson.typings || loadedPackageJson.types;
packageJson.tsdocMetadata = loadedPackageJson.tsdocMetadata;
packageJson.version = loadedPackageJson.version;
}
Object.freeze(packageJson);
this._packageJsonCache.set(normalizedFilePath, packageJson);
}
return {
packageJson
};
}
// Recursive part of the algorithm from tryGetPackageFolderFor()
_tryGetPackageFolderFor(resolvedFileOrFolderPath) {
// Two lookups are required, because get() cannot distinguish the undefined value
// versus a missing key.
if (this._packageFolderCache.has(resolvedFileOrFolderPath)) {
return this._packageFolderCache.get(resolvedFileOrFolderPath);
}
// Is resolvedFileOrFolderPath itself a folder with a valid package.json file? If so, return it.
const packageJsonFilePath = `${resolvedFileOrFolderPath}/${Constants_1.FileConstants.PackageJson}`;
const packageJson = this._loadPackageJsonInner(packageJsonFilePath, new Set(['FILE_NOT_FOUND', 'MISSING_NAME_FIELD']));
if (packageJson) {
this._packageFolderCache.set(resolvedFileOrFolderPath, resolvedFileOrFolderPath);
return resolvedFileOrFolderPath;
}
// Otherwise go up one level
const parentFolder = path.dirname(resolvedFileOrFolderPath);
if (!parentFolder || parentFolder === resolvedFileOrFolderPath) {
// We reached the root directory without finding a package.json file,
// so cache the negative result
this._packageFolderCache.set(resolvedFileOrFolderPath, undefined);
return undefined; // no match
}
// Recurse upwards, caching every step along the way
const parentResult = this._tryGetPackageFolderFor(parentFolder);
// Cache the parent's answer as well
this._packageFolderCache.set(resolvedFileOrFolderPath, parentResult);
return parentResult;
}
}
exports.PackageJsonLookup = PackageJsonLookup;
//# sourceMappingURL=PackageJsonLookup.js.map