digitransit-ui hosts four independently-versioned and independently-published
npm package families, managed as yarn workspaces via lerna:
| Family | npm scope | Packages directory | Entry point | Build step |
|---|---|---|---|---|
digitransit-component |
@digitransit-component/* |
digitransit-component/packages/* |
src/index.js |
Rollup (compiles JSX) |
digitransit-search-util |
@digitransit-search-util/* |
digitransit-search-util/packages/* |
index.js (except digitransit-search-util-query-utils, which has a build step and uses src/index.js) |
none, except query-utils (relay-compiler) |
digitransit-store |
@digitransit-store/* |
digitransit-store/packages/* |
src/index.js |
Rollup (no JSX) |
digitransit-util |
@digitransit-util/* |
digitransit-util/packages/* |
index.js |
none |
The main app consumes component/store via their built Rollup output —
real ESM (lib/index.js, via each package's "module"/exports fields,
preferred by webpack for import) with a UMD lib/index.cjs ("main") as
the require/Node fallback — and search-util/util via raw source, using
webpack's native ESM support. Treat these packages like
semi-external dependencies: changes to their public API should bump their
version (see Publishing) the same way a change to a real npm
dependency would.
- Most work happens inside a family's
packages/<family>-<module>directory. - If you'd like to propose a new module or feature, open an issue first.
- Always include tests, using Vitest. Component
packages use
@testing-library/react(render,screen,fireEvent); every other family uses Vitest's built-inexpectagainst real input/output pairs — no placeholders, no commented-out tests. - Keep modules small and focused (one exported component/function per package) and avoid large dependencies.
README.mdfiles are generated from source JSDoc — never edit a package'sREADME.mddirectly; see Documentation.- Before submitting, run
yarn lintandyarn test-unitfrom the repo root.
At the repository root:
$ yarn lintRuns eslint, prettier (for .scss), and stylelint. Follow the
Airbnb JavaScript style guide, which
the eslint config is based on.
A component/store package looks like:
digitransit-<family>-<module>
│ package.json
│ README.md
│ test.js
│ LICENSE-AGPL.txt
│ LICENSE-EUPL.txt
└── src
└── index.js
A search-util/util package is the same, but flat (no build step, no
src/ directory — index.js sits at the package root next to test.js).
src/index.js/index.js— the module's implementation, documented with JSDoc. This JSDoc is the only source of truth for the generatedREADME.md— write real descriptions,@param/@returns, and an@example, not just type annotations.test.js— real, executable Vitest (and, forcomponent, RTL) tests, run against raw source (src/index.js/index.js) rather than a built artifact —component/storepackages need nopretest: yarn buildstep just to test.componenttests use literal JSX.package.json— runtime imports go underdependencies; anything the host app must also provide goes underpeerDependenciesinstead (see Dependency classification); build/compile-only tooling goes underdevDependencies.README.md— generated, do not hand-edit (see below).LICENSE-*.txt— copied from the repository root, do not hand-edit.
There's no scaffolding script (the old per-family create-new-module
scripts were removed — they generated stale, webpack-based tooling that no
package has actually used since the migration to Rollup). Instead:
- Copy the nearest existing sibling package as a starting point, e.g.:
$ cp -r digitransit-component/packages/digitransit-component-icon \ digitransit-component/packages/digitransit-component-<name> - In the new
package.json: rename"name", reset"version"to"0.0.1", update"description", and clear out anything the copied package needed that yours doesn't (extradependencies/peerDependencies). - Delete any build output that came along (
lib/) — it's gitignored and regenerated byyarn build. - Replace
src/index.js(orindex.js) with your implementation, andtest.jswith real tests for it. - Run
yarn installfrom the repo root so the workspace picks up the new package (the rootpackage.json'sworkspacesglobs already cover anydigitransit-<family>/packages/*directory — nothing to add there). - Generate its README: run
yarn workspace-packages-docsfrom the repo root, oryarn docsfrom inside the new package's directory (see Documentation). - Add tests to CI simply by existing —
yarn workspace-packages-test(part ofyarn test-unit) discovers every package in every family automatically via Vitest'sincludeglob (digitransit-<family>/packages/*/test.js).
Tests run on Vitest, configured from a single root
vitest.config.js (one test.projects entry per family, plus one for the
main app suite — see docs/Tests.md). component
runs under a jsdom environment, configured entirely in vitest.config.js
(no setup file): environmentOptions.jsdom.html seeds a persistent
<div id="app"> for @hsl-fi/modal's appElement prop, and globals: true
makes afterEach a real global, which is all RTL's own automatic
cleanup() needs to fire after each test. The other three families run
under plain Node. component's real, un-stubbed ESM @hsl-fi/* peer
dependencies (and their own @radix-ui/@floating-ui dependencies) ship
extensionless react/jsx-runtime imports that Node's own resolver can't
match; component's project config forces those through Vite's own (more
lenient) resolver instead (server.deps.inline), with a small plugin
unwrapping the handful of @hsl-fi/* packages that need a manual
CJS/ESM default-export unwrap.
Run everything from the repository root:
$ yarn test-unitOr just the workspace packages (all four families in one run):
$ yarn workspace-packages-testOr a single package, from inside its own directory:
$ yarn testSome digitransit-component packages ship their own i18next translation
bundle (src/helpers/translations.js or src/utils/translations.js) — one
file per package holding every locale, each nested under a translation
namespace, consumed by a package-local i18n.js instance. This is a
different shape from app/translations/*.js (one locale per file, no
namespace), so it has its own tooling, separate from the root
sort-translations script:
$ yarn workspace-packages-translations-check # verify only, wired into `yarn lint`
$ yarn workspace-packages-translations-fix # sort in place, wired into `yarn format`Both modes also flag any key that isn't present in every locale of a file —
a missing translation, or a typo'd key duplicating another with a different
spelling. That mismatch is never auto-fixed (there's no way to guess the
correct key or translation), so --fix still exits non-zero if any remain;
resolve those by hand in the package's translations.js.
Every package's README.md is generated from its JSDoc by
documentation.js, via the single shared
scripts/workspace-packages/generate-readmes.js script. If you find an
error in a README, fix the source JSDoc and regenerate — never hand-edit
the README.md file. A hand-edit will silently disappear the next time
anyone regenerates it.
The script needs no family/package argument — it works out what to regenerate from where it's run: from inside a single package's directory it regenerates just that package; from anywhere else (e.g. the repository root) it discovers and regenerates every package in every family.
# regenerate one package's README (run from inside the package's directory)
$ yarn docs
# regenerate every package, in every family (run from the repository root)
$ yarn workspace-packages-docsCI enforces this: the check-readmes job in .github/workflows/dev-pipeline.yml
regenerates every family and fails the build if that produces any diff
against what's committed — so a PR that changes JSDoc without regenerating
its README (or one that only hand-edits a README) won't merge.
$ yarn workspace-packages-publish # interactive, for local/manual use
$ yarn workspace-packages-publish-ci # non-interactive (-y), used by CIBoth run lerna publish from-package --no-git-tag-version --no-push;
the only difference is CI's -y to skip lerna's confirmation prompt.
Versioning is independent per package (lerna.json's "version": "independent")
and bumped manually:
$ yarn workspace-packages-version-bump # git fetch --tags && lerna version --include-merged-tagslerna version pushes the version-bump commit and its tags to the git
remote by default; this is intentional here so release tags actually reach
GitHub. git fetch --tags first, plus --include-merged-tags, keep
independent-mode change detection (which package changed since its last
release tag) reliable even when local tags are stale or only exist on
another branch pending merge.
yarn workspace-packages-version-check then verifies every internal
@digitransit-* dependency range across all four families is satisfied by
the versions actually present — this runs in CI on every push/PR.
lerna.json's command.publish.conventionalCommits is deliberately false
— changelog generation is not automated, and there's no commitlint
enforcing commit message format. Don't enable either; this project's commit
history doesn't follow the Conventional Commits format the tooling expects.
When adding an import to a package's src/index.js/index.js, classify it
in package.json as:
dependencies— a real runtime dependency that this package should pull in on its own (e.g.lodash,downshift).peerDependencies— anything the host application is expected to already provide a single shared instance of. This includesreact,react-dom, and every@hsl-fi/*package — regardless of whether it's imported directly or only used in an SCSS@import(e.g.@hsl-fi/sass). Keep the version range in sync with what the rootpackage.jsonactually installs; a stale/wrong peer range (too narrow, or naming a major version the host doesn't ship) is a bug even if it happens to install locally.devDependencies— anything needed only to build, test, or generate docs for the package itself (e.g.babel-plugin-relayforquery-utils's own relay-compiler step) but that the published bundle never needs at runtime.
component/store packages using SCSS (e.g. src/helpers/styles.scss) get
it compiled as a CSS Module by the shared config/rollup.config.js, which
runs once per package with cwd set to that package's own directory. Since
many packages share the same relative file path (e.g.
src/helpers/styles.scss), the config passes a per-package hashPrefix
(the package's npm name) into postcss-modules so that two unrelated
packages using the same local class name (e.g. .combobox-icon) never hash
to the same scoped class name and leak styles into each other once bundled
into the app — don't remove hashPrefix/autoModules: false from that
config without preserving this.
Because nx.json's build target defines an explicit inputs list, any
future shared build config file (beyond config/rollup.config.js/
config/babel.config.cjs, already listed there) needs adding to that list
too, or editing it won't invalidate every package's Nx build cache.
These packages are marked deprecated — via a @deprecated JSDoc tag (which
also shows up in the generated README.md) and a "deprecated" field in
package.json (which npm surfaces on install/publish) — and are scheduled
for removal in a future change. Don't add new code to them, and don't add
any new dependents.
@digitransit-component/digitransit-component— this family's meta-package, re-exporting every sibling component. No call site inapp/**used it; every consumer imports the specific@digitransit-component/digitransit-component-*sub-package it needs directly.@digitransit-util/digitransit-util— this family's equivalent meta-package. Same reasoning; import the specific@digitransit-util/digitransit-util-*sub-package instead.@digitransit-component/digitransit-component-abtesting— never adopted, unused.@digitransit-component/digitransit-component-traffic-now-link— no longer maintained or used.@digitransit-component/digitransit-component-with-breakpoint— no direct consumers.@digitransit-store/digitransit-store-common-functions— no real consumers.@digitransit-util/digitransit-util-route-pattern-option-text— no longer used by the app; route pattern option texts are built inapp/component/routepage/instead.@digitransit-search-util/digitransit-search-util-execute-search-immidiate— renamed due to a spelling fix; use@digitransit-search-util/digitransit-search-util-execute-search-immediateinstead.