"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