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

@defra/lis-infra-ui-services

v0.48.0

Published

UI-Services shared definitions for the livestock taxonomy topology.

Readme

lis-infra-ui-services

Part of the Livestock Information Service (LIS), which lets livestock keepers, owners, agents and delegates record and manage their animals.

@defra/lis-infra-ui-services is a shared npm package with the UI building blocks that LIS hubs and spokes share: the page layout, shared error pages, scheduled maintenance, Nunjucks setup, GOV.UK Frontend scripts and styles, static files, security headers and session cache.

The templates follow the GOV.UK Design System.

Used by:

Installation

npm install @defra/lis-infra-ui-services \
  @defra/lis-hubs-infra-access @defra/lis-hubs-infra-core @defra/lis-hubs-infra-registry govuk-frontend

These are peer dependencies, so the consuming app must install them too:

  • @defra/lis-hubs-infra-access >=0.29.0 <1.0.0
  • @defra/lis-hubs-infra-core >=0.4.0 <1.0.0
  • @defra/lis-hubs-infra-registry >=0.6.0 <1.0.0
  • govuk-frontend >=6.5.0 <7.0.0

Usage

Import from the subpath you need, for example @defra/lis-infra-ui-services/errors. The package has no config of its own. Each function takes the values it needs from the consuming app's convict config.

Nunjucks

createNunjucksConfig returns a registration for @hapi/vision. It sets up Nunjucks with the GOV.UK Frontend templates, this package's layout (layouts/base.njk) and components, and the shared error pages. It also adds the filters and globals, and a view context with assetPath, serviceName, getAssetPath, isSignedIn, hasPermission and errorPages.

import { createNunjucksConfig } from '@defra/lis-infra-ui-services/nunjucks/plugin'

const nunjucksConfig = createNunjucksConfig({
  config, // reads root, assetPath, serviceName, isProduction, nunjucks.watch, nunjucks.noCache
  logger,
  moduleId: 'cattle-home' // spokes only
})

await server.register(nunjucksConfig)

The template components are appHeading, appAutocomplete, lisSubNavigation, plus the search form, sortable paginated table and date templates. createNunjucksContextBuilder (also exported from /nunjucks-context) builds just the view context. The filters formatAge, formatDate, formatEarTag, formatCurrency and queryString are also exported from /nunjucks/filters, and the individual /nunjucks/format-currency, /nunjucks/format-date and /nunjucks/query-string subpaths. /nunjucks/globals exports PERMISSIONS and govukRebrand (always true, which turns on the GOV.UK rebrand in templates).

Asset path

A hub serves its built CSS and JavaScript at its configured assetPath. A spoke serves its own at its assets mount from @defra/lis-hubs-infra-registry (/assets/cattle-home), which the hub proxies to it unchanged, so the same URL works through a hub or directly. createAssetPathHelpers resolves that root once; getAssetPaths feeds the static files plugin and the Nunjucks context derives the same root itself from moduleId.

import { createAssetPathHelpers } from '@defra/lis-infra-ui-services/asset-path'

const { getAssetPaths } = createAssetPathHelpers({
  moduleId: 'cattle-home', // leave out in a hub
  assetPath: config.get('assetPath')
})

The standalone getAssetRoot is exported too.

Static files

import { createStaticFilesPlugin } from '@defra/lis-infra-ui-services/static-files'
import { statusCodes } from '@defra/lis-infra-ui-services/status-codes'

const serveStaticFiles = createStaticFilesPlugin({
  assetPaths: getAssetPaths(),
  staticCacheTimeout: config.get('staticCacheTimeout'),
  noContentStatusCode: statusCodes.noContent
})

It serves /favicon.ico and the files under each asset path. /status-codes exports statusCodes. /duration exports the milliseconds and seconds lookups and unixEpoch().

Config formats

