@masknet/eslint-plugin
Version:
eslint plugin for masknet
208 lines (194 loc) • 7.33 kB
text/typescript
import type { TSESTree } from '@typescript-eslint/types'
import type { ReportFixFunction, RuleContext, RuleFixer, SourceCode } from '@typescript-eslint/utils/ts-eslint'
import { AST_NODE_TYPES } from '@typescript-eslint/types'
import { createRule } from '../rule.js'
export interface Options {
deferPackages?: string[]
eagerPackages?: string[]
syntax: 'webpack-magic-comment'
}
export default createRule({
name: 'prefer-defer-import',
meta: {
type: 'problem',
fixable: 'code',
docs: {
description:
'Prefer defer import a module. See <https://github.com/tc39/proposal-defer-import-eval> and <https://github.com/webpack/webpack/pull/16567/>.',
},
schema: [
{
type: 'object',
oneOf: [
{
type: 'object',
additionalProperties: false,
properties: {
deferPackages: { type: 'array', items: { type: 'string' }, minItems: 0 },
syntax: { type: 'string', enum: ['webpack-magic-comment'] },
},
},
{
type: 'object',
additionalProperties: false,
properties: {
eagerPackages: { type: 'array', items: { type: 'string' }, minItems: 0 },
syntax: { type: 'string', enum: ['webpack-magic-comment'] },
},
},
],
},
],
messages: {
prefer: 'Prefer import the package with defer syntax.',
},
},
resolveOptions(options?: Options) {
if (options?.deferPackages && options.eagerPackages)
throw new Error('Cannot use both deferPackages and eagerPackages in the rule prefer-defer-import.')
return options
},
create(context, options: Options) {
function handle(
node: TSESTree.ImportDeclaration | TSESTree.ExportAllDeclaration | TSESTree.ExportNamedDeclaration,
) {
if (!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: TSESTree.ImportDeclaration | TSESTree.ExportAllDeclaration | TSESTree.ExportNamedDeclaration,
code: Readonly<SourceCode>,
options: Options,
) {
if (!node.source) return false
if (node.type === AST_NODE_TYPES.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 === AST_NODE_TYPES.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
for (const comment of code.getCommentsInside(node)) {
if (comment.value.includes('webpackDefer: true')) return false
}
return true
}
function makeFixer(
context: Readonly<RuleContext<string, unknown[]>>,
node: TSESTree.ImportDeclaration | TSESTree.ExportAllDeclaration | TSESTree.ExportNamedDeclaration,
): ReportFixFunction | null {
if (node.type === AST_NODE_TYPES.ExportNamedDeclaration || !node.source) return null
if (
context.sourceCode
.getDeclaredVariables(node)
.some((x) => x.references.some((x) => x.identifier.parent?.type === AST_NODE_TYPES.ExportSpecifier))
)
return null
return function* (fixer) {
if (node.type === AST_NODE_TYPES.ExportAllDeclaration && node.exported) {
yield fixer.insertTextBefore(node.source, `/* webpackDefer: true */ `)
}
if (node.type !== AST_NODE_TYPES.ImportDeclaration) return
// reuse the name from the original import if possible
const suggestedName =
(
node.specifiers.find((x) => x.type === AST_NODE_TYPES.ImportNamespaceSpecifier) as
| TSESTree.ImportNamespaceSpecifier
| undefined
)?.local.name ??
(
node.specifiers.find((x) => x.type === AST_NODE_TYPES.ImportDefaultSpecifier) as
| TSESTree.ImportDefaultSpecifier
| undefined
)?.local.name ??
node.source.value.replaceAll(/[^\dA-Za-z]/g, '_')
{
let imports = `import * as ${suggestedName} from /* webpackDefer: true */ ${node.source.raw}`
let assertions = ''
const assertionList: string[] = []
if (node.assertions.length > 0) {
assertions += ` assert {`
for (const assertion of node.assertions) {
assertionList.push(
` ${assertion.key.type === AST_NODE_TYPES.Literal ? assertion.key.raw : assertion.key.name}: ${
assertion.value.raw
}`,
)
}
assertions += assertionList.join(', ') + ' }'
}
imports += assertions
imports += ';'
let typeOnlyImports = 'import type { '
const typeOnlyImportList: string[] = []
let useTypeOnlyImports = false
for (const spec of node.specifiers) {
if (spec.type === AST_NODE_TYPES.ImportSpecifier && spec.importKind === 'type') {
useTypeOnlyImports = true
typeOnlyImportList.push(
spec.imported.name === spec.local.name ? spec.imported.name : `${spec.imported.name} as ${spec.local.name}`,
)
}
}
if (useTypeOnlyImports) {
typeOnlyImports += typeOnlyImportList.join(', ') + ' } from ' + node.source.raw + assertions + ';'
imports += ' '
imports += typeOnlyImports
}
if (node.specifiers.every((x) => x.type === AST_NODE_TYPES.ImportSpecifier && x.importKind === 'type')) {
yield fixer.replaceText(node, typeOnlyImports)
return
}
yield fixer.replaceText(node, imports)
}
for (const spec of node.specifiers) {
if (spec.type === AST_NODE_TYPES.ImportNamespaceSpecifier) continue
if (spec.type === AST_NODE_TYPES.ImportDefaultSpecifier) {
yield* replaceAllReference(context, spec, `${suggestedName}.default`, fixer)
} else {
if (spec.importKind === 'type') continue
const imported = spec.imported.name
yield* replaceAllReference(
context,
spec,
/^\p{ID_Start}\p{ID_Continue}{0,}$/u.test(imported)
? `${suggestedName}.${imported}`
: `${suggestedName}[${JSON.stringify(imported)}]`,
fixer,
)
}
}
}
}
function* replaceAllReference(
context: Readonly<RuleContext<string, unknown[]>>,
node: TSESTree.Node,
replacement: string,
fixer: RuleFixer,
) {
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
// eslint-disable-next-line unicorn/prefer-ternary
if (id.parent?.type === AST_NODE_TYPES.Property && id.parent.shorthand && id.parent.value === id) {
yield fixer.replaceText(id.parent, `${id.name}: ${replacement}`)
} else {
yield fixer.replaceText(id, replacement)
}
}
}