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

gtfs-departures

v1.0.1

Published

Turn a transit agency's GTFS feed into lazy-loadable departures-per-stop, schedules-per-pattern, and line geometries that a browser frontend consumes directly.

Readme

gtfs-departures

Turn a transit agency's GTFS feed into exactly what a mobility frontend consumes and nothing else: departures per stop, schedules per pattern (line variant), and the line geometries — each as a lazy-loadable, per-id resource — plus the resolved service calendar and a flat route index.

This is not a "dump the whole feed as JSON" converter. GTFS is an interchange format — a zip of CSVs — awkward to read in a browser, where you would parse and re-aggregate on every page load. gtfs-departures does that work once, ahead of time (via npx), and shards the result so a station page fetches one small file for one stop, a map draws one line by fetching one shape, and nothing else is downloaded. Everything GTFS also contains (agency, fares, feed_info, transfers, …) is off by default and opt-in via --include.

Requirements

  • Node.js ≥ 22. That's it — gtfs-departures has zero dependencies: no native modules, no C/C++ toolchain, nothing to compile. npx gtfs-departures works out of the box on macOS, Linux and Windows. The GTFS zip is read by a built-in pure-JS extractor and CSV parser.

Usage

npx gtfs-departures --url <FEED_URL>

Options:

| Flag | Required | Description | | --- | --- | --- | | --url <FEED_URL> | yes | Direct URL to a zipped GTFS feed. | | --include <list> | no | Comma-separated GTFS tables to also emit as raw 1:1 JSON under raw/ (e.g. --include agency,fares). Off by default. | | --out <DIR> | no | Output directory (default: current folder). | | -h, --help | no | Show usage. |

Examples:

npx gtfs-departures --url https://example.com/gtfs.zip
npx gtfs-departures --url https://example.com/gtfs.zip --include agency,fares --out ./bundles

Progress is narrated on stderr as it runs; the final summary (counts, shard counts, any included raw tables, output path) goes to stdout. The output folder is <agency-slug>-gtfs-departures, from the primary agency in agency.txt.

Large feeds

The pipeline is memory-flat by design — a national feed cannot OOM it:

  • the zip is streamed to disk while downloading (hashed on the way through), never held as one buffer;
  • each zip entry is decompressed as a stream (pure JS), so a 1 GB+ stop_times.txt never becomes a 1 GB allocation;
  • stop_times and shapes — the giant tables — are hash-partitioned into temporary disk buckets in one streaming pass, then every builder processes one bucket at a time (~1/64 of the table). Peak heap is one bucket plus the small tables, regardless of feed size.

Measured: a synthetic feed with 3.3 million stop_times builds in ~1 minute with a peak of ~225 MB RSS, comfortably inside a 256 MB heap cap. No --max-old-space-size juggling needed. Temporary bucket files (roughly the size of the uncompressed stop_times) live in the OS temp dir and are always cleaned up, including on error.

Output

Everything is UTF-8, pretty-printed JSON (2-space indent) so the bundle stays diff-able. Geometry is valid GeoJSON (RFC 7946) with coordinates in [longitude, latitude] order; everything else uses verbatim GTFS field names (stop_id, route_short_name, departure_time, …).

<slug>-gtfs-departures/
├── manifest.json            # entry point: metadata, resource map, counts, warnings, includes
├── calendar.json            # resolved service calendar (weekly pattern + exceptions)
├── routes.json              # line index: route metadata + colors + pattern_ids[]
├── stops/
│   ├── index.json           # every served stop: name, coords, routes, parent, file path
│   └── <enc(stop_id)>.json  # departures for that one stop (sorted array)
├── patterns/
│   ├── index.json           # every pattern: route, direction, headsigns, shape, counts, file
│   └── <enc(pattern_id)>.json # full schedule (stop sequence + trips) for that pattern
├── shapes/
│   ├── index.json           # every shape: route_ids, pattern_ids, color, bbox, file
│   └── <enc(shape_id)>.json # one shape as a GeoJSON FeatureCollection (single LineString)
└── raw/                     # ONLY if --include was used
    └── <table>.json         # faithful 1:1 mirror of the named GTFS table(s)

manifest.json is the single entry point — load it first. Its resources map points at the three index files plus routes.json and calendar.json (never at individual shards). It also records the source feed URL and SHA-1, the agencies, a bounding box, counts, included_raw, and warnings.