/config-formats exports convict formats for settings the hubs and spokes share. Register each with convict.addFormat(); a bad value fails config.validate() with the setting named.

  • positiveInteger ('positive-integer'): a whole number of at least 1, for timeouts, TTLs and circuit breaker thresholds, where 0 is a misconfiguration rather than "off". Values from environment variables are coerced from strings.
  • duration({ max }) ('duration', or 'duration-max-P7D' when max is given): a length of time read as milliseconds. An environment variable may give an ISO 8601 duration of days, hours, minutes and seconds (PT4H, P7D, P1DT12H) or a whole number of milliseconds; both are coerced to milliseconds (the schema default must be a number). max is itself an ISO 8601 duration and rejects anything longer, for SESSION_CACHE_TTL where CDP requires sessions to expire within 7 days.
  • cdpEnvironment ('cdp-environment'): one of the CDP environments the platform injects as ENVIRONMENT (infra-dev, management, dev, test, ext-test, perf-test, prod), or local or docker_compose outside CDP. One list for every service, so a new environment is added here rather than in each repository's own enum; ENVIRONMENTS exports the list.
  • secret ('secret'): a signing or encryption secret of at least 32 characters with no default, for HUB_JWT_SECRET and SESSION_COOKIE_PASSWORD. The setting must also be marked sensitive: true, or convict appends the value to the validation error.
import convict from 'convict'
import {
  cdpEnvironment,
  duration,
  positiveInteger,
  secret
} from '@defra/lis-infra-ui-services/config-formats'
import { milliseconds } from '@defra/lis-infra-ui-services/duration'

const sessionTtl = duration({ max: 'P7D' })
convict.addFormat(cdpEnvironment)
convict.addFormat(positiveInteger)
convict.addFormat(sessionTtl)
convict.addFormat(secret)

environment: {
  format: 'cdp-environment',
  default: 'local',
  env: 'ENVIRONMENT'
},
sessionCacheTtl: {
  format: sessionTtl.name,
  default: milliseconds.fourHours,
  env: 'SESSION_CACHE_TTL'
},

timeout: {
  format: 'positive-integer',
  default: 5000,
  env: 'CATTLE_HOME_API_TIMEOUT'
},
hubJwtSecret: {
  format: 'secret',
  default: null,
  env: 'HUB_JWT_SECRET',
  sensitive: true
}

Security headers

securityHeaders is a hapi plugin that adds HSTS, x-content-type-options, referrer-policy and permissions-policy headers to every response, including error responses. Register it after catchAll, so its onPreResponse runs last.

import { securityHeaders } from '@defra/lis-infra-ui-services/security-headers'

await server.register(securityHeaders)

Session cache

getCacheEngine picks Redis or in-memory Catbox. createSessionCachePluginForConfig registers yar from the session config (cache.name, cache.ttl, cookie.password, cookie.ttl). Session cookies are always Secure. Both engines cap the ttl of every write at MAX_CACHE_TTL (7 days), so no cache policy can keep session data longer than CDP allows.

import { getCacheEngine } from '@defra/lis-infra-ui-services/session-cache/cache-engine'
import { createSessionCachePluginForConfig } from '@defra/lis-infra-ui-services/session-cache'

const server = hapi.server({
  cache: [
    {
      name: config.get('session.cache.name'),
      engine: getCacheEngine({
        engine: config.get('session.cache.engine'), // 'redis' or anything else for memory
        config, // reads redis when the engine is 'redis'
        logger
      })
    }
  ]
})

await server.register(createSessionCachePluginForConfig(config))

/redis-client exports buildRedisClient, which getCacheEngine uses. It takes the redis config (host, port, keyPrefix, username, password, useTLS, useSingleInstanceCache).

Outbound proxy

setupProxy sends fetch and other HTTP clients through the proxy when proxyUrl is set. It does nothing when it is empty.

import { setupProxy } from '@defra/lis-infra-ui-services/proxy/setup-proxy'

setupProxy({ proxyUrl: config.get('httpProxy'), logger })

Referrer link

createReferrerLink remembers where a user came from, so a page reached from a list can link back to that list with its query. Use getSameHostReferrerPath to read the referrer.

import { createReferrerLink } from '@defra/lis-infra-ui-services/referrer-link'

const referrerLink = createReferrerLink(server, {
  cacheName: 'session',
  allow: (path) => path.startsWith('/cphs'),
  fallbackHref: '/cphs'
})

const back = await referrerLink.resolve(request) // { id, href } or null
const href = referrerLink.link('/cphs/12/34/5678', back?.id ?? null)

