327 lines
14 KiB
JavaScript
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
|