@echecs/pgn
Version:
Parse PGN (Portable Game Notation) chess games into structured JavaScript objects. Zero dependencies, strict TypeScript, no-throw API.
375 lines (268 loc) • 14.1 kB
Markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [3.10.1] - 2026-03-17
### Changed
- Build tool replaced with `tsdown`. The published package now ships a single
bundled, minified `dist/index.js` instead of multiple unbundled files.
Published package size reduced from ~200KB to ~42KB (79% reduction).
`dist/grammar.cjs.map` (84KB) is no longer published. Public API unchanged.
## [3.10.0] - 2026-03-17
### Added
- `stringify(input: PGN | PGN[], options?: ParseOptions): string` — converts
parsed PGN objects back to valid PGN strings (semantic round-trip fidelity).
Accepts a single game or an array of games. Reconstructs SAN from `Move`
fields, re-serializes annotation commands (`[%cal]`, `[%csl]`, `[%clk]`,
`[%eval]`) back into comment blocks, and preserves RAVs and NAGs. Fires
`onWarning` for recoverable issues (invalid castling destination, negative
clock).
### Changed
- `src/index.ts` refactored into focused internal modules (`src/types.ts`,
`src/parse.ts`, `src/stream.ts`, `src/stringify.ts`). Public API unchanged.
### Fixed
- Tagless games with no moves (e.g. `[Result "*"]\n\n*`) now parse correctly.
Previously the grammar required at least one move in the movetext.
## [3.9.1] - 2026-03-16
### Fixed
- `AnnotationColor` now includes `'C'` (cyan) and `'O'` (orange) in addition to
`'B'`, `'G'`, `'R'`, `'Y'` — matching real-world Lichess and ChessBase
exports. Tokens with these colors were previously silently dropped.
- Empty `[%csl ]` and `[%cal ]` commands (with no token list) are now silently
stripped from the comment text instead of leaking through as raw strings.
## [3.9.0] - 2026-03-16
### Added
- `Move` now exposes structured fields parsed from embedded PGN comment
commands:
- `arrows?: Arrow[]` — from `[%cal ...]` (coloured arrows on board)
- `squares?: SquareAnnotation[]` — from `[%csl ...]` (coloured square
highlights)
- `clock?: number` — from `[%clk ...]` (remaining time in seconds, sub-second
precision preserved)
- `eval?: Eval` — from `[%eval ...]` (engine evaluation: centipawns or
mate-in-N, with optional search depth)
- New exported types: `AnnotationColor`, `Arrow`, `SquareAnnotation`, `Eval`
- Command strings are stripped from `move.comment`; unknown `[%...]` commands
are left in the comment string unchanged
- De-facto standard followed:
[python-chess](https://python-chess.readthedocs.io/en/latest/pgn.html)
## [3.8.3] - 2026-03-15
### Added
- `stream()` now accepts a Web Streams `ReadableStream<string>` in addition to
`AsyncIterable<string>`. This covers
`fetch().body.pipeThrough(new TextDecoderStream())` in browser and edge
runtimes. The type signature is widened accordingly — no behaviour change for
existing `AsyncIterable` callers.
## [3.8.2] - 2026-03-15
### Fixed
- `comment_multiline` now handles one level of nested braces
(`{ see {Fischer} 1972 }`). Previously the comment terminated at the first
`}`, causing the rest of the game to fail to parse. Real-world PGN from GUI
exports commonly contains embedded `{…}` spans inside comments.
## [3.8.1] - 2026-03-15
### Fixed
- `PIECE_MOVE` no longer accepts promotion syntax (`=Q`, `=N`, etc.) on non-pawn
pieces. `Nf3=Q` previously parsed silently and produced a `Move` object with a
nonsensical `promotion` field; it now causes a parse failure. Only `PAWN_PUSH`
and `PAWN_CAPTURE` accept the `PROMO` suffix, as required by the PGN spec.
- `Meta.Result` is now typed as optional (`Result?: Result`) to reflect that
tagless games (no tag pairs) return `meta: {}` with no `Result` key. The field
was previously typed as required, which was incorrect at runtime.
## [3.8.0] - 2026-03-15
### Added
- `onWarning` now fires for move number mismatches (e.g. `5. e4` appearing as
the first move). Previously emitted unconditionally to `console.warn`; now
routed through `onWarning` when provided. `console.warn` is no longer called
for move number mismatches — if you relied on it, add an `onWarning` callback.
- `onWarning` fires when the `[Result "..."]` tag value does not match the game
termination marker at the end of the movetext (e.g. `[Result "1/2-1/2"]` with
a `1-0` termination marker).
- `onWarning` fires for duplicate tag names (same tag appearing more than once
in the tag pair section). The `line` and `column` fields point to the opening
`[` of the duplicate — exact source position, not a nominal placeholder.
### Changed
- Move number mismatch no longer emits to `console.warn` when `onWarning` is not
provided. It is now silently ignored, consistent with how missing STR tag
warnings are handled.
## [3.7.0] - 2026-03-15
### Added
- `onWarning` option for `parse()` and `stream()`: fires once per missing STR
tag. Emitted in alphabetical key order: `Black`, `Date`, `Event`, `Result`,
`Round`, `Site`, `White`.
- `ParseWarning` is now an exported type with the same shape as `ParseError`.
### Fixed
- `parse()` and `stream()` now strip a UTF-8 BOM (`\uFEFF`) at the start of
input. Chessbase and Windows editors commonly produce BOM-prefixed PGN files
that previously failed silently.
- Tag values containing escaped quotes (`\"`) or escaped backslashes (`\\`) now
parse correctly per PGN spec section 7.
- Games with no tag pairs (bare move list + result) now parse correctly per PGN
spec section 8.1 ("zero or more tag pairs"). These games return `meta: {}`.
## [3.6.2] - 2026-03-15
### Performance
- `pairMoves`: replaced `delete move.number; delete move.long` with an explicit
clean output object constructed from only the known public `Move` fields.
`delete` was fragmenting V8 hidden classes across move objects with different
optional fields (e.g. promotion moves), causing megamorphic deoptimisation and
GC pressure. `promotion.pgn` gap vs `pgn-parser` restored from 1.26x to 1.06x;
`long.pgn` and `twic.pgn` also improved.
## [3.6.1] - 2026-03-15
### Fixed
- `stream()`: result tokens straddling a chunk boundary (e.g. `1` at end of
chunk N, `-0` at start of chunk N+1) were silently missed, causing the two
affected games to be merged and the second game to be lost. The boundary
scanner now looks back up to 6 characters into already-scanned content on each
chunk to catch split tokens.
- `stream()`: `onError` was incorrectly forwarded to the remainder-flush path,
causing it to fire for truncated streams (input ending without a result token)
— which is expected behaviour, not a parse error. The remainder-flush path now
calls `parse()` without options.
- `toParseError`: `ParseError.offset` was always `0` — the field was being read
from the top level of the Peggy error object, which does not exist. It is now
correctly read from `location.start.offset`.
## [3.6.0] - 2026-03-15
### Added
- `onError` option for `parse()` and `stream()`: pass
`onError: (err: ParseError) => void` to observe parse failures instead of
silently receiving `[]`. `ParseError` carries `message`, `offset`, `line`, and
`column` from the Peggy parser.
- `ParseError` and `ParseOptions` are now exported types.
### Changed
- `stream()` accepts an optional second argument `options?: ParseOptions`
(backward-compatible).
### Performance
- Grammar `MOVE` action block: eliminated per-move object spread (`{ ...san }`)
and replaced `filter`/`join` chains with explicit loops — reduces heap
allocation on every move.
- `pairMoves`: pre-sized accumulator with `new Array(...)` and removed
rest-spread destructuring — avoids per-move object creation and reduces V8
array resizing.
- `stream()` boundary scanner: now O(n) per chunk — regex is only attempted at
characters that can start a result token (`1`, `0`, `*`) rather than at every
depth-0 character.
## [3.5.3] - 2026-03-14
### Fixed
- Castling moves with check or checkmate indicators (`O-O+`, `O-O#`, `O-O-O+`,
`O-O-O#`) now correctly set `check: true` / `checkmate: true` on the returned
Move object — previously the indicator was consumed and silently discarded
- Security: pin `vite>=6.4.1`, `rollup>=4.59.0`, `glob>=10.5.0` via pnpm
overrides to resolve 8 Dependabot CVEs in devDependencies
### Changed
- Grammar: extract `applyIndicators` preamble helper to eliminate repeated
`promo`/`ind` logic across all SAN action blocks
### Internal
- Add `vitest.config.ts` to exclude `grammar.cjs` and bench files from coverage
reporting — coverage now reflects only authored source (`index.ts`)
- Add explicit SAN unit tests for all grammar alternatives and indicator
combinations, covering patterns that snapshot tests cannot detect regressions
in (full-square disambiguation, castling indicators, promotion+checkmate,
etc.)
## [3.5.2] - 2026-03-14
### Fixed
- Benchmark: `benko.pgn` moved to multi-game group and compared with the correct
`parseGames` API — previously called `parseGame` (single-game), causing all
comparison parsers to error
- Benchmark: `comments.pgn` BOM (`U+FEFF`) now stripped before parsing, enabling
comparison against `@mliebelt/pgn-parser` and `pgn-parser`
- Benchmark: fixture exclusion reasons documented in `BENCHMARK_RESULTS.md` and
in bench source comments
## [3.5.1] - 2026-03-14
### Changed
- README: document `stream()` API with signature and Node.js usage example
- README: update type names (`Moves` → `MoveList`) and clarify `Move.from`
disambiguation and move tuple slot semantics
- Updated benchmark results for v3.5.0 SAN rule restructure
## [3.5.0] - 2026-03-14
### Added
- `stream(input: AsyncIterable<string>): AsyncGenerator<PGN>` — new named export
for incremental, memory-efficient parsing of large PGN databases
### Changed
- `Move.from` widened from `File | Rank` to `Disambiguation`
(`Square | File | Rank`) to correctly type fully-disambiguated moves (e.g.
`Qd1xe4` → `from: "d1"`)
- `type Moves` renamed to `MoveList`; new
`MovePair = [number, Move | undefined, Move?]` tuple
- `type Variation` simplified to `MoveList[]`
### Performance
- Restructured `SAN` grammar rule to eliminate post-match regex on every move;
closes remaining ~1.1–1.2x gap vs `pgn-parser` on move-heavy fixtures
## [3.4.0] - 2026-02-21
### Changed
- Rewrote README following `@echecs/elo` library style with badges, Why, Quick
Start, Usage, and Contributing sections
- Updated AGENTS.md to reflect Peggy migration and remove stale nearley/moo
references
### Added
- Features section in README highlighting RAV and NAG support with a parser
comparison table
- Performance section in README with benchmark results table
- Codecov badge to README
### Removed
- `docs.yml` workflow (no hosted docs in this project)
## [3.3.0] - 2026-02-21
### Added
- Peggy PEG parser replacing nearley/moo for O(n) linear-time parsing —
delivering up to 10× throughput improvement on large PGN files
- Comparative benchmark (`comparison.bench.ts`) measuring `@echecs/pgn` against
`@mliebelt/pgn-parser` and `chess.js`
### Performance
- Replaced `pickBy` with direct property assignment in SAN action block,
reducing allocations per move
- Added length-check guards before NAG and comment processing in MOVE action,
skipping unnecessary work for moves without annotations
- Removed `delete` mutations and reduced allocations in `pairMoves`, avoiding V8
hidden-class transitions
### Removed
- Stale `tokenizer.ts` debug script
## [3.2.1] - 2026-02-20
### Fixed
- Sort `multiGameFixtures` keys in comparison benchmark to satisfy the
`sort-keys` lint rule
## [3.2.0] - 2026-02-20
### Added
- Comparative PGN parser benchmark (`comparison.bench.ts`) for cross-library
performance tracking
### Performance
- Reduced Earley parser overhead via grammar and caching optimizations
## [3.1.3] - 2025-03-27
### Fixed
- Removed accidental production dependency introduced in 3.1.2
## [3.1.2] - 2025-03-01
### Fixed
- Increased per-test timeout to accommodate `long.pgn` (~3 500 games) on slow CI
runners
## [3.1.1] - 2025-03-01
### Fixed
- Corrected `.js` extension on relative imports in test files (NodeNext
resolution)
## [3.1.0] - 2025-03-01
### Added
- moo tokenizer for faster lexing
- New grammar supporting the full PGN specification including RAV (recursive
annotated variations) and NAG (numeric annotation glyphs)
[unreleased]: https://github.com/mormubis/pgn/compare/v3.10.0...HEAD
[3.10.0]: https://github.com/mormubis/pgn/compare/v3.9.1...v3.10.0
[3.9.1]: https://github.com/mormubis/pgn/compare/v3.9.0...v3.9.1
[3.9.0]: https://github.com/mormubis/pgn/compare/v3.8.3...v3.9.0
[3.8.3]: https://github.com/mormubis/pgn/compare/v3.8.2...v3.8.3
[3.8.2]: https://github.com/mormubis/pgn/compare/v3.8.1...v3.8.2
[3.8.1]: https://github.com/mormubis/pgn/compare/v3.8.0...v3.8.1
[3.8.0]: https://github.com/mormubis/pgn/compare/v3.7.0...v3.8.0
[3.7.0]: https://github.com/mormubis/pgn/compare/v3.6.2...v3.7.0
[3.6.2]: https://github.com/mormubis/pgn/compare/v3.6.1...v3.6.2
[3.6.1]: https://github.com/mormubis/pgn/compare/v3.6.0...v3.6.1
[3.6.0]: https://github.com/mormubis/pgn/compare/v3.5.3...v3.6.0
[3.5.3]: https://github.com/mormubis/pgn/compare/v3.5.2...v3.5.3
[3.5.2]: https://github.com/mormubis/pgn/compare/v3.5.1...v3.5.2
[3.5.1]: https://github.com/mormubis/pgn/compare/v3.5.0...v3.5.1
[3.5.0]: https://github.com/mormubis/pgn/compare/v3.4.0...v3.5.0
[3.4.0]: https://github.com/mormubis/pgn/compare/v3.3.0...v3.4.0
[3.3.0]: https://github.com/mormubis/pgn/compare/v3.2.1...v3.3.0
[3.2.1]: https://github.com/mormubis/pgn/compare/v3.2.0...v3.2.1
[3.2.0]: https://github.com/mormubis/pgn/compare/v3.1.3...v3.2.0
[3.1.3]: https://github.com/mormubis/pgn/compare/v3.1.2...v3.1.3
[3.1.2]: https://github.com/mormubis/pgn/compare/v3.1.1...v3.1.2
[3.1.1]: https://github.com/mormubis/pgn/compare/v3.1.0...v3.1.1
[3.1.0]: https://github.com/mormubis/pgn/releases/tag/v3.1.0