UNPKG

@masknet/eslint-plugin

Version:
208 lines (194 loc) 7.33 kB
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) } } }