npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hans774882968/eslint-plugin-use-i18n

v1.0.7

Published

The function of this eslint plugin is to remind you to use i18n() method correctly

Readme

引言

看到参考链接1以后,觉得用TS写一个eslint插件应该很~~简单🐔⌨️🍚~~,尝试下来确实如此。

前置知识

本文假设

  • 你对AST遍历有所了解。
  • 你写过单测用例。

作者:hans774882968以及hans774882968以及hans774882968

本文52pojie:https://www.52pojie.cn/thread-1742210-1-1.html

本文CSDN:https://blog.csdn.net/hans774882968/article/details/128891004

本文juejin:https://juejin.cn/post/7196517835379900477

Usage

npm install -D @hans774882968/eslint-plugin-use-i18n
# or
yarn add -D @hans774882968/eslint-plugin-use-i18n

then in .eslintrc.js

module.exports = {
  plugins: [
    // other plugins ...
    '@hans774882968/use-i18n',
  ],
  extends: [
    // other extends ...
    'plugin:@hans774882968/use-i18n/all',
  ],
  rules: {
    // override other rules ...
    '@hans774882968/use-i18n/i18n-usage': ['error', {
      i18nFunctionNames: ['$i18n', '$t'],
    }],
  }
}

rules:

  • no-console
  • i18n-usage

Rule: i18n-usage

  • legal: $gt('abc'), $gt('hello {world}', null, { world: 'world' })
  • illegal: $gt(), $gt(12), $gt(1 + 2), $gt(null), $gt(undefined), $gt(x), $gt(x, null, {})

第一个eslint规则:no-console

为了简单,我们只使用tsc进行构建。首先package.json需要设置入口"main": "dist/index.js",tsconfig.json需要设置"outDir": "dist""include": ["src"]。接下来设计一下单元测试和构建命令:

"scripts": {
  "clean": "rm -Rf ./dist/",
  "build": "yarn clean && mkdir ./dist && tsc",
  "test": "jest",
  "test:help": "jest --help",
  "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \"test/**/*.{js,jsx,ts,tsx}\" \"*.{js,jsx,ts,tsx}\" --fix"
},

ESLintUtils.RuleTester写的.test.tsmocha或者jest都能运行,我选择了jest

当我们运行yarn lint时,node_modules/@eslint/eslintrc/lib/config-array-factory.jsthis._loadPlugin会加载插件,相当于在node环境运行上面指定的入口点dist/index.js。所以我们需要知道@eslint如何描述一个eslint插件,才能写出src/index.ts。查看this._loadPlugin可知,plugin.definition的类型应为:

type Plugin = {
  configs?: Record<string, ConfigData> | undefined;
  environments?: Record<string, Environment> | undefined;
  processors?: Record<string, Processor> | undefined;
  rules?: Record<...> | undefined;
}

结合参考链接1,我们得出结论:一般来说需要提供rulesconfigs属性。rules可以理解为具体的规则定义;configs可以理解为规则的集合,可以称为“最佳实践”,最常见的configsrecommended

于是写出src/index.ts

import rules from './rules';
import configs from './configs';

const configuration = {
  rules,
  configs
};

export = configuration;

src/rules/index.ts

import noConsole from './noConsole';

const allRules = {
  'no-console': noConsole
};

export default allRules;

src/configs/index.ts

import all from './all';
import recommended from './recommended';

const allConfigs = {
  all,
  recommended
};

export default allConfigs;

src/configs/all.ts

export default {
  parser: '@typescript-eslint/parser',
  parserOptions: { sourceType: 'module' },
  rules: {
    '@hans774882968/use-i18n/no-console': 'error'
  }
};

我们用createRule函数来创建一条规则,createRule函数定义如下:

import { ESLintUtils } from '@typescript-eslint/utils';

export default ESLintUtils.RuleCreator(
  () => 'rule documentation url'
);

