@masknet/eslint-plugin
Version:
eslint plugin for masknet
170 lines • 7.33 kB
JavaScript
import { createRule } from "../rule.js";
import { SyntaxKind } from 'typescript';
export default createRule({
name: 'prefer-defer-import',
meta: {
type: 'problem',
fixable: 'code',
docs: {
description: 'Prefer defer import a module.',
},
schema: [
{
type: 'object',
oneOf: [
{
type: 'object',
additionalProperties: false,
properties: {
deferPackages: { type: 'array', items: { type: 'string' }, minItems: 0 },
},
},
{
type: 'object',
additionalProperties: false,
properties: {
eagerPackages: { type: 'array', items: { type: 'string' }, minItems: 0 },
},
},
],
},
],
messages: {
prefer: 'Prefer import the package with defer syntax.',
},
},
defaultOptions: [{ deferPackages: undefined, eagerPackages: undefined }],
create(context, [options]) {
if (options.deferPackages === undefined && options.eagerPackages === undefined) {
return {};
}
if (options.deferPackages && options.eagerPackages)
throw new Error('Cannot use both deferPackages and eagerPackages in the rule prefer-defer-import.');
function handle(node) {
if (!options || !needFix(node, context.sourceCode, options))
return;
const fix = makeFixer(context, node);
context.report({ node, messageId: 'prefer', fix });
}
return {
ImportDeclaration: handle,
ExportAllDeclaration: handle,
ExportNamedDeclaration: handle,
};
},
});
function needFix(node, code, options) {
if (!node.source)
return false;
if (node.type === 'ImportDeclaration' && (node.importKind === 'type' || node.specifiers.length === 0))
return false;
const [char] = node.source.value;
// nodejs import map, relative url, absolute url
if (char === '#' || char === '.' || char === '/')
return false;
if (node.type === 'ExportNamedDeclaration' && (node.exportKind === 'type' || node.specifiers.length === 0))
return false;
if (options.deferPackages && !options.deferPackages.includes(node.source.value))
return false;
if (options.eagerPackages?.includes(node.source.value))
return false;
if (node.type === 'ImportDeclaration') {
// TODO: https://github.com/typescript-eslint/typescript-eslint/issues/11389
// typescript-eslint does not provide phaseModifier yet.
const tsNode = code.parserServices?.esTreeNodeToTSNodeMap?.get(node);
if (tsNode) {
if (tsNode.importClause?.phaseModifier === SyntaxKind.DeferKeyword) {
return false;
}
}
else {
if (code.getText(node).replaceAll(/\s+/g, ' ').includes('import defer'))
return false;
}
}
return true;
}
function makeFixer(context, node) {
if (node.type === 'ExportNamedDeclaration' || !node.source)
return null;
if (context.sourceCode
.getDeclaredVariables(node)
.some((x) => x.references.some((x) => x.identifier.parent?.type === 'ExportSpecifier')))
return null;
return function* (fixer) {
if (node.type !== 'ImportDeclaration')
return;
// reuse the name from the original import if possible
const suggestedName = node.specifiers.find((x) => x.type === 'ImportNamespaceSpecifier')?.local.name ??
node.specifiers.find((x) => x.type === 'ImportDefaultSpecifier')
?.local.name ??
node.source.value.replaceAll(/[^\dA-Za-z]/g, '_');
{
let imports = `import defer * as ${suggestedName} from ${node.source.raw}`;
let attributes = '';
const assertionList = [];
if (node.attributes.length > 0) {
attributes += ` assert {`;
for (const assertion of node.attributes) {
assertionList.push(` ${assertion.key.type === 'Literal' ? assertion.key.raw : assertion.key.name}: ${assertion.value.raw}`);
}
attributes += assertionList.join(', ') + ' }';
}
imports += attributes;
imports += ';';
let typeOnlyImports = 'import type { ';
const typeOnlyImportList = [];
let useTypeOnlyImports = false;
for (const spec of node.specifiers) {
if (spec.type === 'ImportSpecifier' && spec.importKind === 'type') {
useTypeOnlyImports = true;
typeOnlyImportList.push(spec.imported.type === 'Identifier' && spec.imported.name === spec.local.name
? spec.imported.name
: `${spec.imported.type === 'Identifier' ? spec.imported.name : JSON.stringify(spec.imported.value)} as ${spec.local.name}`);
}
}
if (useTypeOnlyImports) {
typeOnlyImports += typeOnlyImportList.join(', ') + ' } from ' + node.source.raw + attributes + ';';
imports += ' ';
imports += typeOnlyImports;
}
if (node.specifiers.every((x) => x.type === 'ImportSpecifier' && x.importKind === 'type')) {
yield fixer.replaceText(node, typeOnlyImports);
return;
}
yield fixer.replaceText(node, imports);
}
for (const spec of node.specifiers) {
if (spec.type === 'ImportNamespaceSpecifier')
continue;
if (spec.type === 'ImportDefaultSpecifier') {
yield* replaceAllReference(context, spec, `${suggestedName}.default`, fixer);
}
else {
if (spec.importKind === 'type')
continue;
const imported = spec.imported;
yield* replaceAllReference(context, spec, imported.type === 'Identifier' && /^\p{ID_Start}\p{ID_Continue}{0,}$/u.test(imported.name)
? `${suggestedName}.${imported.name}`
: `${suggestedName}[${JSON.stringify(imported.type === 'Identifier' ? imported.name : imported.value)}]`, fixer);
}
}
};
}
function* replaceAllReference(context, node, replacement, fixer) {
const variables = context.sourceCode.getDeclaredVariables(node);
if (variables.length !== 1)
throw new Error('Expected exactly one reference');
if (replacement === variables[0].name)
return;
for (const reference of variables[0].references) {
const id = reference.identifier;
if (id.parent?.type === 'Property' && id.parent.shorthand && id.parent.value === id) {
yield fixer.replaceText(id.parent, `${id.name}: ${replacement}`);
}
else {
yield fixer.replaceText(id, replacement);
}
}
}
//# sourceMappingURL=prefer-defer-import.js.map