The source-map feature (parseMdWithSourceMap, src/source-map/build-source-map.ts)
does not rely only on the public remark API. Its recordingExtension
injects handlers into the mdast-util-from-markdown compile phase and reads
undocumented internal compile context (see the comment block above
recordingExtension). Because of this, some dependency upgrades are parser
behavior upgrades, not routine bumps — an upstream change can silently rename a
token handler, reshape the compile context, or drift entity-decoding semantics,
and the type system will not catch it.
Treat a version change to any of these as a parser behavior upgrade:
mdast-util-from-markdownmdast-util-gfm-autolink-literaldecode-named-character-referencemicromark-util-decode-numeric-character-referenceremarkremark-parseremark-gfmremark-frontmatterremark-directiveremark-mathmicromarkand any transitivemicromark-*package pulled in by the above
Note that several of these are pinned to exact versions on purpose (e.g.
mdast-util-from-markdown, decode-named-character-reference,
micromark-util-decode-numeric-character-reference) so their internal contracts
stay locked. Do not relax those pins without going through this checklist.
Before merging any change that touches the versions above:
-
pnpm test— full Jest suite, including:- AST parity (
parseMddeep-equalsparseMdWithSourceMap().ast) - position parity (
__tests__/position.spec.ts) - source-map suites (
__tests__/source-map.spec.ts,__tests__/source-map/*) - roundtrip (
__tests__/roundtrip.spec.ts) - CJS/ESM behavior (
__tests__/index.spec.ts,__tests__/parser-isolation-bundle.spec.ts)
- AST parity (
-
pnpm run test:types— type declarations still compile against the public API -
pnpm run test:package—publint+ packed CJS/ESM tarball smoke test -
pnpm run bench:source-map— no unexpected build/query performance regression - Manually review whether the handlers overridden in
recordingExtension(data,characterEscape(Value),characterReference(Value),lineEnding,autolinkProtocol,autolinkEmail) or the compile-context fields it reads (stack,config.canContainEols,getData/setDatakeys,sliceSerialize) changed upstream. - Run the downstream
@lint-md/corecompatibility test suite against the upgraded parser.
If any parity or source-map assertion changes, that is a behavior change and
must be documented in CHANGELOG.md — do not silently accept new snapshots.
The regular Node 22 push and pull-request job runs the bounded source-map
benchmark smoke automatically. The full downstream compatibility check is
heavier, so it runs every Monday at 03:00 UTC and can be started before a
parser-sensitive dependency upgrade, a source-map implementation change, or a
release candidate from the Actions tab (or
gh workflow run source-map-validation.yml --ref <branch>).
Scheduled runs use the default branch; use the manual dispatch with an explicit
branch before merging a candidate.
The automatic smoke uses a 256 KiB alternating-atomic input, queries every
value code unit, and rejects the known linear-search regression. The scheduled
or manually triggered workflow runs the full lint-md/lint-md Jest suite after
installing the packed parser tarball, which verifies the package artifact and
downstream source-map integration together.
The workflow uses a read-only token and disables persisted checkout credentials because it executes downstream repository code.
The profile:source-map script captures V8 heap snapshots at specific
construction phases to verify that lazy indexes (lineStarts,
sourceGapPrefix) do not exist in retained heap until first use.
# Capture a snapshot
pnpm run profile:source-map -- segments build
pnpm run profile:source-map -- segments raw
pnpm run profile:source-map -- segments range
# Open in Chrome DevTools → Memory → Load .heapsnapshotFixtures:
| Fixture | Shape | Purpose |
|---|---|---|
many-nodes |
10k short paragraphs | high node count, low segment density |
segments |
256 KiB &\( repeats |
maximum segment count per node |
fenced-code |
1 MiB code block | single large code node with source-map segments |
urls |
1000 link definitions | URL segment construction |
Phases:
| Phase | What runs | What should NOT exist in heap |
|---|---|---|
build |
parseMdWithSourceMap() only |
lineStarts, sourceGapPrefix |
raw |
+ getRaw() on every mapped node |
lineStarts, sourceGapPrefix |
range |
+ getSourceRange() on every mapped value node |
(indexes now created on demand) |
Retained size dominators — in the Dominators view, check:
- AST nodes (
type,position,children,value) - Source strings (the original Markdown input)
- Segment objects (
valueStart,valueEnd,sourceStart,sourceEnd,kind) - Segment arrays (one per mapped node)
- WeakMap state (sourceMap closure → WeakMap → values)
- Lazy indexes (
lineStartsarray,sourceGapPrefixarrays)
The build → raw → range progression should show lineStarts and
sourceGapPrefix (for multi-segment ranges) appearing only in the
range phase. If they appear
in build or raw, the lazy design is broken.
The goal is a retained-heap breakdown:
retained heap
├── AST ??%
├── source strings ??%
├── segment objects ??%
├── segment arrays ??%
├── WeakMap state ??%
└── lazy indexes ??%
If AST + source strings + parser strings dominate, source-map memory optimization is not worthwhile. If segment objects/arrays are large, consider a more compact segment representation.
Rule: no heap dominator / retained-size evidence → no memory optimization PRs.