UNPKG

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
'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