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

@weasel-js/labkit

v1.13.0

Published

React widgets for building self-contained interactive lab pages

Readme

@weasel-js/labkit

React components for building labs: pages where you change parameters and watch something redraw. A lab hosts one or more instruments — self-contained experiments that own their settings and their state and render a picture from both. Each open instrument gets a trial, one tile of the lab's workspace, and the trial supplies the apparatus around it: a control panel, a toolbar, a sidebar, undo, snapshots, persistence and drawing tools.

Reach for it when you have a visualization, a simulation or a rendering algorithm and would rather not build the page around it.

Install

npm i @weasel-js/labkit @weasel-js/core

@weasel-js/core is a peer dependency, pinned to the matching version. labkit does not ship its own copy: core keeps its content handlers, paint kinds, shape painters and markers in module-global registries, and two copies means anything registered through one is invisible to the other — a blank canvas with no error.

React 19 is a peer dependency too (react and react-dom, ^19.0.0). One stylesheet import covers everything labkit draws, including the theme tokens and the @weasel-js/ui components it passes through:

import '@weasel-js/labkit/styles.css';

Its glyphs come from the app's icon registry, which labkit does not mount: with none, every button in the lab draws a placeholder and warns. Mount one at the root — the whole kit's tables, or the trimmed set virtual:weasel-icons builds (docs/icons.md in the weasel repo):

import { IconRegistryProvider } from '@weasel-js/core';
import { kitIcons } from '@weasel-js/ui/kit-icons';

<IconRegistryProvider icons={kitIcons}>
  <App />
</IconRegistryProvider>

A lab

import { type ConfigOf, defineInstrument, f, Lab } from '@weasel-js/labkit';
import '@weasel-js/labkit/styles.css';

const config = f.schema({
  bins: f.number(20).range(5, 100).step(1).label('Bins'),
  showAxes: f.boolean(true),
});

const Histogram = defineInstrument<{ samples: number[] }, ConfigOf<typeof config>>({
  name: 'Histogram',
  config,
  initialState: () => ({ samples: sample(500) }),
  render: () => null,
  canvas: {
    layers: [
      { id: 'axes', draw: drawAxes },
      { id: 'bars', draw: drawBars },
    ],
  },
  layers: { ids: ['axes', 'bars'] },
});

export function App() {
  return (
    <Lab
      instruments={[Histogram]}
      defaultInstrument="Histogram"
      title="Histogram"
      storageKey="histogram-lab"
    />
  );
}

storageKey makes the lab persist: its trials, snapshots, layout and mode survive a reload and stay in step across tabs. It persists to IndexedDB — or localStorage, with a warning, where IndexedDB will not open — and storage names another substrate: localStorageAdapter, sessionStorageAdapter, urlHashAdapter, or your own StorageAdapter over a server. The lab renders fallback while it loads. Leave storageKey off and nothing persists.

Presenting a lab

<Lab present> shows one trial and nothing else — no lab chrome, no trial chrome, a transparent ground — for embedding a lab as a figure. ?present in the page's URL does the same, so one build serves both the lab and its embed. seed={{ instrument, config, state, view }} says what that trial opens on. A stored lab presents from records of its own (storageKey + ':present'), so an embed never shows a visitor their last session in the full lab or writes into it; changing the seed reopens the trial on it for returning visitors.

Inside any lab, usePresentation() gives { active, enter, exit }: enter presents the focused trial and Escape returns. Nothing unmounts either way, so a canvas keeps its context and a clock its place. Entering refits a view the instrument places by the viewport's size (initialView as a function, or a fitted stage) to the presented box; leaving gives the tile its view back.

A presented trial whose instrument declares a clock gets play controls along its bottom: play and pause, speed and loop, and — for a seekable clock with a duration — a scrub bar and a reverse switch. Space plays and pauses, the arrows step, Home and End jump, R reverses and < / > change speed. A run that ends plays again after three seconds until a visitor touches the controls, and the controls hide in a box narrower than 480px — <Lab transport={{ minWidth: 760 }}> moves that line. <Lab transport={false}> leaves them off; <TrialTransport> is the same controls for any lab chrome.

