UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

169 lines (168 loc) 6.68 kB
"use strict"; var _push = _interopRequireDefault(require("core-js-pure/stable/instance/push.js")); var _includes = _interopRequireDefault(require("core-js-pure/stable/instance/includes.js")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const stylelint = require('stylelint'); const fs = require('fs'); const path = require('path'); const RULE_NAME = 'eufemia/no-undefined-custom-property'; const SRC_ROOT = path.resolve(__dirname, '../../..'); const messages = stylelint.utils.ruleMessages(RULE_NAME, { undefined: prop => `Unexpected reference to undefined custom property "${prop}". ` + `It is referenced via "var(${prop})" without a fallback but is never ` + `defined in any SCSS file nor set via JavaScript/TypeScript. A var() ` + `pointing at an undefined custom property resolves to a ` + `guaranteed-invalid value, which silently breaks the declaration. ` + `Define the property, add a fallback ("var(${prop}, <fallback>)"), or ` + `remove the reference.`, undefinedWithFallback: prop => `Unexpected reference to undefined custom property "${prop}". ` + `It is referenced via "var(${prop}, <fallback>)" but is never defined ` + `in any SCSS file nor set via JavaScript/TypeScript, so it always ` + `resolves to the fallback \u2013 a "phantom" reference. Reference the ` + `real variable or value directly, define the property, or add it to ` + `"ignoreProperties" if it is an intentional override hook or is set at ` + `runtime with a dynamically built name.` }); const meta = { url: 'https://github.com/dnbexperience/eufemia' }; const SCSS_DEFINITION = /(?:^|[^\w-])(--[\w-]+)\s*:/g; const ANY_CUSTOM_PROPERTY = /--[\w-]+/g; const VAR_WITHOUT_FALLBACK = /var\(\s*(--[\w-]+)\s*\)/g; const VAR_WITH_FALLBACK = /var\(\s*(--[\w-]+)\s*,/g; const IGNORED_DIRECTORIES = new Set(['node_modules', '__tests__', '__snapshots__', 'build']); const knownPropertiesCache = new Map(); const walkFiles = (dir, extensions, onFile) => { let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (IGNORED_DIRECTORIES.has(entry.name) || entry.name.startsWith('.')) { continue; } walkFiles(fullPath, extensions, onFile); } else if (extensions.some(ext => entry.name.endsWith(ext))) { onFile(fullPath); } } }; const collectMatches = (content, regex, group, target) => { let match; while ((match = regex.exec(content)) !== null) { target.add(group === 0 ? match[0] : match[group]); } }; const scanFiles = async (files, regex, group, target) => { for (const file of files) { let content; try { content = await fs.promises.readFile(file, 'utf8'); } catch { continue; } collectMatches(content, regex, group, target); } }; const buildKnownProperties = srcRoot => { if (knownPropertiesCache.has(srcRoot)) { return knownPropertiesCache.get(srcRoot); } const scan = (async () => { const properties = new Set(); const styleFiles = []; walkFiles(srcRoot, ['.scss', '.css'], file => (0, _push.default)(styleFiles).call(styleFiles, file)); await scanFiles(styleFiles, SCSS_DEFINITION, 1, properties); const scriptFiles = []; walkFiles(srcRoot, ['.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs'], file => (0, _push.default)(scriptFiles).call(scriptFiles, file)); await scanFiles(scriptFiles, ANY_CUSTOM_PROPERTY, 0, properties); return properties; })(); knownPropertiesCache.set(srcRoot, scan); return scan; }; const collectLocalDefinitions = root => { const local = new Set(); root.walkDecls(decl => { if (decl.prop && decl.prop.startsWith('--')) { local.add(decl.prop); } }); return local; }; const ruleFunction = (primary, secondaryOptions) => { return async (root, result) => { const validOptions = stylelint.utils.validateOptions(result, RULE_NAME, { actual: primary, possible: [true, false] }, { actual: secondaryOptions, possible: { ignoreProperties: [value => typeof value === 'string'], reportFallbacks: [true, false] }, optional: true }); if (!validOptions || !primary) { return; } const ignoreExact = new Set(); const ignorePatterns = []; for (const entry of secondaryOptions && secondaryOptions.ignoreProperties || []) { const regexMatch = /^\/(.*)\/([a-z]*)$/.exec(entry); if (regexMatch) { try { (0, _push.default)(ignorePatterns).call(ignorePatterns, new RegExp(regexMatch[1], regexMatch[2])); } catch (error) { result.warn(`Invalid "ignoreProperties" regex "${entry}" for ${RULE_NAME}: ${error.message}`, { stylelintType: 'invalidOption' }); } } else { ignoreExact.add(entry); } } const reportFallbacks = Boolean(secondaryOptions && secondaryOptions.reportFallbacks); const knownProperties = await buildKnownProperties(SRC_ROOT); const localDefinitions = collectLocalDefinitions(root); const isKnown = property => knownProperties.has(property) || localDefinitions.has(property) || ignoreExact.has(property) || ignorePatterns.some(pattern => pattern.test(property)); root.walkDecls(decl => { var _context; if (!decl.value || !(0, _includes.default)(_context = decl.value).call(_context, 'var(')) { return; } let match; while ((match = VAR_WITHOUT_FALLBACK.exec(decl.value)) !== null) { const property = match[1]; if (isKnown(property)) { continue; } stylelint.utils.report({ message: messages.undefined(property), node: decl, word: property, result, ruleName: RULE_NAME }); } if (!reportFallbacks) { return; } let fallbackMatch; while ((fallbackMatch = VAR_WITH_FALLBACK.exec(decl.value)) !== null) { const property = fallbackMatch[1]; if (isKnown(property)) { continue; } stylelint.utils.report({ message: messages.undefinedWithFallback(property), node: decl, word: property, result, ruleName: RULE_NAME }); } }); }; }; ruleFunction.ruleName = RULE_NAME; ruleFunction.messages = messages; ruleFunction.meta = meta; module.exports = stylelint.createPlugin(RULE_NAME, ruleFunction); module.exports.ruleName = RULE_NAME; module.exports.messages = messages; //# sourceMappingURL=no-undefined-custom-property.js.map