createRule需要传一个对象,我列举一下这个对象常用的几个属性:

  • meta.schema:配置eslint规则的时候可以指定的options参数。通常传入的值为{}(不接收参数)和object[]
  • meta.messages:一个对象,{ messageId: text }
  • create方法:eslint需要建立AST并遍历,所以要拿到这个方法的返回值作为遍历AST的配置。输入参数是context对象,常用的方法有:context.options[0]获取传入的参数;context.getFilename()获取当前yarn lint正在解析的文件名;context.report函数向用户报错,通常这么用:context.report({ node, messageId: 'xxMessageId' })messageId必须符合meta.messages给出的定义。create方法返回的对象有点类似于@babel/traversetraverse方法的第二个参数,具体写法看参考链接1的项目就行。
import { TSESLint, ASTUtils } from '@typescript-eslint/utils';
import createRule from '../utils/createRule';
import path from 'path';
import multimatch from 'multimatch';

// 模仿babel中的写法 import { isIdentifier } from '@babel/types';
const {
  isIdentifier
} = ASTUtils;

const whiteList = ['memory'];

const rule = createRule({
  name: 'no-console',
  meta: {
    docs: {
      description: 'Remember to delete console.method()',
      recommended: 'error',
      requiresTypeChecking: false
    },
    messages: {
      rememberToDelete: 'Remember to delete console.method()'
    },
    type: 'problem',
    schema: {}
  },
  create (
    context: Readonly<TSESLint.RuleContext<'rememberToDelete', never[]>>
  ) {
    return {
      MemberExpression (node) {
        if (isIdentifier(node.object) && node.object.name === 'console' &&
            isIdentifier(node.property) && Object.prototype.hasOwnProperty.call(console, node.property.name) &&
            !whiteList.includes(node.property.name)
        ) {
          context.report({ node, messageId: 'rememberToDelete' });
        }
      }
    };
  }
});

export default rule;

代码传送门:src/rules/noConsole.ts

本地测试

单元测试:

yarn test

测试用例的编写

这里只是一个简单的介绍,更具体的介绍参见下一个名为《测试用例的编写》的章节。基本格式如下:

import rule from '../src/rules/noConsole';
import { ESLintUtils } from '@typescript-eslint/utils';

const ruleTester = new ESLintUtils.RuleTester({
  parser: '@typescript-eslint/parser'
});

ruleTester.run('no-console', rule, {
  valid: [
    { code }
  ],
  invalid: [
    {
      code,
      options: [
        { param1, param2 }
      ], // 向待测试规则传入的参数
      // 不含有修复建议的eslint错误
      errors: [{ messageId: 'rememberToDelete' }]
    }
  ]
});

在IDE中,将鼠标放上去即可看到类型推断结果和官方解释,单击即可查看完整的类型定义。

本地查看效果

首先:

yarn build

接下来有两种方式模拟已发布的npm包。

(1)在另一个项目(这里用了相对路径,用绝对路径也行):

yarn add -D file:../eslint-plugin-use-i18n-hans

注意:每次重新build后都需要在另一个项目重新yarn add

这样会得到:

{
  "devDependencies": {
    "@hans/eslint-plugin-use-i18n-hans": "file:../eslint-plugin-use-i18n-hans",
  }
}

(2)先在本项目根目录运行yarn link,再在另一个项目:

yarn link @hans774882968/eslint-plugin-use-i18n

这个做法不会改变package.json

但这两种做法都有一个问题:不能检测出应列举在dependencies的包被错误地列举在devDependencies的情况。TODO:佬们教教我如何检测出这种情况。

接下来配置.eslintrc.js

module.exports = {
  plugins: [
    '@hans774882968/use-i18n',
  ],
  extends: [
    'plugin:@hans774882968/use-i18n/recommended',
  ],
}

插件名为@hans774882968/use-i18n,使用了configs中的recommended

最后重启vscode或运行yarn lint就能看到我们的第一个eslint插件生效了。

