- TypeScript 98.6%
- CSS 0.6%
- JavaScript 0.4%
- Shell 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Vulnerability scan / vuln-scan (push) Successful in 7s
|
||
| .forgejo/workflows | ||
| .githooks | ||
| .github/workflows | ||
| scripts | ||
| src | ||
| .cz.toml | ||
| .gitignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CODE_OF_CONDUCT.md | ||
| CURRENT_PLAN.md | ||
| eslint.config.js | ||
| GOVERNANCE.md | ||
| LICENSE.txt | ||
| MAINTAINERS.md | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
interlocking
Shared browser-side modules for the gtfs.zone apps: coloring-book (edit.gtfs.zone), test-track (viz.rt.gtfs.zone) and yard-master (manage.rt.gtfs.zone).
Ships raw .ts source under src/. There is no build step: each app's vite
compiles it as source. Consumed as a pinned git dependency:
pnpm add "interlocking@github:gtfs-zone/interlocking#vX.Y.Z"
maplibre-gl, @leeoniya/ufuzzy, jszip and papaparse are peer
dependencies. Every consumer already carries all four, and a second copy of
maplibre is a broken map rather than a duplicate. gtfs-realtime-bindings is
an optional peer: gtfs/rt-types.ts imports its namespace as a type and
nothing here pulls protobufjs into a bundle, so only the apps that read GTFS-RT
need it installed.
What it is
A browser-side library for GTFS and GTFS-RT frontends: the UI chrome (navbar, modals, notifications, theme, search), the GTFS domain modules (route ordering, route diagrams, colors, feed loading) and the MapLibre layer specs. Nothing in here is specific to one of the three apps; anything that is belongs in the app.
Modules arrive by moving out of an app, not by being copied from it: once a module lives here it is edited here.
CURRENT_PLAN.md holds the roadmap, including what is still hand-copied
between the apps and the layout this package is moving to.
Checks
pnpm run check runs all three, and a pre-commit hook runs them on every
commit (enable it with git config core.hooksPath .githooks).
| script | what it does |
|---|---|
typecheck |
tsc --noEmit over src/ and scripts/ |
lint |
eslint src/ scripts/ --max-warnings 0 |
check:exports |
reports exports no consumer imports |
format runs prettier over the same directories.
check:exports replaces knip, which is vacuous for a library with no barrel
files: every module is its own entry point, so nothing ever looks unused.
Instead it resolves the sibling checkouts, collects every interlocking/...
import across them and diffs that against what src/ exports. An export only
another module here imports is reported as internal rather than unused. A
sibling that is not checked out is skipped, and the run exits 0 when all three
are absent. Unused exports warn; --strict makes them fatal.
Layout
Four peers, organised by domain rather than by the modules/utils/types split
the apps use:
src/ui/ chrome that knows nothing about GTFS
src/gtfs/ the transit domain, including its own rendering
src/map/ everything that imports maplibre-gl
src/util/ pure, domain-free
No barrel index.ts files: every module is its own entry point, imported as
interlocking/ui/navbar-actions and resolved through each consumer's tsconfig
path and vite alias.
App shell
The layout every app shares, in four parts that go together:
ui/app-shell.tsmounts the markup: navbar, map with its search card and auto-zoom toggle, the resizable right panel with#panel-content, and an optional mobile dock. Its element ids are whatsearch-controller,bottom-sheet,panel-resizerandnavbar-actionsbind to.ui/app-shell.cssstyles it: the map/panel grid, the <=767px bottom-sheet drawer and the map controls. Imported from the app's stylesheet.ui/page-state-manager.tsowns the current page state, its history and the URL hash, generic over the app's page-state union. The app supplies the hash codec and the validator.ui/focus-controller.ts(setFocus,onFocusChange/onStateChange) andui/panel-host.ts(thedata-navdispatch, the breadcrumb header, and the scroll and<details>restore) sit on top of it. The app supplies the pages.
Published data
The load modal's feed catalog is fetched at runtime from https://data.gtfs.zone
(gtfs/data-origin.ts), published daily by geometry-car: search.json, a
compact cut of feeds.json listed in manifest.json with its hash, one entry
per transit system from Transitland, the Mobility Database and rt.gtfs.zone,
with the last reachability check of each of its roles. gtfs/feed-catalog.ts
loads it (falling back to feeds.json when the manifest does not list it),
gtfs/feed-search.ts searches it and gtfs/feed-badges.ts renders a feed's
state and role chips. The modal lists the feeds the host app can use by default
(a schedule that answered; in the visualiser, plus a realtime role that did),
newest schedule first, with a "show all" toggle. Nothing is baked into a
consumer's public/.
Releasing
cz bump on main, which writes the version into package.json, updates
CHANGELOG.md and cuts the annotated vX.Y.Z tag. Push the commit and the tag,
then repin each consumer. A shared change is one commit here, one tag, and three
consumer bumps.
Restart any dev server the repinned app has running. The interlocking alias
resolves through a pnpm symlink to a path in the store, and a repin swaps that
symlink for a new one. Vite does not watch node_modules, so every app file
whose transform is still cached keeps importing the old store path: the browser
then loads two copies of a shared module, one per path, and each copy gets its
own module-level state. util/module-state.ts keeps that from corrupting
anything and logs loaded twice when it happens; the restart is still the fix.