stylelint-no-unresolved-module
Version:
Ensures that module (import-like or `url`) can be resolved to a module on the file system.
423 lines (399 loc) • 12.3 kB
JavaScript
'use strict';
var Ajv = require('ajv');
var stylelint = require('stylelint');
var parse = require('postcss-value-parser');
var path = require('path');
var oxcResolver = require('oxc-resolver');
var isUrl = require('is-url');
var pWaterfall = require('p-waterfall');
var scssParser = require('scss-parser');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var Ajv__default = /*#__PURE__*/_interopDefaultLegacy(Ajv);
var stylelint__default = /*#__PURE__*/_interopDefaultLegacy(stylelint);
var parse__default = /*#__PURE__*/_interopDefaultLegacy(parse);
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
var isUrl__default = /*#__PURE__*/_interopDefaultLegacy(isUrl);
var pWaterfall__default = /*#__PURE__*/_interopDefaultLegacy(pWaterfall);
var scssParser__default = /*#__PURE__*/_interopDefaultLegacy(scssParser);
/**
* @typedef {import('postcss').Root} postcss.Root
* @typedef {import('postcss').ChildNode} postcss.ChildNode
*/
const urlRegex = /^url/;
const defaultResolveValues = {
extensions: ['.css'],
conditionNames: ['style', 'browser', 'import', 'require', 'node', 'default'],
mainFields: ['style', 'browser', 'module', 'main'],
mainFiles: ['index'],
modules: ['node_modules']
};
class NodeResolver {
/**
* @param {NodeResolverOptions} options
*/
constructor(options) {
var _this$cssRoot;
const {
cssRoot = null,
cwd = process.cwd(),
alias = (/** @type {Record<string, (string|string[])>}*/{}),
roots = (/** @type {string[]}*/[]),
extensions = [...defaultResolveValues.extensions],
conditionNames = [...defaultResolveValues.conditionNames],
mainFields = [...defaultResolveValues.mainFields],
mainFiles = [...defaultResolveValues.mainFiles],
modules = [...defaultResolveValues.modules]
} = options !== null && options !== void 0 ? options : {};
this.cwd = cwd;
this.alias = alias;
this.roots = [...roots];
this.extensions = extensions;
this.conditionNames = conditionNames;
this.mainFields = mainFields;
this.mainFiles = mainFiles;
this.modules = modules;
this.cssRoot = cssRoot;
if ((_this$cssRoot = this.cssRoot) !== null && _this$cssRoot !== void 0 && (_this$cssRoot = _this$cssRoot.source) !== null && _this$cssRoot !== void 0 && _this$cssRoot.input.file) {
this.context = path__default["default"].dirname(this.cssRoot.source.input.file);
} else {
this.context = this.cwd;
}
this.importAtRules = ['import'];
if (this.context === this.cwd) {
this.roots.push(this.cwd);
}
this.roots = [...new Set(this.roots)].map(root => {
return path__default["default"].resolve(this.cwd, root);
});
const preparedAlias = Object.entries(this.alias).reduce((array, [key, value]) => {
array[key] = /** @type {string[]}*/[].concat(value);
return array;
}, /** @type {Record<string, string[]>} */{});
this.enhancedResolve = new oxcResolver.ResolverFactory({
roots: this.roots,
alias: preparedAlias,
extensions: this.extensions,
conditionNames: this.conditionNames,
mainFields: this.mainFields,
mainFiles: this.mainFiles,
modules: this.modules
});
}
/**
* @param {string} entry
*/
async resolvePath(entry) {
const result = await this.enhancedResolve.async(this.context, entry);
if (result.error) {
throw new Error(result.error);
} else if (typeof result.path === 'string') {
return result.path;
} else {
throw new TypeError(`Unable to resolve "${entry}".`);
}
}
/**
* @param {string} value
* @param {postcss.ChildNode} rootNode
*/
resolve(value, rootNode) {
if (isUrl__default["default"](value) || rootNode.type === 'atrule' && !this.isGenericUrlImport(rootNode)) {
return false;
}
return {
promise: this.resolvePath(value),
message: this.message.bind(this)
};
}
/**
* @param {string} resource
* @param {postcss.ChildNode} rootNode
*/
message(resource, rootNode) {
if (rootNode.type === 'atrule') {
return `Unable to resolve path to import "${resource}".`;
}
return `Unable to resolve path to resource "${resource}".`;
}
/**
* @param {postcss.ChildNode} rootNode
*/
isGenericUrlImport(rootNode) {
return rootNode.type === 'atrule' && urlRegex.test(rootNode.params);
}
}
/**
* @typedef {import('postcss').Root} postcss.Root
* @typedef {import('postcss').ChildNode} postcss.ChildNode
* @typedef {import('./node').NodeResolverOptions} NodeResolverOptions
*/
const sassCoreModuleRegex = /^sass:[^:]+$/;
class SassResolver extends NodeResolver {
/**
* @param {NodeResolverOptions} options
*/
constructor(options) {
super({
...options,
extensions: ['.scss', '.css'],
conditionNames: ['sass', 'style', 'browser', 'import', 'require', 'node', 'default'],
mainFiles: ['_index', 'index']
});
this.importAtRules = ['import', 'use', 'forward'];
}
/**
* @param {string} rawEntry
*/
async resolvePath(rawEntry) {
const entry = rawEntry.replace('pkg:', '');
const dirname = path__default["default"].dirname(entry);
const basename = path__default["default"].basename(entry);
const underscoreEntry = path__default["default"].join(dirname, `_${basename}`);
const genericEntry = path__default["default"].join(dirname, basename);
const resolvePathTasks = [`./${underscoreEntry}`, `./${genericEntry}`, underscoreEntry, genericEntry].map(currentEntry => {
return async (/** @type {string?} */previousEntry) => {
if (typeof previousEntry === 'string') {
return previousEntry;
}
try {
return await super.resolvePath(currentEntry);
} catch {
return previousEntry;
}
};
});
/** @type {string?} */
// @ts-ignore
const resolvedPath = await pWaterfall__default["default"](resolvePathTasks, null);
if (resolvedPath === null) {
throw new Error(`Unable to resolve Sass path "${entry}".`);
}
return resolvedPath;
}
/**
* @param {string} value
* @param {postcss.ChildNode} rootNode
*/
resolve(value, rootNode) {
if (this.isSassCoreModuleRequest(value, rootNode) || this.isGenericUrlImport(rootNode)) {
return false;
}
return {
promise: this.resolvePath(value),
message: this.message.bind(this)
};
}
/**
* @param {string} resource
* @param {postcss.ChildNode} rootNode
*/
message(resource, rootNode) {
if (this.isSassModuleAtRule(rootNode)) {
return `Unable to resolve path to module "${resource}".`;
}
return super.message(resource, rootNode);
}
/**
* @param {postcss.ChildNode} rootNode
*/
isSassModuleAtRule(rootNode) {
return rootNode.type === 'atrule' && ['use', 'forward'].includes(rootNode.name);
}
/**
* @param {string} value
* @param {postcss.ChildNode} rootNode
*/
isSassCoreModuleRequest(value, rootNode) {
return rootNode.type === 'atrule' && this.isSassModuleAtRule(rootNode) && sassCoreModuleRegex.test(value);
}
/**
* @param {string} value
*/
isStaticString(value) {
/* eslint-disable unicorn/catch-error-name */
let parsed;
/*
* Try parsing raw value. If that fails, try to parse it as double-quoted string value.
* Otherwise, assume this isn’t static string.
*/
try {
parsed = scssParser__default["default"].parse(value);
} catch (error1) {
try {
parsed = scssParser__default["default"].parse(`"${value}"`);
} catch (error2) {
return false;
}
}
let result = false;
if (Array.isArray(parsed.value)) {
result = !parsed.value.some(({
type
}) => ['interpolation', 'variable'].includes(type));
}
return result;
}
}
/**
* @typedef {import('postcss').ChildNode} postcss.ChildNode
* @typedef {{
* rootNode: postcss.ChildNode,
* value: string,
* promise: Promise<string>,
* message: (resource: string, rootNode: postcss.ChildNode) => string
* }} ResolvedModule
*/
const ruleName = 'plugin/no-unresolved-module';
const ajv = new Ajv__default["default"]();
const validateOptions = ajv.compile({
oneOf: [{
type: 'boolean'
}, {
type: 'object',
additionalProperties: false,
properties: {
cwd: {
type: 'string'
},
modules: {
type: 'array',
minItems: 1,
items: {
type: 'string'
}
},
roots: {
type: 'array',
minItems: 1,
items: {
type: 'string'
}
},
alias: {
type: 'object',
'anyOf': [{
patternProperties: {
'.+': {
type: 'string'
}
}
}, {
patternProperties: {
'.+': {
type: 'array',
minItems: 1,
items: {
type: 'string'
}
}
}
}]
}
}
}]
});
const messages = stylelint__default["default"].utils.ruleMessages(ruleName, {
report: (/** @type string */value) => value
});
/**
* @param {string} value
*/
function isDataUrl(value) {
try {
return new URL(value).protocol === 'data:';
} catch {
return false;
}
}
/**
* @type {stylelint.RuleBase}
*/
function ruleFunction(resolveRules) {
return async function (cssRoot, result) {
const validOptions = stylelint__default["default"].utils.validateOptions(result, ruleName, {
actual: resolveRules,
possible: value => (/** @type {boolean}*/validateOptions(value))
});
if (!validOptions) {
return;
}
const nodeResolver = new NodeResolver({
cssRoot,
...resolveRules
});
const sassResolver = new SassResolver({
cssRoot,
...resolveRules
});
/** @type {ResolvedModule[]} */
const values = [];
cssRoot.walkAtRules(new RegExp([...nodeResolver.importAtRules, ...sassResolver.importAtRules].join('|')), atRule => {
const parsed = parse__default["default"](atRule.params);
let shouldExit = false;
parsed.walk(node => {
if (['use', 'forward'].includes(atRule.name) && node.value === 'with') {
shouldExit = true;
}
if (node.type === 'string' && !shouldExit) {
const value = [nodeResolver.resolve(node.value, atRule), sassResolver.resolve(node.value, atRule)].find(entry => entry !== false);
if (typeof value === 'object') {
values.push({
rootNode: atRule,
value: node.value,
...value
});
}
}
});
});
cssRoot.walkDecls(decl => {
if (decl.value.includes('url')) {
const parsed = parse__default["default"](decl.value);
parsed.walk(topNode => {
if (topNode.type === 'function' && topNode.value === 'url') {
const [node] = topNode.nodes;
if (sassResolver.isStaticString(node.value) && !isDataUrl(node.value)) {
const value = [nodeResolver.resolve(node.value, decl)].find(entry => entry !== false);
if (typeof value === 'object') {
values.push({
rootNode: decl,
value: node.value,
...value
});
}
}
}
});
}
});
const resolvedValues = await Promise.allSettled(values.map(({
promise
}) => promise));
resolvedValues.forEach(({
status
}, index) => {
if (status === 'rejected') {
const {
value,
rootNode,
message
} = values[index];
stylelint__default["default"].utils.report({
ruleName: ruleName,
result: result,
node: rootNode,
word: value,
message: messages.report(message(value, rootNode))
});
}
});
};
}
// @ts-ignore
const plugin = stylelint__default["default"].createPlugin(ruleName, ruleFunction);
var index = {
...plugin,
messages
};
module.exports = index;
//# sourceMappingURL=index.js.map