The three sharded resources

  • stops/ — departures per stop. stops/index.json lists every served stop with its coordinates (this doubles as the stop-markers data source), the lines serving it, and the file of its departures shard. Each shard is that stop's departures, sorted ascending by departure_time.
  • patterns/ — schedule per pattern. A pattern is one distinct ordered stop sequence for a route_id + direction_id (the granularity OpenTripPlanner / Transitland use), the right unit for a timetable. patterns/index.json is a light list; each shard has the ordered stop_sequence and the trips with their stop_times.
  • shapes/ — one line geometry per shard, each a GeoJSON FeatureCollection with a single LineString (hand it straight to MapLibre/Leaflet). shapes/index.json carries a per-shape bbox so the map can fit the camera before fetching the full geometry.

Index-driven lookup (safe filenames)

A stop_id, pattern_id, or shape_id can contain /, :, spaces, or accents. Shards are named with encodeURIComponent(id) (deterministic and reversible), and the real encoded path is stored in each index entry's file. The frontend reads the index and fetches file; it never rebuilds a filename from an id.

Deterministic pattern_id

pattern_id = "{route_id}-{direction_id}-{hash8}"

hash8 is the first 8 hex of the SHA-1 of the ordered stop_id sequence (joined with >). It changes only when the stop sequence changes, so a frontend's per-pattern caches survive routine feed updates. Given the same feed, the same ids and the same shard filenames come out every time.

--include (opt-in raw mirror)

By default there is no raw/. --include <list> adds faithful 1:1 JSON mirrors (verbatim columns, values as strings) of the named GTFS tables — any base name (agency, feed_info, transfers, stop_times, …). The alias fares expands to every fare-related table present (fare_attributes, fare_rules, and GTFS-Fares-v2 files like fare_products, fare_leg_rules, fare_media, …). A requested table the feed lacks is skipped with a warning, never a crash.

Bundle viewer (view.html)

Every bundle ships a self-contained view.html: browse stations, see each station's departures (with a day filter), tap a departure to open the full trip schedule — with each line rendered in its official GTFS colors (route_color / route_text_color). Open it directly in Firefox, or serve the bundle folder with any static server for Chrome/Safari (npx serve .). Deterministic content, so bundle reproducibility is unaffected.

Notes / gotchas

  • Shard filenames are filesystem/URL-safe. GTFS ids like IDFM:monomodalStopPlace:47884 become IDFM-monomodalStopPlace-47884-<hash>.json — never percent-encoded, so an HTTP server can't mangle them by decoding the path. Case-colliding ids (RA vs ra), Windows-reserved names and over-long ids are disambiguated with a deterministic hash. Consumers must always use the file field from each index — never rebuild a filename from an id.

  • GTFS edge cases handled: unpadded times (7:30:0007:30:00, fixing sort order), times past 24:00:00 kept verbatim, arrival-only timepoints included on boards, calendar_dates.txt-only feeds, missing shapes.txt, missing colors (defaulted, warned), accented/empty agency names (safe output-folder slug), and frequencies.txt — frequency-defined trips are expanded into real per-headway departures (flagged frequency_based, with exact_times).

  • Times can exceed 24:00:00. GTFS represents after-midnight service as 25:30:00. These are preserved verbatim as strings — never parsed to Date, never wrapped at 24h. Zero-padding means the string sort still orders the boards correctly.

  • Realtime-ready, no realtime fields. Departures and patterns keep the GTFS-Realtime join keys (trip_id, stop_id, stop_sequence), so a future realtime layer matches with no translation. No empty realtime_* placeholders are added.

  • Link, don't embed. A departure/pattern stores shape_id; coordinates live only in that shape's shard.

  • Graceful degradation. Missing shapes.txt → empty shapes/index.json ({}) + warning; no calendar.txt (everything in calendar_dates.txt) → calendar still resolves, omitting weekly flags; missing optional fields are defaulted (route colors → 777777 on FFFFFF), never crashed on. A leading UTF-8 BOM — common in European feeds — is detected from the file head and stripped before import.

Roadmap

Later: Transitland / agency-name lookup (so you don't have to find the zip URL), and a GTFS-Realtime layer merged over the shared join keys.

License

MIT — see LICENSE.