A host page with a play control of its own reaches the lab by postMessage, in a frame or in the page itself. A presented lab answers these, and posts its state to the window around it as it starts, on every change, and when asked — never per frame:

const lab = iframe.contentWindow; // `window` for a <Lab present> in this page
lab.postMessage({ type: 'labkit:play' }, origin); // an ended run starts over
lab.postMessage({ type: 'labkit:pause' }, origin);
lab.postMessage({ type: 'labkit:seek', time: 1200 }, origin); // ms into the pass
lab.postMessage({ type: 'labkit:query' }, origin); // ask for the state

window.addEventListener('message', ({ data }) => {
  if (data?.type !== 'labkit:state') return;
  // { clock, playing, ended, time, duration, rate, seekable }
  button.textContent = data.playing ? 'Pause' : 'Play';
});

<Lab transport={false}> leaves the lab's own controls off for a page that draws its own. <Lab messages={{ name: 'hero' }}> names a lab on a page with several: a message carrying lab: 'hero' reaches only it, and its state says lab: 'hero'. messages={{ origins: [...] }} says which origins it takes messages from, and messages={false} turns the channel off. The types are PresentationCommand and PresentationState.

<Lab still>, or ?still in the URL, presents the lab held still for a poster: every clock paused where it opens — still={1200} or ?still=1200 holds them 1200 ms in — with no play controls and nothing read from or written to storage, so it shows the seed in anyone's browser. The lab's root gains lk-lab--still-ready once that frame has been drawn, which is what a capture waits for:

await page.goto('https://example.com/lab.html?still=1200');
await page.locator('.lk-lab--still-ready').waitFor();
await page.locator('.lk-lab').screenshot({ path: 'poster.png', omitBackground: true });

A clock that is not seekable is run forward to the time before the still is ready, step ms a frame, when its declaration names a step; without one it stays where it opens. Whatever an instrument animates off the clock or loads on its own is the instrument's to settle.

gestures on <Lab> decides what the trials' cameras take — { drag, wheel: 'plain' | 'mod' | false, pinch, tap, home }, over each instrument's own canvas.gestures / stage.gestures / orbit.gestures — and whatever they leave reaches the page: <Lab present gestures={{ drag: false, wheel: false }}> is an embed the page scrolls past, keeping only the pinch. The keys mean the same on every camera: drag pans a 2D one and turns an orbit, and home — a double click back to where it opened — is on by default only for an orbit.

A widget's own state persists the same way through usePersistedState(name, initial), which is useState whose value survives a reload — kept per trial inside one, and plain useState wherever no <Lab> or <Persistence> is above it.

f.schema states the config once — values, types and controls — and the trial renders the settings panel from it. render returns the instrument's own DOM; returning null alongside canvas means the canvas layers are the whole picture.

A value is addressed by its path: bins above sits at config.bins and is written as setConfig('bins', 40). f.group nests one — paper: f.group({ width: f.number(210) }) puts the value at config.paper.width and addresses it as 'paper.width', in setConfig, in a control override, and anywhere else a path is taken. .section('Advanced') only puts a heading over sibling rows and leaves their paths alone.

A group's card or a section's frame can be drawn in any MotifFrame motif from @weasel-js/ui: f.group({...}).motif(stereo()), or .section('Look', { motif: stereo() }). <ControlPanel motif> sets the motif for every group and section that names none; unset, they keep the default card.

A stored config missing something its schema has since gained — a whole nested group included — is filled from the instrument's defaults as it loads, so growing a schema never strands a saved trial or a snapshot.

Auto

A field can be auto: nobody is pinning it, and the instrument decides what it should be. Shift-click a row in the settings panel, or click the pin dot beside its label, and the control draws ghosted at whatever the value resolved to. Shift-click again — or just grab the control — and it pins back at the value it was holding all along.

auto is a value, so it goes wherever a config value goes:

import { auto, f } from '@weasel-js/labkit';