<path>/file-encrypt/webpack-plugin-utils.js
  16:5  error  Remember to delete console.log()  @hans774882968/use-i18n/no-console

第一个eslint插件第一个规则

no-console规则添加功能:排除用户指定的文件

修改一下meta.schema,新增输入参数:

schema = [
  {
    properties: {
      excludedFiles: {
        type: 'array'
      }
    }
  }
]

和对应的类型定义:

type Options = [{
  excludedFiles: string[];
}];

{
  create (
    context: Readonly<TSESLint.RuleContext<'rememberToDelete', Options>>
  ) {}
}

然后在create函数体加几句判定:

const fileName = context.getFilename();
const options = context.options[0] || {};
const { excludedFiles } = options;
if (Array.isArray(excludedFiles)) {
  const excludedFilePaths = excludedFiles.map(excludedFile => path.resolve(excludedFile));
  if (multimatch([fileName], excludedFilePaths).length > 0) {
    return {};
  }
}

context.getFilename()文档:https://eslint.org/docs/latest/extend/custom-rules#the-context-object 。其特性:在yarn test时会返回file.ts,在作为npm包引入另一个项目后,可以正常获取文件的绝对路径。

为了支持glob语法,我们引入了multimatch。但需要指定版本为5.0.0,因为multimatch6.0.0只支持es module,而我反复尝试都无法找到一个可以生效的jest配置。transformIgnorePatterns等配置项的资料都极少,这篇blog看上去操作性很强,但尝试后依旧无效……TODO:让佬们教教我。

构建完成后,我们可以在另一个项目尝试配置@hans774882968/use-i18n/no-console规则:

'@hans774882968/use-i18n/no-console': ['error', {
  excludedFiles: [
    'add-copyright-plugin.js',
    'copyright-print.js',
    'webpack-plugin-utils.js',
    'src/utils/my-eslint-plugin-tests/no-warn-folder/**/*.js',
    'tests/**/*.js',
    'src/utils/my-eslint-plugin-tests/i18n-tests/*.js',
  ],
}],

.eslintrc.js取消或添加注释并保存,vscode应该能立刻看到报错的产生和消失。

TODO:是否能够mock context.getFilename(),让本地可以写测试用例?

i18n-usage规则:检测不合法的i18n方法使用方式

在Vue里,我们通过调用i18n方法来实现国际化。于是我们可能会希望实现一个eslint规则,指出用户调用i18n方法的方式不合法。输入参数:i18nFunctionNames: string[],指定是i18n的方法名,也就是这条eslint规则的检测范围,比如['$gt', '$t', '$i18n']。不合法情形:

  • 不传入参数。如:$gt()
  • 第一个参数不是字符串字面量(String Literal)。如:$gt(12), $gt(1 + 2), $gt(null), $gt(undefined), $gt(x), $gt(x, null, {})

从本质上来说,实现它并不比上文的no-console规则难。所以我仅指出实现上的注意点:

  1. 规则的meta.messages的一条消息可以是string template,context.report可以向string template传入参数。比如:meta.messages = { a: '{{var}}' }context.report({ node, messageId: 'a', data: { var } }),我们通过data属性向消息的string template传入参数。这个功能有什么用呢?我们在给出eslint提示的时候,希望给出我们检测出的用户正在使用的i18n方法名,就可以用这个功能实现。
  2. 面对判断节点类型的需求,@typescript-eslint/utilsTSESTree确实不如@babel/types好用。我们可以用ASTUtils.isIdentifier判断node is TSESTree.Identifier,但对于MemberExpression等类型,则需要node.type === AST_NODE_TYPES.MemberExpression来判定。更麻烦的一个例子是:为了检测字符串字面量,我不得不使用node.type !== AST_NODE_TYPES.Literal || typeof node.value !== 'string'。TODO:是否能找到更好的判定方式?
  3. eslint单测ESLintUtils.RuleTester的测试用例,可以指定errors: TestCaseError<messageId[]>[]这个数组,errorsTestCaseError应按文本从上往下列出。