Microsite paths

The main entry point (@defra/lis-infra-ui-services) exports buildMicrositePath(taxonomy, species), which returns /<species>/<taxonomy>.

Error pages

catchAll from @defra/lis-infra-ui-services/errors, registered as an onPreResponse extension, renders one of four shared pages from src/nunjucks/routes/error/ by status code:

| Status | Page | | ------------- | ---------------------- | | 404 | page-not-found | | 401, 403 | not-authorised | | anything else | problem-with-service |

A 503 from a dependency (a service token that cannot be issued, an open circuit) is a problem with the service, not maintenance. The service-unavailable page is only rendered by the maintenance plugin below, which knows when the service is back.

5xx responses are logged at error, and 4xx responses other than 401, 403 and 404 at warn, each with the error object and the ECS fields http.request.method, url.path and http.response.status_code. Boom messages are logged, so application code must not put user-supplied text in them.

import { catchAll } from '@defra/lis-infra-ui-services/errors'

server.ext('onPreResponse', catchAll)

The contact link on these pages comes from the errorPages option of createNunjucksConfig (or createNunjucksContextBuilder), exposed to templates as errorPages. The key is optional:

createNunjucksConfig({
  config,
  logger,
  errorPages: {
    contactHref:
      'https://www.gov.uk/government/organisations/british-cattle-movement-service' // default
  }
})

Layout scripts

layoutComponents from @defra/lis-infra-ui-services/components/layout is the list of GOV.UK Frontend components the layout template renders: Service navigation and Skip link.

errorPageComponents from @defra/lis-infra-ui-services/components/error-pages is the list the shared error pages render (Button, for the not-authorised sign-in button). Every app that serves the shared error pages must add it.

Start each with createAll, alongside any page-level components (see the full example under Scrollable table).

Wrap the combined list in a Set, so a component that appears in more than one list (or that the app also lists itself) is only initialised once: govuk's base class logs an InitError on double init.

Scrollable table

Apps that render the sortable paginated table macro also start ScrollableTable from @defra/lis-infra-ui-services/components/scrollable-table. It drops the tab stop (and the region role and label) from a table wrapper that fits, and restores them when the table overflows, re-checking whenever the wrapper or table changes size. Without JavaScript the wrapper stays focusable.

import { createAll, Tabs } from 'govuk-frontend'
import { errorPageComponents } from '@defra/lis-infra-ui-services/components/error-pages'
import { layoutComponents } from '@defra/lis-infra-ui-services/components/layout'
import { ScrollableTable } from '@defra/lis-infra-ui-services/components/scrollable-table'

for (const component of new Set([
  ...layoutComponents,
  ...errorPageComponents,
  ScrollableTable,
  Tabs
])) {
  createAll(component)
}

Autocomplete

For the appAutocomplete template component, start the script with initAllAutocompletes. It enhances each input with accessible-autocomplete.

import { initAllAutocompletes } from '@defra/lis-infra-ui-services/components/autocomplete'

initAllAutocompletes()

Styles

Import the Sass files you need from your app's stylesheet: components/autocomplete.scss, components/search-form.scss, components/service-navigation.scss, components/sortable-paginated-table.scss and components/sub-navigation.scss.

@use 'pkg:@defra/lis-infra-ui-services/components/service-navigation.scss';

Scheduled maintenance

Each hub switches maintenance on with its own config value, maintenance.availableFrom: the UK local date-time the service is back, in ISO 8601 form without an offset, e.g. 2026-11-19T09:00 or 2026-11-19T09:00:00. Values with a Z or +hh:mm offset, and dates without a time, throw at start-up. An empty value leaves the service running as normal.

// config.js
maintenance: {
  availableFrom: {
    doc: 'UK local date-time the service is back from, without an offset; empty when not in maintenance',
    format: String,
    default: '',
    env: 'MAINTENANCE_AVAILABLE_FROM'
  }
}

// server.js, after @hapi/vision is registered
import { createMaintenancePlugin } from '@defra/lis-infra-ui-services/maintenance'

await server.register(
  createMaintenancePlugin({
    availableFrom: config.get('maintenance.availableFrom'),
    healthPath: '/health', // default
    assetPath: config.get('assetPath') // default '/public'
  })
)

