UNPKG

stylelint

Version:

A mighty CSS linter that helps you avoid errors and enforce conventions.

145 lines (118 loc) 4.38 kB
import fs from 'node:fs'; import module from 'node:module'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath, pathToFileURL } from 'node:url'; // TODO: refactor to use `import.meta.resolve()` when the `parent` argument // is available without the --experimental-import-meta-resolve flag // @see https://github.com/stylelint/stylelint/pull/8886 import { resolve } from 'import-meta-resolve'; /** * @typedef {object} PnpApi * @property {(specifier: string, issuer: string | null) => string | null} resolveRequest * @property {(specifier: string, issuer: string | null) => string | null} resolveToUnqualified */ /** * @typedef {typeof module & { findPnpApi?: (issuer: string) => PnpApi | null }} ModuleWithPnp */ /** * Attempts to resolve a module specifier using Yarn PnP's API. * * When running under Yarn PnP, the import-meta-resolve polyfill doesn't work * because it bypasses PnP's virtual filesystem. This function uses * Module.findPnpApi() to get the PnP API for the target directory and * resolves packages through it. * * @see https://yarnpkg.com/advanced/pnpapi * @see https://github.com/stylelint/stylelint/issues/8971 * @param {string} specifier The module specifier to resolve. * @param {string} issuer The directory to resolve from. Must end with /. * @returns {string | undefined} The resolved path, or undefined if resolution fails. */ function resolvePnP(specifier, issuer) { // Module.findPnpApi is added by Yarn PnP at runtime. const mod = /** @type {ModuleWithPnp} */ (module); if (typeof mod.findPnpApi !== 'function') { return undefined; } const pnpapi = mod.findPnpApi(issuer); if (!pnpapi) { return undefined; } try { // First try resolveRequest. return pnpapi.resolveRequest(specifier, issuer) ?? undefined; } catch { // resolveRequest can fail for packages with only exports field. Fall // back to resolveToUnqualified to locate the package, then use import- // meta-resolve to handle the exports field resolution correctly. try { const packagePath = pnpapi.resolveToUnqualified(specifier, issuer); if (!packagePath) { return undefined; } // Use import-meta-resolve from the package's location to resolve // its entry point. const packageBase = pathToFileURL(path.join(packagePath, 'noop.js')).toString(); const resolved = resolve(specifier, packageBase); return fileURLToPath(resolved); } catch { return undefined; } } } // TODO: Remove support for CommonJS-style filename-less and extension-less paths in the // next major release to align with ESM import behavior. const pathSuffixes = ['', '.js', '.json', `${path.sep}index.js`, `${path.sep}index.json`]; const specifierSuffixes = ['', '.js', '.json', '/index.js', '/index.json']; /** * Checks whether the given file exists as a file. * @param {string} filename The file to check. * @returns {boolean} `true` if the file exists and is a file, otherwise `false`. */ function existsFile(filename) { return fs.statSync(filename, { throwIfNoEntry: false })?.isFile() ?? false; } /** * @param {string} parent * @param {string} lookup * @returns {string | undefined} */ export default function resolveSilent(parent, lookup) { if (path.isAbsolute(lookup)) { for (const suffix of pathSuffixes) { const filename = lookup + suffix; if (existsFile(filename)) { return filename; } } return; } const base = pathToFileURL(path.resolve(parent, 'noop.js')).toString(); // In Yarn PnP environments, try PnP resolution first for bare specifiers. // The import-meta-resolve polyfill doesn't work with PnP because it // bypasses the virtual filesystem. const isBareSpecifier = !lookup.startsWith('./') && !lookup.startsWith('../'); if (isBareSpecifier && process.versions.pnp) { const issuer = path.resolve(parent) + path.sep; // Try with suffixes for CommonJS compatibility. for (const suffix of specifierSuffixes) { const resolved = resolvePnP(lookup + suffix, issuer); if (resolved && existsFile(resolved)) { return resolved; } } } // Try ESM resolution with suffixes for CommonJS compatibility. for (const suffix of specifierSuffixes) { try { const resolved = fileURLToPath(resolve(lookup + suffix, base)); if (existsFile(resolved)) { return resolved; } } catch { // } } return undefined; }