stylelint
Version:
A mighty CSS linter that helps you avoid errors and enforce conventions.
145 lines (118 loc) • 4.38 kB
JavaScript
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;
}