While availableFrom is set, every request except the health check and assets under assetPath gets the service unavailable page with a 503, showing the time as "9am on Thursday 19 November 2026" and carrying it in a Retry-After header.

Cookies

The base layout shows the GOV.UK cookie banner until the visitor has made a choice, and its footer links to a cookies page at /cookies. The choice is kept for a year in a cookies_policy cookie (URL-encoded JSON, readable by client-side scripts so analytics can check it before loading), set by the banner's JavaScript or, without JavaScript, by posting the banner's form to /cookies. Each hub registers the page with the essential cookies it sets, which the page lists:

// server.js, after @hapi/vision is registered
import { createCookiesPlugin } from '@defra/lis-infra-ui-services/cookies'

await server.register(
  createCookiesPlugin({
    essentialCookies: [
      {
        name: config.get('session.cache.name'),
        purpose: 'Keeps you signed in as you move between pages',
        expires: 'After 4 hours, or when you sign out'
      }
    ]
  })
)

GET /cookies shows the page, with a form to change the choice. POST /cookies records a choice (analytics=yes|no), ignoring cross-site posts, and redirects to the page with a confirmation and a link back to the same-host page the visitor came from. The CookieBanner component is part of layoutComponents (see Layout scripts), so a hub that already initialises those needs no further client-side change. getCookiesPolicy(request) from @defra/lis-infra-ui-services/cookies/cookies-policy reads the recorded choice server-side.

Environment variables

The package reads no environment variables itself. It takes all its settings from the config object the consuming app passes in. In the example above, MAINTENANCE_AVAILABLE_FROM is a variable the hub defines in its own config.

Prerequisites

  • Node.js 24 (see .nvmrc, use nvm use)
  • To check changes against a consuming app: a local clone of lis-dev with its one-time setup done

Setup

Development

nvm use
npm install

Test

There are no tools to install beyond npm install. Tests use Vitest.

Running in development

This is a library, so it has no server of its own. To try a change in a consuming app without publishing, link the local copy into the lis-dev stack:

cd lis-dev
.bin/sideload link infra/ui-services # every installed copy now points at this repo
.bin/sideload up front-office       # optional: run the consumer locally with hot reload
.bin/sideload unlink infra/ui-services # when done

Running tests

npm test                 # unit tests
npm run test:coverage    # with coverage
npm run lint             # ESLint
npm run format:check     # Prettier

Before releasing a change, also run the tests of each consuming repo against the linked package.

Pipelines

Build, versioning and publishing run in GitHub Actions (.github/workflows) using Cake scripts (*.cake):

  • Pull request to main: validate (format, lint, test), Sonar analysis, version check
  • Push to a feature/ or hotfix/ branch: prerelease version and tag
  • Merge to main: release tag, then publish to npm

Versions come from git tags (SemVer). After a merge, wait for the publish job to finish before bumping the package in consuming repos.

Branching policy

The repo uses mainline development. Branch from main and name the branch:

  • feature/LREG-<ticket>-<short-kebab-description>
  • hotfix/LREG-<ticket>-<short-kebab-description>

LREG must be uppercase, because the GitHub ruleset rejects other branch names. Rebase onto main rather than merging main in.

Contributing to this project

  • Follow the Defra software development standards.
  • npm install sets up a pre-commit hook (Husky) that runs the security audit, format check and lint.
  • Open a pull request against main using the PR template. CI must pass and the Sonar quality gate must be green before merge.
  • Feature branches bump the minor version and hotfix branches the patch version. While the package is on 0.x, a breaking change to the public API ships as a minor version, so call it out in the PR so consumers can update.

Licence

THIS INFORMATION IS LICENSED UNDER THE CONDITIONS OF THE OPEN GOVERNMENT LICENCE found at:

http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3

The following attribution statement MUST be cited in your products and applications when using this information.

Contains public sector information licensed under the Open Government licence v3

About the licence

The Open Government Licence (OGL) was developed by the Controller of His Majesty's Stationery Office (HMSO) to enable information providers in the public sector to license the use and re-use of their information under a common open licence.

It is designed to encourage use and re-use of information freely and flexibly, with only a few conditions.