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

gff-nostream

v5.5.0

Published

utilities to read GFF3 data

Readme

gff-nostream

NPM version Build Status

Parse GFF3 data. A simplified version of @gmod/gff with no Node.js stream dependency.

Install

pnpm add gff-nostream

Usage

import { readFileSync } from 'node:fs'
import { parseStringSync } from 'gff-nostream'

const features = parseStringSync(readFileSync('my_annotations.gff3', 'utf8'))

There is no filesystem dependency — in the browser, pass any GFF3 string, such as one from fetch(…).then(r => r.text()). If your GFF3 is a remote .gff3.gz, query it with @gmod/tabix, reading through a filehandle from @gmod/range-cache-filehandle so that its byte ranges are cached, and parse the lines it hands back with the functions here.

Object format

The parser hands back flat objects: coordinates converted to 0-based half-open, strand as a number (1/-1/0), attributes spread as lowercase top-level keys, single-valued attributes unwrapped from their array, and child features nested under subfeatures.

A gene with an mRNA child:

{
  "refName": "ctg123",
  "source": null,
  "type": "gene",
  "start": 999,
  "end": 9000,
  "strand": 1,
  "subfeatures": [
    {
      "refName": "ctg123",
      "source": null,
      "type": "mRNA",
      "start": 1049,
      "end": 9000,
      "strand": 1,
      "subfeatures": [],
      "id": "mRNA00001",
      "parent": "gene00001",
      "name": "EDEN.1"
    }
  ],
  "id": "gene00001",
  "name": "EDEN"
}

The fixed fields are refName, source, type, start, end, score, strand, phase, and subfeatures. The parser appends 2 to an attribute whose lowercased name matches one of them — Start= becomes start2. It reserves seq_id and refname the same way, so a Seq_id= attribute becomes seq_id2 and is never stored beside refName.

Parsing behavior

These apply to every parse function below.

The parser ignores comments, directives, and ##FASTA sections.

Attribute tags are matched with surrounding spaces trimmed, so Name=A; ID=x carries an ID, as it does under @gmod/gff.

A multi-location feature — the same ID on several lines, such as a CDS spanning several segments — stays one line per segment under its parent: each line attaches to the parent as its own flat feature. With no parent, the first line becomes the feature, spanning every segment, and each segment — the first included — hangs under it as a subfeature of the same type. An NCBI cDNA_match, written one line per aligned block, therefore comes back as one feature carrying its blocks, and a single-line feature gains no child. A repeated ID whose first line already has children, or whose lines differ in type, is a duplicate rather than one feature, and each line stays its own top-level item.

A feature whose Parent never appears in the input comes back as a top-level feature, after the ones that did, rather than dropping. This happens routinely when parsing a slice of a file, e.g. a tabix region query that cuts off the parent line.

Lazy parsing

Every parse function has a …Lazy counterpart that leaves column 9 as raw text on feature.attributeString instead of spreading it into keys. It reads only ID and Parent, since it cannot build the tree without them.

import { getAttribute, parseLinesLazy } from 'gff-nostream'

const features = parseLinesLazy(lines)
const names = features.map(f => getAttribute(f, 'name'))

Lazy parsing is worth it when most attributes are never read, and not when they are, because each getAttribute rescans the string. See docs/lazy-parsing.md for the measured trade-off and the gotchas.

API

Parsing

parseStringSync(str: string): GffFeature[]

Parse a GFF3 string.

parseLines(lines: readonly string[]): GffFeature[]

Parse an array of raw GFF3 feature lines, for a caller that has split and filtered the file itself — a tabix region query, or a whole-file scan grouping lines by reference sequence. The lines must already be free of blanks, comments, and any ##FASTA section.

parseRecords<R extends LineRecord>(records: readonly R[]): ParsedRecord<R>[]

Parse an array of records wrapping raw GFF3 lines. Each top-level feature comes back paired with the record it came from, so a caller can attach its own stable id (a byte offset, a hash, …) without the parser stamping anything onto the feature. Records may carry extra fields (R is inferred), which pass through untouched on record.

const records = lines.map((line, i) => ({ line, offset: offsets[i] }))
const features = parseRecords(records).map(({ feature, record }) => ({
  ...feature,
  id: record.offset,
}))

parseLinesLazy / parseRecordsLazy

parseLines and parseRecords returning LazyGffFeatures. See Lazy parsing.

parseFeatureLazy(line: string): LazyGffFeature

Parse a single line, with no tree building.

Attributes

getAttribute(feature: LazyGffFeature, key: string): unknown

The value of one attribute, by its parsed key (lowercased, 2-suffixed if reserved). A single-valued attribute comes back as a string, a multi-valued one as a string array, and an absent one as undefined — the same shapes the eager parser produces.

getAttributes(feature: LazyGffFeature): Record<string, unknown>

Every attribute, as the eager parser would have spread them onto the feature.

getLinkAttributes(feature: LazyGffFeature): { id: unknown; parent: unknown }

ID and Parent in one pass. The lazy parsers build the tree with it, and the package exports it for callers doing their own linking.

Other

extractType(line: string): string

Extract the feature type (GFF3 column 3) from a raw line without fully splitting it. Returns '' for a line with fewer than two tabs.

hasIdAttribute(line: string): boolean

Whether a raw line carries an ID the parser would link children to. A record with no ID can be named by no Parent=, so this is the exact test for whether a line can have children, and it uses the parser's own tag scan, so it agrees with the linker about spacing and case. A region reader that widens its fetch to complete a record's subfeature list can bound the widening with it.

Types

interface LineRecord {
  line: string
}

interface ParsedRecord<R extends LineRecord = LineRecord> {
  feature: GffFeature
  record: R // the input record this top-level feature came from
}

ParsedLazyRecord is ParsedRecord with a LazyGffFeature. The package also exports GffFeature and LazyGffFeature.

Contributing

See CONTRIBUTING.md for development and release steps.