const config = f.schema({
  width: f.number(432),
  gap: f.number(12).auto((c) => (c.width as number) / 24),
  cols: f.number(3).initial(auto),
  seed: f.number(1),
  scale: f.number(1).auto(),
});

setConfig('gap', auto);
addTrial('demo', { gap: auto });

A field can be auto only if it says so. .auto() opts it in, and an auto field reads undefined. .auto(fn) also attaches a resolver: the instrument reads the computed value rather than writing ?? compute() itself, and the ghosted control has a real number to draw. .initial(auto) opts in and opens the field auto instead of pinned at its default. A field that says none of these, like seed, has no dot and no gesture: the default for a value the instrument cannot receive as undefined, like a color going straight to a uniform.

Nothing stores the sentinel. It is normalized at every boundary into the trial's set of unpinned paths, and the pinned value stays in the config underneath, so un-pinning loses nothing and Reset returns the trial to the auto fields it opened on.

Capabilities

A capability is a field on the instrument. Declaring it is what makes the trial provide the corresponding chrome; they compose freely.

| Field | What the trial then provides | | --- | --- | | canvas | A stack of layered <canvas> elements with pan and zoom, redrawing only the layers that changed | | stage | Pan and zoom over the instrument's own DOM, laid out at a fixed size | | orbit | A 3D orbit camera — turn, pan, dolly, home — for a renderer the instrument brings | | layers | A sidebar list that hides and reorders those layers | | annotations | Drawing tools, an overlay on each region you name, a Marks panel, undo and export | | dragDrop | A palette to drag items from, and a drop pipeline that hands you world coordinates | | undo | Undo and redo buttons, snapshotting state on the events you name — or stepping a history of the instrument's own, handed over with useTrialHistory | | job | Starts async work, aborts it on unmount and on a key change, and renders progress and a cancel control | | tools | A tool palette and a tool slot on the trial | | chrome | Anything else, as contributions keyed to a region |

Annotations

An instrument names regions of itself that accept marks — freehand strokes, lines, arrows, rectangles, ellipses and text drawn over its picture. labkit supplies the tool palette, the overlay, the store, undo, persistence and export.

const paneRef = createRef<HTMLDivElement>();

defineInstrument<State, Config>({
  // …
  annotations: {
    targets: () => [
      { id: 'pane', ref: paneRef, content: { w: 260, h: 180 }, positionDependsOn: ['angle'] },
    ],
    meaning: {
      statuses: [
        { id: 'open', label: 'Open', color: '#e5484d' },
        { id: 'fixed', label: 'Fixed', color: '#30a46c' },
      ],
    },
  },
});

A mark's position is stored as fractions of its target's content box, so it stays on the same feature when the picture is rendered at a different size. positionDependsOn names the config keys that move the picture; labkit snapshots their values beside each mark, and a mark whose values have since changed draws dashed and reports isStale. meaning.statuses is the optional vocabulary a mark can be labelled with — a status carries its own color, which the mark on the canvas follows.

targets is declared once per instrument and called once per trial, with the asking trial as its third argument: targets(state, config, trial). trial.id is the id useTileId scopes a surface tile under, so a consumer holding its own per-trial refs keys them by it and hands back the ones this trial owns; a module-level ref like the one above is one ref shared by every trial of the instrument, which only shows once a second trial is open. trial.view is that trial's camera.

Marks live in the trial's record and survive a reload. An instrument that would rather keep them in a format it already owns declares annotations.storage with a load/save pair, and labkit never writes its own slot.

useAnnotations() reaches the store from the instrument's render or from a chrome contribution, and re-renders its caller as marks change:

const marks = useAnnotations();
const stale = marks.query({ target: 'pane' }).filter((m) => marks.isStale(m, config));

selection() answers which marks the user has picked on the overlay, as annotation ids, and setSelection() replaces them — the same selection weasel's own click, marquee and handles drive, merged across every target. A panel that opens a card for the clicked mark subscribes and re-reads it.

It also carries get, hitTest, within, add, update, setMeta, remove, undo/redo and capture.

Export. A target declaring base() hands over the picture underneath its marks — SVG markup, an image src or a canvas — and capture(target, { format, scale }) returns a Blob with the marks drawn on top, as PNG or, from an SVG base, as vector SVG. Declaring annotations earns an Export button in the toolbar; onCapture fires after every export, labkit's own chrome included.

The pieces are exported for a host that wants to assemble them itself: createAnnotationStore, <AnnotationOverlay>, <AnnotationTargets>, <MarkList>, <ExportMenu>, ANNOTATION_TOOLS and capturePlan.

A weasel scene in a trial

An instrument whose subject is an editable scene — tools, selection, undo, modes — renders a <TrialSceneCanvas> and declares no canvas or stage. The trial then runs no camera of its own, so every pointer, wheel and key gesture over the canvas is the scene's, its pan and zoom included.

import { createScene } from '@weasel-js/core';
import { gridSnapSource } from '@weasel-js/guides';
import { icon } from './icons.gen'; // the app's icon(): see @weasel-js/icons
import {
  type ConfigOf, defineInstrument, f, type RenderContext, type SceneTrialState,
  TrialSceneCanvas, useTrialScene,
} from '@weasel-js/labkit';

const config = f.schema({ snap: f.boolean(true).label('Snap to grid') });

function Body({ ctx }: { ctx: RenderContext<SceneTrialState, ConfigOf<typeof config>> }) {
  const scene = useTrialScene(ctx, () => createScene({ systemLayers: [{ id: 'default' }], initial: NODES }));
  const snap = useMemo(() => ({ sources: ctx.config.snap ? [gridSnapSource({ spacing: 20 })] : [] }), [ctx.config.snap]);
  return (
    <TrialSceneCanvas features={['draw']} defaultTools={['rect', 'pen']}
      scene={scene} trial={ctx.trial} snap={snap} />
  );
}

defineInstrument<SceneTrialState, ConfigOf<typeof config>>({
  name: 'Scene',
  config,
  initialState: () => ({ scene: null }),
  undo: {},
  tools: { tools: [{ id: 'select', label: 'Select', icon: icon('core:select') }, { id: 'rect', label: 'Rectangle', icon: icon('core:rect') }] },
  render: (ctx) => <Body ctx={ctx} />,
});
  • The scene. useTrialScene builds it once per trial, writes every edit its history records into state.scene, and loads whatever arrives there from outside — Reset, Clone, a snapshot, a reload — into the same Scene. create is what a fresh or reset trial opens on.
  • Undo. Declaring undo gives the buttons; <TrialSceneCanvas> hands the trial the scene's history, so the buttons and Mod+Z step scene.undo() and scene.redo(), and the trial stops snapshotting state. Any history of an instrument's own goes the same way: useTrialHistory({ canUndo, canRedo, undo, redo, subscribe }).
  • Tools. A tools capability whose ids are the canvas's tool ids makes the trial's palette the canvas's tool slot, in both directions: a pick there activates the tool, and the canvas's own hotkeys move the palette. ctx.trial.setActiveTool(id) switches tool from anywhere else.
  • Config. ctx.config is current on every render, so props built from it — selectTool, toolOptions, layers, a mode registry — reach the canvas live. A tool of your own reads it through thunked binding params, as core's usePolygonTool does, so a change mid-gesture reaches it too.
  • Modes. Pass modes={registry} as to any <SceneCanvas>, holding the registry in state so each trial has its own; registry.setMode(id) switches.

The trial mounts the kit's providers isolated from its neighbors, so each trial's canvas keeps its own selection and tool, and needs no <WeaselProvider>.

Chrome

A trial's chrome has six named regions — titlebar, toolbar, palette, sidebar, viewport, status. A TrialContribution names one and supplies data the region lays out, or a render function that opts out of that layout. The palette region is a rail of tools and commands: a ToolItem writes the tool slot when pressed and reads as current while the slot names it, and a CommandItem carries an onActivate, runs once per press, and never latches:

<Lab
  chrome={[{ id: 'seed', region: 'status', item: { text: `seed ${seed}` } }]}
  suppress={['fps']}
  // …
/>