代码传送门

构建完成后,另一个项目的eslint配置:

module.exports = {
  plugins: [
    '@hans774882968/use-i18n',
  ],
  extends: [
    'plugin:@hans774882968/use-i18n/all',
  ],
  rules: {
    '@hans774882968/use-i18n/i18n-usage': ['error', {
      i18nFunctionNames: ['$i18n', '$t'],
    }],
  }
}

效果:

2-i18n方法用法检测效果图

i18n-usage规则:自动修复功能——eslint fix函数的使用

接下来我们为上面实现的i18n-usage规则添加一个自动修复功能。为了优化用户体验,我们约定:

  • 对于$i18n(x)这种第一个参数是标识符的情况,修复为$i18n('{x}', null, { x })
  • 对于$i18n(a[0]+a[1]*a[2])这种第一个参数不是标识符的情况,修复为$i18n('{value}', null, { value: a[0] + a[1] * a[2] })。注意这里进行了格式化,因为我在实现时使用了escodegen来输出第一个参数的AST节点的代码。
  • 对于$i18n(arg0, arg1, ...)这种多于1个参数,和没有参数的情况,不修复。

另外:

  1. 要求在执行yarn lint时自动修复。为了实现这条需求,我们可以为context.report方法传入的对象指定一个fix函数。
  2. 在vscode可以看到“快速修复”的建议,我们希望在那显示一条建议信息。为了实现这条需求,我们可以为context.report方法传入的对象指定一个suggest数组。

下面是一个能同时实现上面两点需求的简单🌰:

context.report({
  *fix(fixer) {
    yield fixer.replaceText(node, 'str');
    yield fixer.insertBefore(node, 'str');
  },
  suggest: [
    {
      messageId: 'autofixFirstArgSuggest',
      data: { i18nFunctionName, replaceResult },
      *fix (fixer) {
        yield fixer.replaceText(node, 'str');
        yield fixer.insertBefore(node, 'str');
      }
    }
  ]
});

fixer提供的方法可在IDE中点击变量查看:

interface RuleFixer {
  insertTextAfter(nodeOrToken: TSESTree.Node | TSESTree.Token, text: string): RuleFix;
  insertTextAfterRange(range: Readonly<AST.Range>, text: string): RuleFix;
  insertTextBefore(nodeOrToken: TSESTree.Node | TSESTree.Token, text: string): RuleFix;
  insertTextBeforeRange(range: Readonly<AST.Range>, text: string): RuleFix;
  remove(nodeOrToken: TSESTree.Node | TSESTree.Token): RuleFix;
  removeRange(range: Readonly<AST.Range>): RuleFix;
  replaceText(nodeOrToken: TSESTree.Node | TSESTree.Token, text: string): RuleFix;
  replaceTextRange(range: Readonly<AST.Range>, text: string): RuleFix;
}

实现修复功能唯一的难点是:第一个参数不是标识符的情况如何修复。有一种可行的方式是:

yield fixer.insertTextBefore(args[0], '\'{value}\', null, { value: ');
yield fixer.insertTextAfter(args[0], ' }');

但后来了解到,类似于@babel/generatorescodegen这个包可以输入espree(eslint所使用的AST)的AST节点,输出代码。因此我们有了更简单的实现方式:

const args0Code = escodegen.generate(args[0]);
yield fixer.replaceText(args[0], `'{value}', null, { value: ${args0Code} }`);

代码传送门

测试用例的编写

添加自动修复功能后,测试用例的格式会有些变化。代码传送门

