UNPKG

@rushstack/node-core-library

Version:

Core libraries that every NodeJS toolchain project should use

177 lines 8.59 kB
"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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.RealNodeModulePathResolver = void 0; const nodeFs = __importStar(require("node:fs")); const nodePath = __importStar(require("node:path")); /** * This class encapsulates a caching resolver for symlinks in node_modules directories. * It assumes that the only symlinks that exist in input paths are those that correspond to * npm packages. * * @remarks * In a repository with a symlinked node_modules installation, some symbolic links need to be mapped for * node module resolution to produce correct results. However, calling `fs.realpathSync.native` on every path, * as is commonly done by most resolvers, involves an enormous number of file system operations (for reference, * each invocation of `fs.realpathSync.native` involves a series of `fs.readlinkSync` calls, up to one for each * path segment in the input). * * @public */ class RealNodeModulePathResolver { constructor(options = {}) { const { fs: { lstatSync = nodeFs.lstatSync, readlinkSync = nodeFs.readlinkSync } = nodeFs, path: { isAbsolute = nodePath.isAbsolute, join = nodePath.join, resolve = nodePath.resolve, sep = nodePath.sep } = nodePath, ignoreMissingPaths = false } = options; const cache = (this._cache = new Map()); this._errorCache = new Map(); this._fs = { lstatSync, readlinkSync }; this._path = { isAbsolute, join, resolve, sep }; this._lstatOptions = { throwIfNoEntry: !ignoreMissingPaths }; const nodeModulesToken = `${sep}node_modules${sep}`; const self = this; function realNodeModulePathInternal(input) { // Find the last node_modules path segment const nodeModulesIndex = input.lastIndexOf(nodeModulesToken); if (nodeModulesIndex < 0) { // No node_modules in path, so we assume it is already the real path return input; } // First assume that the next path segment after node_modules is a symlink let linkStart = nodeModulesIndex + nodeModulesToken.length - 1; let linkEnd = input.indexOf(sep, linkStart + 1); // If the path segment starts with a '@', then it is a scoped package const isScoped = input.charAt(linkStart + 1) === '@'; if (isScoped) { // For a scoped package, the scope is an ordinary directory, so we need to find the next path segment if (linkEnd < 0) { // Symlink missing, so see if anything before the last node_modules needs resolving, // and preserve the rest of the path return join(realNodeModulePathInternal(input.slice(0, nodeModulesIndex)), input.slice(nodeModulesIndex + 1), // Joining to `.` will clean up any extraneous trailing slashes '.'); } linkStart = linkEnd; linkEnd = input.indexOf(sep, linkStart + 1); } // No trailing separator, so the link is the last path segment if (linkEnd < 0) { linkEnd = input.length; } const linkCandidate = input.slice(0, linkEnd); // Check if the link is a symlink const linkTarget = self._tryReadLink(linkCandidate); if (linkTarget && isAbsolute(linkTarget)) { // Absolute path, combine the link target with any remaining path segments // Cache the resolution to avoid the readlink call in subsequent calls cache.set(linkCandidate, linkTarget); cache.set(linkTarget, linkTarget); // Joining to `.` will clean up any extraneous trailing slashes return join(linkTarget, input.slice(linkEnd + 1), '.'); } // Relative path or does not exist // Either way, the path before the last node_modules could itself be in a node_modules folder // So resolve the base path to find out what paths are relative to const realpathBeforeNodeModules = realNodeModulePathInternal(input.slice(0, nodeModulesIndex)); if (linkTarget) { // Relative path in symbolic link. Should be resolved relative to real path of base path. const resolvedTarget = resolve(realpathBeforeNodeModules, input.slice(nodeModulesIndex + 1, linkStart), linkTarget); // Cache the result of the combined resolution to avoid the readlink call in subsequent calls cache.set(linkCandidate, resolvedTarget); cache.set(resolvedTarget, resolvedTarget); // Joining to `.` will clean up any extraneous trailing slashes return join(resolvedTarget, input.slice(linkEnd + 1), '.'); } // No symlink, so just return the real path before the last node_modules combined with the // subsequent path segments // Joining to `.` will clean up any extraneous trailing slashes return join(realpathBeforeNodeModules, input.slice(nodeModulesIndex + 1), '.'); } this.realNodeModulePath = (input) => { return realNodeModulePathInternal(resolve(input)); }; } /** * Clears the cache of resolved symlinks. * @public */ clearCache() { this._cache.clear(); } /** * Tries to read a symbolic link at the specified path. * If the input is not a symbolic link, returns undefined. * @param link - The link to try to read * @returns The target of the symbolic link, or undefined if the input is not a symbolic link */ _tryReadLink(link) { const cached = this._cache.get(link); if (cached !== undefined) { return cached || undefined; } const cachedError = this._errorCache.get(link); if (cachedError) { // Fill the properties but fix the stack trace. throw Object.assign(new Error(cachedError.message), cachedError); } // On Windows, calling `readlink` on a directory throws an EUNKOWN, not EINVAL, so just pay the cost // of an lstat call. try { const stat = this._fs.lstatSync(link, this._lstatOptions); if (stat === null || stat === void 0 ? void 0 : stat.isSymbolicLink()) { // path.join(x, '.') will trim trailing slashes, if applicable const result = this._path.join(this._fs.readlinkSync(link, 'utf8'), '.'); return result; } // Ensure we cache that this was not a symbolic link. this._cache.set(link, false); } catch (err) { this._errorCache.set(link, err); } } } exports.RealNodeModulePathResolver = RealNodeModulePathResolver; //# sourceMappingURL=RealNodeModulePath.js.map