ESLint package.json scripts 命名规范深度解析:从 ABNF 语法到仓库实战
2026/9/10 20:15:33 网站建设 项目流程

ESLint package.json scripts 命名规范深度解析:从 ABNF 语法到仓库实战

【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint

package.jsonscripts字段是 npm 生态中每个项目最常用的"任务入口",但当项目规模增长到 ESLint 这种程度——仓库内同时存在根目录、docs目录以及packages/*多个 npm 包,几十个脚本名如何做到一眼可读、机器可排序、贡献者可预判?ESLint 用一份贡献者约定文档 docs/src/contribute/package-json-conventions.md 给出了完整答案。本篇以该文档为骨架,结合仓库中 package.json、docs/package.json、packages/eslint-config-eslint/package.json 等真实配置与 Makefile.js 的实现细节,系统讲解这套命名体系:读完你将掌握脚本命名的主名、修饰符、排序规则,理解lint:fix:docs:js这类长名字的组成逻辑,并能据此为任何 npm 项目设计一致、可扩展的脚本集。

适用范围:约定只作用于scripts

规范开宗明义:该约定仅适用于package.json文件中的scripts部分,不涉及dependenciesdevDependenciesexportsfiles等其他字段。这意味着:

  • 每个脚本名都必须符合下述命名文法;
  • 脚本所执行的命令本身不受本规范约束(例如eslintprettierwebpack的具体参数写法保持自由);
  • 仓库内所有 npm 包都应遵守同一套约定,保证跨目录、跨包的可预期性。

在 ESLint 仓库中,这类"被规范约束"的package.json不止一份,最典型的有:根目录 package.json(ESLint 主包,版本 10.9.1)、文档站点 docs/package.json(docs-eslint,私有包)、packages/eslint-config-eslint/package.json(官方共享配置)以及 packages/js/package.json(@eslint/js语言实现)。它们的scripts都能在下面的文法框架内被完整解析。

命名规范:字符集、单词与 ABNF 文法

允许的字符与组成部分

脚本名只能由小写字母、:-+组成,且各有分工:

字符用途示例
小写字母单词本身lintbuild
:分隔不同的"部分"(part)lint:fix:docs:js
-在单个部分内分隔多个单词update-links
+分隔被影响的文件扩展名列表lint:js+cjs(列表需按字母序)

除此之外,每个部分的名字(part name)应要么是完整的英文单词(如coverage而非缩写cov),要么是全小写的通用缩写(initialism,如wasm)。这条规则的目的是:让脚本名无需查文档即可被人类与搜索工具理解。

ABNF 文法

规范将命名规则形式化为如下 ABNF 摘要,这是整份约定的"语法核心":

name = life-cycle / main target? option* ":watch"? life-cycle = "prepare" / "preinstall" / "install" / "postinstall" / "prepublish" / "preprepare" / "prepare" / "postprepare" / "prepack" / "postpack" / "prepublishOnly" main = "build" / "lint" ":fix"? / "fmt" ":check"? / "release" / "start" / "test" / "fetch" target = ":" word ("-" word)* / extension ("+" extension)* option = ":" word ("-" word)* word = ALPHA + extension = ( ALPHA / DIGIT )+

逐行解读:

  • name要么是一个 npm 生命周期脚本名(life-cycle,这些名字由 npm 预定义、不受本约定管束,见下文),要么是main主名后跟可选的target、若干个option,以及可选的:watch结尾。注意文法中main target? option* ":watch"?的顺序,它同时约束了修饰符的排列次序。
  • main是七个固定主名之一:buildlintfmtreleasestarttestfetch。其中lint内置可选:fixfmt内置可选:check,这是文法层面对两个高频修饰符的特殊支持。
  • target:引导,单词间可用-连接;或者直接使用由+连接的扩展名列表(扩展名允许数字,如jscjslesscss3)。
  • optiontarget结构相同,是"放不进其他修饰符"的补充选项。
  • word必须是至少一个英文字母;extension允许字母或数字。

一个完整的名字示例:lint:fix:docs:js可以解析为main = lintoption/fix = fixtarget = docsoption = js。而build:docs:update-links则是main = buildtarget = docsoption = update-links

生命周期脚本:唯一豁免

npm 自身预定义的生命周期脚本(preinstallinstallpostinstallprepublishprepareprepackprepublishOnly等)是命名规范的唯一例外——这些名字由 npm 强制规定,项目无法自定义,因此规范明确它们不要求遵守main命名。仓库中的真实案例是 packages/eslint-config-eslint/package.json 中的"prepublish": "npm test":在发布前自动执行测试,这是生命周期脚本的典型用途。

排序规范:字母顺序即逻辑分组

scripts中的脚本名必须按字母顺序排列(MUST)。这份规范的设计巧妙之处在于:只要每个脚本遵循上文的主名 + 修饰符文法,字母顺序就会恰好与逻辑分组重合

原因很简单:同一主名下的脚本共享前缀。以根 package.json 为例,build:...系列的脚本天然聚集在一起,test:...系列聚集在一起:

"build:docs:update-links": "node tools/fetch-docs-links.js", "build:site": "node Makefile.js gensite", "build:webpack": "node Makefile.js webpack", "build:readme": "node tools/update-readme.js", "build:rules-index": "node Makefile.js generateRuleIndexPage",

读者扫一眼就能知道"构建类脚本都在这一块"。同时,lint:fix排在lint:fix:docs:js之前(短名先于其长变体),也符合字母序与层级感的双重直觉。值得说明的是,仓库中个别历史脚本在排列顺序上存在细微出入,但命名文法本身在所有包中都得到了贯彻——这说明该规范是贡献者维护时的"目标状态",而非一次性强制校验。

主脚本名:七个固定前缀与它们的语义

除生命周期脚本外,所有脚本名必须以以下七个名字之一开头。每个主名都有明确、互斥的语义边界:

Build:由源码/数据生成文件

生成一组文件(从源代码或数据)的脚本,名字必须以build开头。仓库中 docs/package.json 是典型代表,它把文档站点的完整构建拆成了并行子任务:

"build": "npm-run-all build:sass build:postcss build:website build:minify-images", "build:postcss": "postcss src/assets/css -d src/assets/css", "build:sass": "sass src/assets/scss:src/assets/css --no-source-map", "build:website": "npx @11ty/eleventy", "build:minify-images": "imagemin '_site/assets/images' --out-dir='_site/assets/images'"

规则同时规定:如果包内存在多个build:*脚本,可以(MAY)提供一个聚合的build脚本,其输出(SHOULD)等于逐个运行各build:*的总和,且必须(MUST)是这些脚本输出的子集。docs包中build通过npm-run-all依次执行四个子构建,正是这条规则的直接应用。

Fetch:从外部数据/资源生成文件

build类似,但数据来源是外部资源的脚本,前缀必须是fetch。语义差异在于:build的输入是仓库内的源码与数据,fetch的输入在仓库之外(远程接口、第三方站点等)。同样允许存在聚合的fetch脚本,输出规则与build一致(SHOULD 等价、MUST 为子集)。ESLint 中抓取规则文档外部链接的工具 tools/fetch-docs-links.js 被挂载在build:docs:update-links下,从脚本结构上体现了命名约定在实际演进中允许的灵活性。

Release:具有公共副作用

只要脚本会对外部世界产生公开可见的副作用——发布网站、提交 Git、推送远程、发布 npm 包等——就必须以release开头。这是区分"内部构建"与"对外发布"的硬边界。根 package.json 中的发布族脚本完整展示了这一设计:

"release:generate:alpha": "node Makefile.js generatePrerelease -- alpha", "release:generate:beta": "node Makefile.js generatePrerelease -- beta", "release:generate:latest": "node Makefile.js generateRelease -- latest", "release:generate:maintenance": "node Makefile.js generateRelease -- maintenance", "release:generate:rc": "node Makefile.js generatePrerelease -- rc", "release:publish": "node Makefile.js publishRelease"

release:generate:*负责生成版本(打 tag、写 CHANGELOG、更新站点数据),release:publish负责真正发布到 npm 并推送远程——两者共享release前缀,职责却通过optiongeneratepublish)与targetalphabetalatestmaintenancerc)进一步细分。

Lint:静态分析

对文件进行静态分析的脚本(绝大多数情况就是运行 ESLint 自身)必须以lint开头。规则包含两个重要约束:

  1. 存在lint:*子脚本时,提供一个聚合的lint脚本,且它必须运行所有lint:*各自会执行的检查的并集;
  2. 若修复功能可用,linter 不得自动修复,除非脚本名带:fix修饰符——这是"检查"与"修复"在命名上的强制性分离。

根 package.json 的 lint 族对此体现得淋漓尽致:

"lint": "node Makefile.js lint", "lint:docs:js": "node Makefile.js lintDocsJS", "lint:docs:rule-examples": "node Makefile.js checkRuleExamples", "lint:unused": "knip", "lint:fix": "node Makefile.js lint -- fix", "lint:fix:docs:js": "node Makefile.js lintDocsJS -- fix", "lint:rule-types": "node tools/update-rule-type-headers.js --check", "lint:types": "attw --pack"

lint聚合了主代码检查,lint:docs:jslint:docs:rule-examples分别覆盖文档目录中的 JavaScript 与规则示例,lint:typesattw --pack做类型发布检查,lint:unusedknip排查未使用的依赖/导出。而lint:fixlint:fix:docs:js则是各自"只检查版本"的修复对——严格遵循"不带:fix就不修复"的原则。

Fmt:格式化源码

格式化源代码的脚本必须以fmt开头。当存在fmt:*子脚本时,提供两个约定搭档:

  • fmt:对全部源文件应用格式化修复;
  • fmt:check:只校验格式、不修改任何文件,一旦发现不合规就退出非零状态码。

根 package.json 就是最小实现:

"fmt": "prettier --write .", "fmt:check": "prettier --check ."

注意主名是fmt而非format,这是文法中固定的拼写,贡献者不应改写。fmt:check的可被 CI 调用的"非零退出"语义,使格式校验天然适合接入持续集成流水线。

Start:启动服务器

start脚本专用于启动服务器。截至本文写作时,ESLint 仓库中只有 docs/package.json 使用它——用于启动 Eleventy 本地文档服务器并监听文件变化:

"start": "npm-run-all build:sass build:postcss --parallel *:*:watch"

规范指出:目前没有任何 ESLint 包拥有超过一个start脚本,因此暂时无需为start设计修饰符(若未来出现多服务器场景,按文法可用:target指明启动的是哪台服务器)。

Test:验证实际行为符合预期

执行代码以验证实际行为与预期一致的脚本,必须以test开头。规则要求:

  1. 存在test:*子脚本时,提供聚合的test脚本,且它必须运行所有test:*各自测试的并集;
  2. 测试脚本不应包含 lint 检查(检查与测试的关注点分离,避免npm testnpm run lint职责混淆);
  3. 测试脚本尽可能输出测试覆盖率。

根 package.json 的测试族规模最大,覆盖了 CLI、浏览器、模糊测试、性能、生态、类型等维度:

"test": "node Makefile.js test", "test:browser": "node Makefile.js cypress", "test:cli": "mocha", "test:ecosystem": "node tools/test-ecosystem/index.mjs", "test:ecosystem:update": "node tools/test-ecosystem/update.mjs", "test:emfile": "node tools/check-emfile-handling.js", "test:fuzz": "node Makefile.js fuzz", "test:performance": "node Makefile.js perf", "test:pnpm": "cd tests/pnpm && node check.js && pnpm install && pnpm exec tsc", "test:types": "tsc -p tests/lib/types/tsconfig.json && npm run test:types --workspaces --if-present", "test:types:5.3": "npx -p typescript@5.3 -y -- tsc -p tsconfig.types-legacy.json", "test:types:5.x": "npx -p typescript@5.x -y -- tsc -p tsconfig.types.json", "test:types:7.x": "npx -p @typescript/native-preview@latest -y -- tsgo -p tsconfig.types.json", "test:types:all": "npm run test:types && npm run test:types:5.3 && npm run test:types:5.x && npm run test:types:7.x"

test:types:5.3/test:types:5.x/test:types:7.x分别针对不同 TypeScript 版本做类型检查,再由test:types:all聚合——这是"target/option 描述被测试对象"的最佳示范:types是 target,版本号是 option。

修饰符:Fix、Check、Target、Options、Watch

主名之后可以追加一个或多个修饰符。若有多个修饰符,必须严格按下述顺序排列(文法中main target? option* ":watch"?即是对此的编码),例如lint:fix:js:watch合法,而lint:watch:js:fix非法。

Fix

若 linter 能修复发现的问题,应额外提供一个在原脚本名末尾追加:fix的副本,该副本同时执行修复。仓库中lint:fixlint:fix:docs:js就是lintlint:docs:js的修复版。从实现看,两者共享同一 Makefile 目标,只是参数不同:"lint": "node Makefile.js lint""lint:fix": "node Makefile.js lint -- fix"最终都进入 Makefile.js 的target.lint(第 535 行),后者通过-- fix参数将修复开关置真。

Check

若脚本只校验代码或产物而不做任何修改,则追加:check。典型场景是格式化校验(fmt:check)。带:check的脚本绝不能修改任何文件或输出,发现问题应以非零状态退出。仓库中lint:rule-types执行node tools/update-rule-type-headers.js --check也体现了同样语义——--check意味着"只比对、不写回"。

Target

描述动作作用的对象:

  • build脚本的 target 应标识构建产物,例如build:websitebuild:webpack中的websitewebpack(后者实为产物名,指向用 webpack 打出的浏览器包);
  • lint/test脚本的 target 应标识被检查/被测试的对象,如lint:docs:jsdocstest:typestypes
  • start脚本的 target 应标识启动的服务器。

target 可以是一组受影响的文件扩展名,用+连接,多个扩展名应按字母序排列;当扩展名存在变体(如 CommonJS 的cjs与 ESM 的mjs)时,允许使用公共部分js代替逐一罗列(js取代cjs+jsx+mjs)。此外,target 不应是执行动作的工具名——所以文档站点的构建脚本叫build:website而非build:eleventy,尽管它实际运行的是npx @11ty/eleventy(见 docs/package.json)。这条规则把"做什么"与"用什么做"彻底解耦。

Options

放不进上述分类的补充选项,用:引导。例如build:docs:update-links中的update-linksrelease:generate:alpha中的alpha。选项的存在让同一 target 下的多个变体脚本可以平行命名、平行扩展。

Watch

脚本若监听文件系统并对变化做出响应,追加:watch。这是文法中唯一的尾缀。docs 站点开发是典型用例,docs/package.json 中:

"build:postcss:watch": "postcss src/assets/css -d src/assets/css --watch --poll", "build:sass:watch": "sass --watch --poll src/assets/scss:src/assets/css --no-source-map", "build:website:watch": "eleventy --serve --incremental --port=2023"

三个*:watch脚本通过start脚本里的npm-run-all ... --parallel *:*:watch一并并行拉起,构成本地开发服务器——start主名 +:watch修饰符的组合在这里完成了从"构建"到"开发"的语义闭环。

仓库实战:从 npm 脚本名到 Makefile 目标

命名约定不止停留在"看起来整齐",它与仓库的执行层实现严格对齐。ESLint 根包的大量脚本通过node Makefile.js <target> [args]形式调用 Makefile.js(基于 shelljs/make 的构建文件),每个 npm 脚本名与文件中的target.*一一对应:

npm 脚本Makefile 目标实现要点(Makefile.js 行号)
lint/lint:fixtarget.lint([fix])--fix参数控制是否修复,见 第 535 行
lint:docs:js/lint:fix:docs:jstarget.lintDocsJS([fix])用 ESLint 校验 docs 目录 JS,见 第 568 行
lint:docs:rule-examplestarget.checkRuleExamples校验 docs 中的规则示例,见 第 934 行
build:sitetarget.gensite生成文档站点数据,见 第 681 行
build:webpacktarget.webpack打包浏览器版本,见 第 742 行
testtarget.test聚合 checkRuleFiles → mocha → fuzz(150) → checkLicenses,见 第 674 行
test:fuzztarget.fuzz模糊测试,默认 1000 次,见 第 583 行
test:browsertarget.cypress浏览器单元测试,见 第 657 行
test:performancetarget.perf性能对比测试,见 第 1122 行
release:generate:*/release:publishtarget.generatePrerelease/target.generateRelease/target.publishRelease版本生成与发布,见 第 1147-1150 行

这段映射印证了规范的深层价值:脚本名是"做什么"的声明,Makefile 目标/命令是"怎么做"的实现,两者通过一致的命名沟通。贡献者看到test:browser就能直接定位到对应实现;看到release:publish就知道它必然涉及公共副作用(事实也正是如此——target.publishRelease会调用ReleaseOps.publishRelease()发布 npm 包并推送 Git 与站点仓库,见 第 411-451 行)。

另外可以观察到:target.test(第 674 行)依次运行checkRuleFilesmochafuzz({ amount: 150 })checkLicenses没有调用任何 lint 目标——与规范中"测试脚本不应包含 lint 检查"的要求严格一致。

给贡献者的自查清单

在 ESLint 仓库(或任何想借鉴这套体系的项目)中新增package.json脚本时,可按以下清单逐条核对:

  1. 主名前缀正确:这个脚本是生成文件(build/fetch)、发布(release)、静态检查(lint)、格式化(fmt)、启动服务器(start)还是测试(test)?
  2. 字符合法:只有小写字母、:-+;单词是完整英文词或全小写通用缩写。
  3. 修饰符顺序正确maintargetoption*:watchfix必须位于check/target之前(如lint:fix:docs:js)。
  4. 修复不静默:linter 修复能力只暴露在带:fix的脚本里;只校验不改动的脚本带:check
  5. 扩展名列表:用+连接并按字母序;有变体时允许用公共部分。
  6. 不写工具名:target 描述对象,不写eleventywebpack这类工具。
  7. 字母排序:把新脚本放到scripts中应处的位置,让同前缀脚本自然聚组。
  8. 生命周期脚本豁免preinstallprepareprepublish等由 npm 决定名字,不适用本规范。

这套约定并非 ESLint 的私有发明,而是一份可复用的工程模板:它把"脚本命名"从个人风格问题转化为可由 ABNF 校验、可被排序、可被搜索的结构化约定,让一个几十脚本规模的大型 npm 仓库保持长期一致的可维护性。当你下次面对一屏混乱的scripts时,不妨直接照搬这套main:target:option文法重新组织一遍。

【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询