const v = {
  code: basicCaseInputCodes[6],
  options: [
    { i18nFunctionNames: ['$i18n'] }
  ], // 向待测试规则传入的参数
  output: basicCaseOutputCodes[6], // 所有修复都应用后输出的完整代码
  errors: [
    {
      messageId: 'firstArgShouldBeString',
      // 修复建议的数组
      suggestions: [
        {
          // 显示在IDE上的修复建议
          messageId: 'autofixFirstArgSuggest',
          data: { i18nFunctionName, replaceResult },
          // 这条修复建议应用后输出的完整代码
          output
        }
      ]
    }
    // 不含有修复建议的eslint错误
    { messageId: 'parameter' },
    // ...
  ]
}

需要注意“两个output”的区别:output是所有修复都应用后输出的完整代码errors[i].suggestion[j].output只有这条修复建议应用后输出的完整代码。官方解释如下:

  • output:The expected code after autofixes are applied. If set to null, the test runner will assert that no autofix is suggested.
  • errors[i].suggestion[j].output:Suggestions will be applied as a stand-alone change, without triggering multi-pass fixes. Each individual error has its own suggestion, so you have to show the correct, isolated output for each suggestion.

效果

修复前:

3-1-i18n-usage-修复前

format on save修复后:

3-2-i18n-usage-修复后

修复建议:

3-3-i18n-usage-修复建议

发布npm包

参考链接3

首次发布包

  1. 首先npm config set registry https://registry.npmjs.org/确保源地址为官方源。

  2. 在 https://www.npmjs.com/ 注册账号。之后在命令行执行npm login登录。可以用npm whoami确保自己已经登录成功。

  3. npm publish发布包。但如果你的包名以@开头,即使用了命名空间,则需要保证:

  4. @后面跟的是自己的账号名,否则会报错404 You should bug the author to publish it (or use the name yourself!)参考链接4是对的,Stack Overflow上说的,重新npm login、使用NPM_TOKENNPM_TOKEN=xxx npm publish --access public)等方式也可以作为备选项。

  5. 发布私有包是要钱💰的,而使用命名空间后npm会默认这是一个私有包,直接发布会报错402 Payment Required,所以需要声明为公有包。做法有:(1)npm publish --access public。(2)package.json配置publishConfig。(3)npm config set access publicnpm publish

{
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  }
}

后续发布

package.jsonversion没变就不能再次发布。可以手动改版本号。也可以用npm version patch来升版本,注意:这条命令会自动产生一次commit,可以直接push。

npm version可用参数如下:

// patch:补丁号,修复bug,小变动,如 1.0.0 -> 1.0.1
npm version patch

// minor:次版本号,增加新功能,如 1.0.0 -> 1.1.0
npm version minor

// major:主版本号,不兼容的修改,如 1.0.0 -> 2.0.0
npm version major

标记某个版本为deprecated

npm deprecate <package_name>@<version> "deprecate reason"

一般不建议使用unpublish,而是用deprecated代替。

修复vue文件的Parsing error

这个版本有一个小问题:对于有vue文件的项目,报错error Parsing error: '>' expected

我一开始想用context.getFilename()获取文件后缀名,避免解析vue文件。但看到参考链接7后尝试了一下,发现也可行。

src/configs/all.ts为例,其他config同理。首先yarn add vue-eslint-parser,然后把parser: '@typescript-eslint/parser'修改为:

export default {
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    sourceType: 'module'
  },
  rules: {
    '@hans774882968/use-i18n/no-console': 'error',
    '@hans774882968/use-i18n/i18n-usage': 'error'
  }
};

参考资料

  1. 值得参考的教程:https://www.darraghoriordan.com/2021/11/06/how-to-write-an-eslint-plugin-typescript/
  2. eslint有编写自定义规则的官方文档:https://eslint.org/docs/latest/extend/custom-rules
  3. https://juejin.cn/post/7170635418549878814
  4. npm publish包报404,is not in the npm registry错误:https://juejin.cn/post/7143988072403697701
  5. https://stackoverflow.com/questions/39115101/getting-404-when-attempting-to-publish-new-package-to-npm
  6. https://reactjs.org/docs/how-to-contribute.html
  7. 同时使用@typescript-eslint/parservue-eslint-parser:https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser