routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+361
@@ -0,0 +1,361 @@
|
||||
import path from 'node:path';
|
||||
import coreJsCompat from 'core-js-compat';
|
||||
import {camelCase} from 'change-case';
|
||||
import isStaticRequire from './ast/is-static-require.js';
|
||||
import {readPackageJson} from './shared/package-json.js';
|
||||
|
||||
const {data: compatData, entries: coreJsEntries} = coreJsCompat;
|
||||
|
||||
const MESSAGE_ID_POLYFILL = 'unnecessaryPolyfill';
|
||||
const MESSAGE_ID_CORE_JS = 'unnecessaryCoreJsModule';
|
||||
const messages = {
|
||||
[MESSAGE_ID_POLYFILL]: 'Use built-in instead.',
|
||||
[MESSAGE_ID_CORE_JS]:
|
||||
'All polyfilled features imported from `{{coreJsModule}}` are available as built-ins. Use the built-ins instead.',
|
||||
};
|
||||
|
||||
const additionalPolyfillModules = {
|
||||
'es.promise.finally': ['p-finally'],
|
||||
'es.object.set-prototype-of': ['setprototypeof'],
|
||||
'es.string.code-point-at': ['code-point-at'],
|
||||
};
|
||||
const additionalPolyfillPatterns = Object.fromEntries(Object.entries(additionalPolyfillModules).map(([feature, modules]) => [feature, `|(${modules.join('|')})`]));
|
||||
|
||||
const prefixes = '(mdn-polyfills/|polyfill-)';
|
||||
const suffixes = '(-polyfill)';
|
||||
const delimiter = String.raw`(\.|-|\.prototype\.|/)?`;
|
||||
const moduleDelimiter = /[./-]/u;
|
||||
|
||||
const getFirstSegment = value => {
|
||||
const [firstSegment = ''] = value.split(moduleDelimiter);
|
||||
return firstSegment;
|
||||
};
|
||||
|
||||
const stripPolyfillPrefix = value => {
|
||||
if (value.startsWith('polyfill-')) {
|
||||
return value.slice('polyfill-'.length);
|
||||
}
|
||||
|
||||
if (value.startsWith('mdn-polyfills/')) {
|
||||
return value.slice('mdn-polyfills/'.length);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
function addPolyfillToken(tokens, value) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lowercaseValue = value.toLowerCase();
|
||||
tokens.add(lowercaseValue);
|
||||
tokens.add(getFirstSegment(lowercaseValue));
|
||||
|
||||
const camelCasedValue = camelCase(value).toLowerCase();
|
||||
tokens.add(camelCasedValue);
|
||||
tokens.add(getFirstSegment(camelCasedValue));
|
||||
}
|
||||
|
||||
const polyfills = Object.keys(compatData).map(feature => {
|
||||
const [rawEcmaVersion, rawConstructorName, rawMethodName = ''] = feature.split('.');
|
||||
let ecmaVersion = rawEcmaVersion;
|
||||
let constructorName = rawConstructorName;
|
||||
let methodName = rawMethodName;
|
||||
|
||||
if (ecmaVersion === 'es') {
|
||||
ecmaVersion = String.raw`(es\d*)`;
|
||||
}
|
||||
|
||||
constructorName = `(${constructorName}|${camelCase(constructorName)})`;
|
||||
methodName &&= `(${methodName}|${camelCase(methodName)})`;
|
||||
|
||||
const methodOrConstructor = methodName || constructorName;
|
||||
|
||||
const patterns = [
|
||||
`^((${prefixes}?(`,
|
||||
methodName && `(${ecmaVersion}${delimiter}${constructorName}${delimiter}${methodName})|`, // Ex: es6-array-copy-within
|
||||
methodName && `(${constructorName}${delimiter}${methodName})|`, // Ex: array-copy-within
|
||||
`(${ecmaVersion}${delimiter}${constructorName}))`, // Ex: es6-array
|
||||
`${suffixes}?)|`,
|
||||
`(${prefixes}${methodOrConstructor}|${methodOrConstructor}${suffixes})`, // Ex: polyfill-copy-within / polyfill-promise
|
||||
`${additionalPolyfillPatterns[feature] || ''})$`,
|
||||
];
|
||||
|
||||
return {
|
||||
feature,
|
||||
pattern: new RegExp(patterns.join(''), 'i'),
|
||||
tokens: (() => {
|
||||
const tokens = new Set();
|
||||
|
||||
if (rawEcmaVersion === 'es') {
|
||||
tokens.add('es');
|
||||
} else {
|
||||
addPolyfillToken(tokens, rawEcmaVersion);
|
||||
}
|
||||
|
||||
addPolyfillToken(tokens, rawConstructorName);
|
||||
addPolyfillToken(tokens, rawMethodName);
|
||||
|
||||
for (const module of additionalPolyfillModules[feature] || []) {
|
||||
addPolyfillToken(tokens, module);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
})(),
|
||||
};
|
||||
});
|
||||
const polyfillsByToken = new Map();
|
||||
const polyfillTokensByFirstCharacter = new Map();
|
||||
const esConstructorTokens = new Set();
|
||||
|
||||
for (const polyfill of polyfills) {
|
||||
const [ecmaVersion, constructorName] = polyfill.feature.split('.');
|
||||
if (ecmaVersion === 'es') {
|
||||
esConstructorTokens.add(constructorName.toLowerCase());
|
||||
esConstructorTokens.add(camelCase(constructorName).toLowerCase());
|
||||
}
|
||||
|
||||
for (const token of polyfill.tokens) {
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (polyfillsByToken.has(token)) {
|
||||
polyfillsByToken.get(token).push(polyfill);
|
||||
} else {
|
||||
polyfillsByToken.set(token, [polyfill]);
|
||||
}
|
||||
|
||||
const firstCharacter = token[0];
|
||||
if (polyfillTokensByFirstCharacter.has(firstCharacter)) {
|
||||
polyfillTokensByFirstCharacter.get(firstCharacter).add(token);
|
||||
} else {
|
||||
polyfillTokensByFirstCharacter.set(firstCharacter, new Set([token]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasEsConstructorPrefix = value => {
|
||||
for (const token of esConstructorTokens) {
|
||||
if (value.startsWith(token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isPotentialEsPrefix = importedModule => {
|
||||
if (!importedModule.startsWith('es')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let constructorIndex = 2;
|
||||
while (
|
||||
constructorIndex < importedModule.length
|
||||
&& importedModule[constructorIndex] >= '0'
|
||||
&& importedModule[constructorIndex] <= '9'
|
||||
) {
|
||||
constructorIndex++;
|
||||
}
|
||||
|
||||
if (importedModule.startsWith('.prototype.', constructorIndex)) {
|
||||
constructorIndex += '.prototype.'.length;
|
||||
} else if (['.', '-', '/'].includes(importedModule[constructorIndex])) {
|
||||
constructorIndex++;
|
||||
}
|
||||
|
||||
return hasEsConstructorPrefix(importedModule.slice(constructorIndex));
|
||||
};
|
||||
|
||||
const getPolyfillCandidates = importedModule => {
|
||||
const normalizedImportedModule = stripPolyfillPrefix(importedModule);
|
||||
if (!normalizedImportedModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstCharacter = normalizedImportedModule[0];
|
||||
const tokens = polyfillTokensByFirstCharacter.get(firstCharacter);
|
||||
if (!tokens) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates = new Set();
|
||||
const firstSegment = getFirstSegment(normalizedImportedModule);
|
||||
if (firstSegment === normalizedImportedModule) {
|
||||
for (const token of tokens) {
|
||||
if (token === 'es') {
|
||||
if (!isPotentialEsPrefix(normalizedImportedModule)) {
|
||||
continue;
|
||||
}
|
||||
} else if (!normalizedImportedModule.startsWith(token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const polyfill of polyfillsByToken.get(token)) {
|
||||
candidates.add(polyfill);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const token of tokens) {
|
||||
if (
|
||||
token === 'es'
|
||||
|| !firstSegment.startsWith(token)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const polyfill of polyfillsByToken.get(token)) {
|
||||
candidates.add(polyfill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPotentialEsPrefix(normalizedImportedModule)) {
|
||||
for (const polyfill of polyfillsByToken.get('es') || []) {
|
||||
candidates.add(polyfill);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
};
|
||||
|
||||
function getTargets(options, dirname) {
|
||||
if (options?.targets) {
|
||||
return options.targets;
|
||||
}
|
||||
|
||||
const packageJsonResult = readPackageJson(dirname);
|
||||
|
||||
if (!packageJsonResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {browserslist, engines} = packageJsonResult.packageJson;
|
||||
return browserslist ?? engines;
|
||||
}
|
||||
|
||||
function create(context) {
|
||||
const targets = getTargets(context.options[0], path.dirname(context.filename));
|
||||
if (!targets) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unavailableFeatures;
|
||||
try {
|
||||
unavailableFeatures = coreJsCompat({targets}).list;
|
||||
} catch {
|
||||
// This can happen if the targets are invalid or use unsupported syntax like `{node:'*'}`.
|
||||
return;
|
||||
}
|
||||
|
||||
const unavailableFeatureSet = new Set(unavailableFeatures);
|
||||
|
||||
// When core-js graduates a feature from `esnext` to `es`, the entries list both (e.g. `['es.regexp.escape', 'esnext.regexp.escape']`),
|
||||
// but `coreJsCompat` only includes the `es` version in its unavailable list, making the `esnext` version appear "available".
|
||||
// To avoid false positives, treat `esnext.*` features as unavailable when their `es.*` counterpart is already in the list.
|
||||
const checkFeatures = features => !features.every(feature =>
|
||||
unavailableFeatureSet.has(feature)
|
||||
|| (feature.startsWith('esnext.') && features.includes(feature.replace('esnext.', 'es.'))));
|
||||
|
||||
context.on('Literal', node => {
|
||||
if (
|
||||
!(
|
||||
(['ImportDeclaration', 'ImportExpression'].includes(node.parent.type) && node.parent.source === node)
|
||||
|| (isStaticRequire(node.parent) && node.parent.arguments[0] === node)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importedModule = node.value;
|
||||
if (typeof importedModule !== 'string' || ['.', '/'].includes(importedModule[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coreJsModuleFeatures = coreJsEntries[importedModule.replace('core-js-pure', 'core-js')];
|
||||
|
||||
if (coreJsModuleFeatures) {
|
||||
if (coreJsModuleFeatures.length > 1) {
|
||||
if (checkFeatures(coreJsModuleFeatures)) {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID_CORE_JS,
|
||||
data: {
|
||||
coreJsModule: importedModule,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (!unavailableFeatureSet.has(coreJsModuleFeatures[0])) {
|
||||
return {node, messageId: MESSAGE_ID_POLYFILL};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const polyfillCandidates = getPolyfillCandidates(importedModule.toLowerCase());
|
||||
if (!polyfillCandidates) {
|
||||
return;
|
||||
}
|
||||
|
||||
const polyfill = polyfillCandidates.find(({pattern}) => pattern.test(importedModule));
|
||||
if (polyfill) {
|
||||
const [, namespace, method = ''] = polyfill.feature.split('.');
|
||||
const features = coreJsEntries[`core-js/full/${namespace}${method && '/'}${method}`];
|
||||
|
||||
if (features && checkFeatures(features)) {
|
||||
return {node, messageId: MESSAGE_ID_POLYFILL};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['targets'],
|
||||
properties: {
|
||||
targets: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: 'A browserslist query string.',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
description: 'An array of browserslist query strings.',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
description: 'A browserslist targets object.',
|
||||
},
|
||||
],
|
||||
description: 'The target environments.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce the use of built-in methods instead of unnecessary polyfills.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
schema,
|
||||
// eslint-disable-next-line eslint-plugin/require-meta-default-options
|
||||
defaultOptions: [],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user