@effected/github-references

中文文档
0.15.0发布于 2026/11/10MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 1.6万

GitHub's issue-reference grammar as pure functions: inline-in-prose harvesting, bare-line parsing and the closing-list dialect.

原始使用文档
文档目录15 节
  1. Why @effected/github-references
  2. Install
  3. Quick start
  4. Inline in prose
  5. Bare lines
  6. Closing lists
  7. Harvesting reference lists inline
  8. Reading a multi-line text
  9. Collecting both postures at once
  10. Keyword families
  11. Grammar rules worth knowing
  12. Out of scope
  13. Coming from @effected/github
  14. Features
  15. License

@effected/github-references

npm License: MIT Node.js %3E%3D24.11.0 TypeScript 7.0

GitHub's issue-reference grammar as pure functions: the nine closing keywords in the three dialects real tools write — inline in prose, one reference per line, and a whole line naming several issues at once. Strings in, values out. No service, no layer, no client, no network, and no octokit anywhere in the dependency graph: effect is the only peer and there are no runtime dependencies at all.

Pre-1.0.0. This package is part of the @effected/* kit, built on stable Effect v4 (effect ~4.1.0) and still in 0.x development. Stable Effect makes a kit 1.0.0 possible, not automatic. To keep your effect and @effect/* versions on the line the kit is built and tested against, install @effected/pnpm-plugin-effected.

Stability: unstable. This package's API surface is not yet considered complete and may change across 0.x releases. Pin an exact version — even a package marked stable before 1.0.0 can introduce a breaking change by accident, and an exact pin turns that into a type-check error rather than a runtime surprise. Full policy: release strategy.

Why @effected/github-references

Deciding which issues a pull request closes is a grammar question, not an API call, and the answer is usually re-derived from memory in a one-line regex. That regex is where the bugs live: accept a colon inline and you harvest references GitHub will not link, so your pipeline reports an issue as closing that merging leaves open. Accept closes 123 without the # and you report a link GitHub never made. Read Closes #1, #2 and #3 with a parser built for one reference per line and you silently see the first issue and lose the rest.

This package is that grammar written once, with the disagreements resolved on purpose rather than by whoever copied the regex last. Each dialect is a separate function because each has a distinct producer: prose is written by humans for GitHub's own scanner, and a generated references region is written by tooling for humans. The functions are pure and synchronous — no Effect, no layer to provide, nothing to stub in a test — so a release script, a lint rule, a commit-message check and an Action can all speak the same grammar without any of them installing a GitHub client.

Install

npm install @effected/github-references effect
pnpm add @effected/github-references effect

Requires Node.js >=24.11.0.

All @effected/* packages are ESM-only: the exports maps publish only import conditions, so require() — including tools that resolve in CJS mode — fails with Node's ERR_PACKAGE_PATH_NOT_EXPORTED rather than loading a CJS build that does not exist. Import from an ES module.

effect v4 is the only peer dependency and there are no others of any kind. Option is the only part of effect the surface touches, and nothing here runs an effect, so every function is callable from ordinary synchronous code.

Quick start

Harvest references out of a pull request body, then read a generated line that names several issues:

import { harvestIssueReferences, parseClosingList } from "@effected/github-references";

console.log(harvestIssueReferences("Fixes #12 and closes #13."));
// [ { issueNumber: 12, keyword: "fixes", start: 0, end: 9 },
//   { issueNumber: 13, keyword: "closes", start: 14, end: 24 } ]

console.log(parseClosingList("Closes #247, #248 and #251"));
// Option.some({ keyword: "closes", issueNumbers: [247, 248, 251] })

Inline in prose

harvestIssueReferences reads the inline-in-prose dialect: one of the nine closing keywords in CLOSING_KEYWORDS, mandatory whitespace, then #<number>, anywhere in the text, case-insensitive, and no colon — that is the spelling GitHub itself scans a pull request body for. Every hit carries start and end offsets into the input, so a caller can underline the match, strip it, or rewrite it in place.

import { harvestIssueReferences } from "@effected/github-references";

console.log(harvestIssueReferences("closes: 123 and fixes #9"));
// [ { issueNumber: 9, keyword: "fixes", start: 16, end: 24 } ]
// — the colon spelling is not the inline dialect, and `123` has no `#`

Duplicates come back as written, because whether fixes #1, fixes #1 means one intent or two is the caller's question. This is the dialect a release pipeline harvests from commit subjects and pull request descriptions.

Bare lines

parseBareLineReference reads the bare-line dialect: after trimming, the whole line is the reference, the colon is optional, and anything left over rejects the line. That is the shape a generated references block writes, one reference per line. There are no offsets, because in a dialect where the line is the reference an offset would restate a constant.

import { parseBareLineReference } from "@effected/github-references";

console.log(parseBareLineReference("Closes: #12"));
// Option.some({ issueNumber: 12, keyword: "closes" })

console.log(parseBareLineReference("closes #12 for real"));
// Option.none() — the bare-line dialect takes the whole line or nothing

Closing lists

parseClosingList reads one whole line naming several issues under a single keyword. Items are #<digits> separated by a comma, by and, or by the Oxford , and, and the keyword's colon is optional as in the bare-line dialect.

import { parseClosingList } from "@effected/github-references";

console.log(parseClosingList("Fixes #1, #2, and #3"));
// Option.some({ keyword: "fixes", issueNumbers: [1, 2, 3] })

console.log(parseClosingList("Closes #1 and please review"));
// Option.none() — trailing prose rejects the line rather than parsing a prefix

parseReferenceList is the wider reading of the same line. It additionally accepts the non-closing REFERENCE_KEYWORDS — ref, refs, references — which associate an issue without closing it, and reports which set matched through a closing flag. parseClosingList is the closing-only view of exactly that engine, so a Refs: line is Option.none() there and closing: false here:

import { parseClosingList, parseReferenceList } from "@effected/github-references";

console.log(parseReferenceList("Refs #7, #8"));
// Option.some({ keyword: "refs", closing: false, issueNumbers: [7, 8] })

console.log(parseClosingList("Refs #7, #8"));
// Option.none() — a reference list, but not a closing one

Harvesting reference lists inline

harvestReferenceLists generalizes the closing-list grammar to the inline-in-prose posture: several lists can share one line of running text, each ending at its own last valid item.

import { harvestReferenceLists } from "@effected/github-references";

console.log(harvestReferenceLists("Closes #123, Fixes #456"));
// [ { keyword: "closes", closing: true, issueNumbers: [123], start: 0, end: 11 },
//   { keyword: "fixes", closing: true, issueNumbers: [456], start: 13, end: 23 } ]

Both keyword sets play, exactly as in parseReferenceList, and each hit carries start / end offsets like harvestIssueReferences. An issue number past Number.MAX_SAFE_INTEGER anywhere in a candidate drops the whole candidate, never a partial list, and scanning resumes after it.

Reading a multi-line text

parseBareLines, parseClosingLists and parseReferenceLists apply their single-line parser across every line of a text and collect the hits in order. Rejected lines contribute nothing, and results carry no line numbers — a caller that needs positions keeps its own split loop.

import { parseBareLines, parseClosingLists } from "@effected/github-references";

console.log(parseBareLines("Closes: #12\nsome unrelated line\nFixed #34"));
// [ { issueNumber: 12, keyword: "closes" }, { issueNumber: 34, keyword: "fixed" } ]

console.log(parseClosingLists("Fixes #1, #2\nRefs #9\nCloses #3 and #4"));
// [ { keyword: "fixes", issueNumbers: [1, 2] }, { keyword: "closes", issueNumbers: [3, 4] } ]

parseReferenceLists reads the same lines the wider way, admitting REFERENCE_KEYWORDS alongside the closing nine.

Collecting both postures at once

collectReferenceLists reads a text the way a real trailer/prose interleave actually looks: each line is tried as a whole-line reference list first (colon-tolerant, per parseReferenceList), and only a line that isn't one is harvested inline (no colon, per harvestReferenceLists). That preference order means a colon-less trailer line contributes its list exactly once, never once per posture. Results carry no offsets — this is the line-granular reading; a caller that needs spans reaches for harvestReferenceLists directly.

import { collectReferenceLists } from "@effected/github-references";

console.log(collectReferenceLists("Fixes: #10\nprose mentioning closes #11"));
// [ { keyword: "fixes", closing: true, issueNumbers: [10] },
//   { keyword: "closes", closing: true, issueNumbers: [11] } ]

Keyword families

keywordFamily(keyword) collapses any of the twelve keywords across both sets to one of four families — "close", "fix", "resolve", "ref" — for a caller building a Closes/Fixes/Refs-style map instead of switching on all twelve spellings by hand.

import { keywordFamily } from "@effected/github-references";

console.log(keywordFamily("resolved"));
// "resolve"

Grammar rules worth knowing

  • The # is mandatory in every dialect. closes: 123 is not a reference; GitHub does not link it, and a parser that accepts it reports links that were never made.
  • The list dialects are whole-line. Same-line whitespace is [ \t] only, so an embedded newline cannot smuggle a second line past a parser whose contract is one line, and trailing content rejects the line instead of yielding a partial reading.
  • A list never continues past a newline, inline or whole-line — though an inline one may open across one. harvestReferenceLists's continuation separators are [ \t] only, so once a candidate has its first item the list ends at the line; the one newline-tolerant spot is the opening gap between the keyword and the first #, which takes any whitespace exactly like harvestIssueReferences. And like that harvest, it takes no colon — the colon spelling belongs to parseReferenceList and the bare-line dialect.
  • Duplicates are preserved, in order. Whether #12, #12 means one issue or two is the caller's business; collapsing them would destroy evidence a caller may be linting for.
  • Keyword case does not matter, and the result is canonical. CLOSES, Closes and closes all match, and keyword always comes back lowercased.
  • Unsafe issue numbers are treated differently on purpose. Digits past Number.MAX_SAFE_INTEGER make the prose harvest skip that one match, while the list dialects — whole-line and inline — reject the whole line or candidate: surrounding prose makes no claim about a skipped number, but a list is a single claim about a set of issues that a partial result would misrepresent.
  • No input is truncated and no length cap applies. The list dialect is parsed by a single character-by-character scan — no regular expressions at all — so a hostile line cannot trigger catastrophic backtracking and there is nothing to defend with truncation.

Out of scope

Cross-repo references (owner/repo#12) and full-URL references (https://github.com/owner/repo/issues/12) are real GitHub spellings that this package does not read yet — guessing at their shape would freeze an API nobody has driven. Issue state is out of scope too, because state is not grammar, and anything that asks GitHub a question belongs to @effected/github.

Coming from @effected/github

The grammar used to live in @effected/github, which still re-exports exactly the six original names — CLOSING_KEYWORDS, ClosingKeyword, IssueReference, harvestIssueReferences, BareLineReference, parseBareLineReference — so code written against the old home keeps compiling. That re-export is a compatibility shim and may be dropped at a later @effected/github release; the closing-list surfaces are deliberately not part of it. Import from this package instead, and drop the @effected/github dependency entirely if the grammar is all you wanted from it.

Features

  • harvestIssueReferences(text) — every inline closing reference in running text, in document order, each with issueNumber, canonical keyword and start / end offsets.
  • harvestReferenceLists(text) — every reference list found inline in running text, in document order, each with keyword, closing, issueNumbers and start / end offsets.
  • parseBareLineReference(line) — the one reference a whole trimmed line carries, colon optional, as an Option.
  • parseBareLines(text) — every bare-line reference in a multi-line text, one per line, collected in order.
  • parseClosingList(line) — the issues a whole line names under one of the nine closing keywords, comma-, and- or Oxford-separated.
  • parseClosingLists(text) — parseClosingList applied across every line of a text, collected in order.
  • parseReferenceList(line) — the same line under the closing and non-closing keyword sets, reporting which matched through a closing flag.
  • parseReferenceLists(text) — parseReferenceList applied across every line of a text, collected in order.
  • collectReferenceLists(text) — every reference list in a multi-line text, whole-line first and inline only for a line that isn't one, so a colon-less trailer line never counts twice.
  • keywordFamily(keyword) — a keyword's tense-collapsed family: close, fix, resolve or ref, total over both keyword sets.
  • CLOSING_KEYWORDS / ClosingKeyword — the nine keywords GitHub acts on, and the type of one of them.
  • REFERENCE_KEYWORDS / ReferenceKeyword — the three non-closing keywords that associate without closing.
  • IssueReference, BareLineReference, ClosingList, ReferenceList, HarvestedReferenceList — the result types, one per dialect reading.
  • KeywordFamily — the type of keywordFamily's return value.

License

MIT

版本与发布记录

@effected/github-references 共发布 17 个版本,首次发布于 2026/08/18;过去 12 个月发布了 17 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.15.0latest2026/11/102 天前
0.14.02026/11/0310 天前
0.13.02026/11/0112 天前
0.12.02026/10/2915 天前
0.11.02026/10/2321 天前
0.10.02026/10/1924 天前

关于 @effected/github-references 的常见问题

@effected/github-references 是什么?

GitHub's issue-reference grammar as pure functions: inline-in-prose harvesting, bare-line parsing and the closing-list dialect.

如何安装 @effected/github-references?

在项目目录运行 npm install @effected/github-references,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(~4.1.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effected/github-references 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@effected/github-references 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@effected/github-references 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.11.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@effected/github-references 可以商用吗?

@effected/github-references 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@effected/github-references 的下载量有多少?

根据 npm 官方下载统计,@effected/github-references 在 2026/10/13 – 2026/11/11 这 30 天内被下载 16,001 次,日均约 533 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@effected/github-references 还在维护吗?

最新版本 0.15.0 发布于 2026/11/10(2 天前);至今共发布 17 个版本,其中过去 12 个月发布了 17 个;首次发布于 2026/08/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。