Contributions passed to <Lab> apply to every trial; an instrument's own chrome field applies to its trials only. suppress drops a built-in by id and throws on an id that is not there. A suppressed button takes its shortcut with it: with snapshot gone, Mod+S is the browser's again.

header={false} drops the lab's own header bar — title, add-trial, zoom, and theme switcher — the menu bar, and the zoom keys with them, for a lab embedded in a page that frames it.

A lab also draws a menu bar above its header: Lab (add a trial, the header's items), then the menus you declare with menus, then View (zoom, color mode). It adds to the header and takes nothing from it. A row names a command by id, and any child of <Lab> registers one with useCommand (header={false} unmounts <Lab>'s children, and their commands with them):

const menus = {
  schema: pref.group('', {
    tools: pref.group('Tools', { reseed: pref.action('app.reseed') }, { as: 'menu' }),
  }),
};

function Reseed() {
  useCommand('app.reseed', 'Reseed', () => setSeed(Math.random()), {
    keys: { kind: 'key', key: 'r', mods: { mod: true, shift: true } },
  });
  return null;
}

<Lab menus={menus}>
  <Reseed />
</Lab>;

A menu keyed lab or view adds its rows to that built-in menu. menus also takes values and onChange for boolean and enum rows. menuBar={false} drops the bar; presenting hides it too. The header's own items are commands as well, lab.item.<id>, and an item's keys binds one wherever focus is in the lab, so a key typed in a text field still types. contextMenus takes MenuDefs that a right-click, Shift+F10 or the ContextMenu key opens over a trial's camera; a lab that declares none keeps the browser's own menu.

A sidebar section can be torn out of the trial into the workspace — as a tile beside the trials, or as a floating panel — with the trial still rendering into it. SidebarSection.undockable and undockAs control the offer; undockPanel / dockPanel move one.

Theming

labkit ships one theme, interstellar — a cosmic dark over weasel's own light mode — as a value, not a stylesheet. <Lab> and <LabShell> apply it for you; mode is "auto" (follow the OS), "light" or "dark". Only styles.css needs importing, and the token values arrive through the provider:

import { interstellarTheme } from '@weasel-js/labkit';
import { ThemeProvider } from '@weasel-js/theme/react';

<ThemeProvider theme={interstellarTheme} selection={{ mode: 'dark' }}>…</ThemeProvider>

Style your own surfaces against the --wzl-* tokens so they follow the mode the lab is in. Class names are lk-* and are not public API — every component takes a className.

Without the runtime

The presentational pieces work on their own. <LabShell> is a titled page frame; <Workspace> is a tile grid that can be resized and reordered and takes any children:

import { LabShell, Workspace } from '@weasel-js/labkit';

<LabShell title="My Lab" header={<button>+ Add</button>}>
  <Workspace ids={['a', 'b']} resizable>
    <MyPane id="a" />
    <MyPane id="b" />
  </Workspace>
</LabShell>;

With reorderable, a tile is dragged by its <TileGrip>: wrap the tile's header in one, and commit the order onReorder reports. A <Trial>'s title bar is a grip already.

import { TileGrip } from '@weasel-js/labkit';

function MyPane({ id }: { id: string }) {
  return (
    <section>
      <TileGrip className="my-pane__bar">{id}</TileGrip>
      …
    </section>
  );
}

Every tile has an expand button in its corner that shows it large over the page; Escape, a click in the margin, or the close button puts it back. The tile is lifted into the browser's top layer rather than remounted, so a WebGL context inside it keeps running and only sees its box grow. A double-click opens it too where you ask for that — expandOnDoubleClick={(id) => id === 'output'} for an image tile. Turn the lightbox off with lightbox={false}, or per tile with lightbox={(id) => …}; the same behavior is available outside a workspace as <Lightbox>. In a <Lab> every trial has the toggle in its title bar, and its shared-surface pixels — annotation marks, a 3D view — come along.

<Toolbar>, <Sidebar>, <StatusBar>, <FpsMeter>, <ScaleIndicator>, <Legend>, <FloatingPanel>, <ZoomControl> and the @weasel-js/ui property rows re-exported from the root are all usable this way. <Trial> is not: it reads the lab store and only mounts inside a <Lab>.

