@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
169 lines (168 loc) • 6.68 kB
JavaScript
;
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