Driving your own renderer

Cameras

canvas, stage and orbit each give the trial a camera, and a trial has at most one: canvas wins over stage, and orbit beside either throws. The camera lives in the trial's view, so it persists, Clone copies it and Reset returns it to initialView; the lab's zoom controls drive whichever one has the focus. zoom in a render context is that camera's magnification — a 2D view's zoom, or an orbit's opening distance over its distance now.

An orbit's view is kernel3d's Camera3d — { target, yaw, pitch, distance, fov, near, far }, plain numbers in radians and scene units. Whatever draws the scene reads it with useOrbitView(), and projects it with kernel3d's cameraViewProjection; chrome reads it with useTrialOrbit(). An instrument that draws 3D — through useOrbitView(), or a three.js renderer in its DOM — without declaring orbit has a camera labkit cannot move, and in development it warns and marks the trial's title bar. Declare orbit: { fixed: true } for a camera that holds still, or orbit: false when the instrument keeps a camera of its own:

orbit: {
  initialView: { distance: 14, pitch: 0.45 },
  limits: { minDistance: 2, maxDistance: 80 },
},

A drag turns it, Shift+drag pans its target, the wheel and a pinch dolly about the point under the pointer — as a 2D camera zooms about it — and a double click goes home. When the instrument draws in a pane rather than its whole body, it hands that pane to useOrbitView().viewportRef: gestures land there, and the dolly anchors there. An instrument running a dispatcher of its own over the pane — tools that select or draw in 3D — keeps the press for them with gestures: { drag: false, home: false, pinch: 'trackpad' } and moves the trial's camera from its own tools; examples/3d-lab does both. The loupe, the overview and the linked cursor are 2D, and stay out of an orbit trial.

The renderer is the instrument's: CanvasStack draws 2D layers, and for three.js or raw WebGL you take rects and dirtiness from labkit and keep the GL yourself:

import { toDeviceRect, useSurfaceTile, useTiledSurface } from '@weasel-js/labkit/surface';

A trial's view is persisted, restored on Reset and handed to the instrument; labkit reads into it only through the camera the instrument declares. See src/surface/AGENTS.md for the contract and the traps.

Subpath exports

The root barrel carries the common surface; a subpath reaches one slice directly. Several expose more than the root does, /state most of all.

| Subpath | | | --- | --- | | @weasel-js/labkit | Everything but /loupe and /overview | | /styles.css | The one stylesheet | | /primitives | Toolbar, Sidebar, StatusBar, Legend, FloatingPanel, meters | | /chrome | Regions, contribution types, built-in contributions | | /controls | <ControlPanel> and the config field types | | /canvas | <CanvasStack>, <Stage>, <Orbit>, <CameraInput>, coordinate helpers | | /layers | <LayerList> from @weasel-js/ui, and the layers capability types | | /loupe | <TrialLoupe>, the magnifier an instrument mounts in its content; the trial offers a Loupe toggle while one is mounted. Not in the root barrel | | /overview | <TrialOverview>, a floating panel showing a trial's whole content. Not in the root barrel | | /surface | Tiled surface hooks for your own renderer | | /job | useJob and the job capability types | | /state | The lab store, storage adapters (re-exported from @weasel-js/storage), useTrialState, serialization helpers | | /undo | Undo stack | | /dragdrop | <Palette>, <DragGhost>, useDragDrop | | /weasel-ui, /weasel-canvas | Passthroughs to @weasel-js/ui and @weasel-js/core |

Development

labkit is a workspace package in the weasel monorepo. From the repo root, npm install once, then from packages/labkit:

npm run dev            # Vite dev server on examples/minimal
npm run dev:annotate   # …on examples/annotate-lab (and dev:drag, dev:weasel, dev:schema)
npm test               # Vitest
npm run lint           # Biome, plus the class-prefix and design-token checks
npm run build          # dist/ for publish

The forge workshop runs at the repo root: npm run dev:forge from there covers every package's stories, labkit's included.

Documentation