Skip to content

sebs/banira

Repository files navigation

banira.js

CI Release GitHub release npm

!!! WARNING: This is a work in progress. Please use at your own risk.

Rationale and background in a blog post

banira.js is an open-source toolchain designed for the development of web components using vanilla JavaScript. It simplifies the process by eliminating the need for bundlers and frameworks, focusing instead on modern CSS and web standards.

banira ships as a single package: a library you import, and a banira command-line tool for compiling components and generating documentation.

Install

# Library + CLI
npm install banira

# Or use the CLI without installing
npx banira --help

Library

import { Compiler, ResultAnalyzer, DocGen, ManifestGenerator } from 'banira';

// Compile a component to browser-ready ES modules
// (uses Compiler.DEFAULT_COMPILER_OPTIONS unless you pass your own)
const compiler = new Compiler(['src/my-button.ts']);
const analyzer = new ResultAnalyzer(compiler.emit());
if (analyzer.diag().hasErrors) throw new Error('compilation failed');

// Generate an HTML documentation page (summary, @demo, and a full API reference)
const page = await new DocGen('my-button').generate('src/my-button.ts');

// Generate a Custom Elements Manifest, then derive artifacts from it
const manifest = new ManifestGenerator(['src/my-button.ts']).generate();
import { manifestToMarkdown, toTypeDefinitions } from 'banira';
const apiDocs = manifestToMarkdown(manifest);   // Markdown API tables
const dts = toTypeDefinitions(manifest);        // typed HTMLElementTagNameMap
Export Description
Compiler Uses tsc to compile TypeScript files to JavaScript
TestHelper Mount web components in JSDOM (including ones that import sibling modules) with deterministic readiness — the mount context exposes shadow-piercing query/queryAll — or, optionally, a real browser via Playwright (mountInBrowser)
DocGen Generates an HTML documentation page (summary, @demo, and a full API reference) for a component
ManifestGenerator Produces a Custom Elements Manifest (custom-elements.json) from vanilla components
manifestToMarkdown Renders a manifest as Markdown API documentation
toVsCodeHtmlData / toVsCodeCssData / toWebTypes Generate editor IntelliSense data (VS Code custom-data, JetBrains web-types) from a manifest
toTypeDefinitions Generates a .d.ts typing the custom elements from a manifest (string-literal union attributes become union types)
toStories / manifestToStories Generate Storybook CSF (*.stories.js) with argTypes from a manifest
validateManifest Structurally validates a manifest against the CEM 2.1.0 shape
validateManifestSchema Validates a manifest against the official CEM JSON Schema (requires the optional ajv dependency)
linkManifestField Point a package.json's customElements field at a generated manifest
checkReflection / checkSlots Smoke-test add-ons: attribute↔property reflection round-trip and @slot contract assertions
lintManifest Audit components against the Gold Standard Checklist + documentation coverage (banira lint)
diffManifests Diffs two manifests and suggests a semver release type
createPrerenderer / declarativeShadowDom SSR primitive: register components once, then renderToString(tag, { attributes, children }) to Declarative Shadow DOM
createEleventyPlugin Eleventy plugin that prerenders matching component tags to DSD at build time
hydrateShadow Client hydration helper: adopt a prerendered DSD shadow root (no flash) or render + adopt styles
parseDesignTokens / designTokensToCss Parse a W3C Design Tokens (DTCG) document and emit :root CSS custom properties (aliases resolved)
tokensToCssProperties / enrichManifestCssProperties Map imported tokens to manifest cssProperties, or backfill missing defaults/descriptions on matching component tokens
scaffoldTheme Generate a light/dark theme contract (theme.css), a <theme-toggle> component, and a demo page
buildImportMap / generateImportMap Scan components' bare imports and build an import map pinning each package to esm.sh (compile --import-map, serve --import-map)

CLI

banira <command> [options]

banira compile <files...>

Compile one or more TypeScript files with banira's compiler defaults.

Option Description
-p, --project <path> Path to a tsconfig.json whose options override the defaults
-o, --output <path> Directory to write the emitted JavaScript to
--import-map [path] Also emit an import map for the components' bare imports (see below)
--optimize-css Run inlined CSS through lightningcss (lower nesting, minify) before it's adopted
banira compile src/my-button.ts -o dist
# also write dist/import-map.json pinning bare imports to esm.sh
banira compile src/my-button.ts -o dist --import-map

--optimize-css needs the optional lightningcss dependency (npm i -D lightningcss), loaded lazily — a clear error is shown if it's requested but not installed.

Compilation emits a .js.map source map next to each .js, with the original TypeScript embedded, so breakpoints in devtools land on the .ts even when the source isn't served alongside the output. Because the map embeds your source, pass --no-source-map for production builds where you don't want to ship it.

With --import-map, banira walks the components' local module graph for bare imports (e.g. import { html } from 'lit'), pins each package to esm.sh at the version declared in package.json, and writes an import map (import-map.json by default; pass a path to override). This lets vanilla components use bare specifiers with no bundler — add the file's contents as a <script type="importmap"> before your module scripts. Unresolved bare imports are expected in this mode (they resolve from the CDN at runtime), so they don't fail the build.

banira doc <file>

Generate an HTML documentation page for a component. Combines the TSDoc summary and @demo blocks with a full API reference — attributes, properties, methods, events, slots, CSS parts and CSS custom properties — derived from the manifest. Writes to stdout unless -o is given.

Option Description
-o, --output <path> Write the page to a file instead of stdout
--script-src <path> Component module src used in the page (default ./dist/<tag>.js)
--stylesheet <value> A URL, a local .css file to inline, or none (default: PicoCSS CDN)

By default the page links PicoCSS from a CDN, so it needs network access to be styled. For a fully offline / self-contained page, pass a local .css file to --stylesheet — it is inlined into the page.

banira doc src/my-button.ts -o docs/my-button.html

# self-contained / offline page: inline a local stylesheet, point at a custom build
banira doc src/my-button.ts --stylesheet ./theme.css --script-src ./my-button.js

# no stylesheet at all
banira doc src/my-button.ts --stylesheet none

banira manifest <files...>

Generate a Custom Elements Manifest (custom-elements.json) — the ecosystem-standard descriptor that powers IDE autocomplete, Storybook controls and template type-checking. Writes to stdout unless -o is given.

banira manifest src/*.ts -o custom-elements.json

Attributes are read from observedAttributes, properties/methods from public class members, events from new CustomEvent(...), and slots / CSS parts / CSS custom properties from class jsdoc tags (@slot, @csspart, @cssprop, @fires). A class-level @role tag records the element's default ARIA role (typically set via ElementInternals.role). Members marked @deprecated carry the note through; @internal / @ignore members are omitted.

Option Description
-o, --output <path> Write the output to a file instead of stdout
--md Emit Markdown API documentation instead of JSON
--validate Validate the generated manifest and print a report (exit 1 on errors)
--link-package Point the nearest package.json's customElements field at the written manifest

--validate runs banira's fast structural checks. Install the optional ajv dependency (npm i -D ajv) to additionally validate the manifest against the official CEM JSON Schema for guaranteed spec-conformance, with precise path-level errors.

--link-package sets "customElements": "<path>" in the nearest package.json (the convention IDEs and Storybook use to auto-discover a package's manifest), preserving the file's indentation. It's a no-op when the field already points there.

# Markdown API tables for a README
banira manifest src/*.ts --md -o API.md
# write the manifest and link it from package.json
banira manifest src/*.ts -o custom-elements.json --link-package

banira editor-data <files...>

Generate editor IntelliSense data from the manifest so consumers get autocomplete and hover docs for your custom elements: VS Code HTML/CSS *.custom-data.json and a JetBrains web-types.json, all written to the output directory.

Option Description
-o, --out-dir <dir> Directory to write the data files to (default .)
banira editor-data src/*.ts -o .vscode

banira types <files...>

Generate a self-contained .d.ts from the manifest that augments HTMLElementTagNameMap (and, with --jsx, JSX.IntrinsicElements) so document.querySelector('my-el') and document.createElement('my-el') are typed for consumers — no runtime import required.

Option Description
-o, --output <path> Write the .d.ts to a file instead of stdout
--jsx Also augment JSX.IntrinsicElements
banira types src/*.ts -o dist/elements.d.ts

An attribute backed by a string-literal union property (size: 'sm' | 'md' | 'lg') is emitted as that union in the .d.ts (and as autocomplete values in the editor-data and options in the Storybook stories), instead of a bare string.

banira stories <files...>

Generate Storybook Component Story Format files (*.stories.js) — one per component — with an argTypes controls panel derived from each element's attributes (string-literal unions become select options) and events (mapped to actions), plus a default story. No hand-written story code required.

Option Description
-o, --out-dir <dir> Directory to write the stories to (default .)
--import-path <path> Module imported per story so the element registers (default ./{tag}.js)
banira stories src/*.ts -o stories

Pair with @storybook/web-components; in .storybook/preview.js, call setCustomElementsManifest(manifest) (from a banira manifest-generated custom-elements.json) for richer auto-docs.

banira diff <baseline> <current>

Compare two custom-elements.json files and report API changes with a suggested semver release type (removals/type changes → major, additions → minor, otherwise patch). Useful as a release gate.

Option Description
--json Emit the diff as JSON
banira diff old/custom-elements.json custom-elements.json

banira watch <files...>

Recompile components whenever their source changes. Same options as compile (-p, -o).

banira watch src/my-button.ts -o dist

banira serve [root]

Serve a directory over HTTP with live reload (changes under the root trigger a browser refresh). Pair it with watch for a compile-and-refresh dev loop. Binds 127.0.0.1 only, so the server is not reachable from the network unless you opt in with --host.

Option Description
-p, --port <number> Port to listen on (default 8080)
--host <host> Host/interface to bind (default 127.0.0.1; use 0.0.0.0 to expose on the network)
--ts Serve TypeScript transpiled on the fly (no separate compile step)
--hmr Hot-swap custom elements in place instead of full-page reload
--import-map Inject a <script type="importmap"> (esm.sh) for the served modules' bare imports

With --import-map, banira scans the served modules for bare imports, pins them to esm.sh (version from the served root's or the project's package.json), and injects the import map ahead of the first <script> in each served HTML page — so bare specifiers resolve in the browser with no bundler.

With --ts, a .ts request is served as an ES module and a request for foo.js falls back to a sibling foo.ts when no compiled foo.js exists — so you can point a page at ./my-button.js and skip the build during development. The served module carries an inline source map (with the original TypeScript embedded), so breakpoints resolve to the .ts in devtools.

# terminal 1: rebuild on change
banira watch src/my-button.ts -o demo/dist
# terminal 2: serve the demo with live reload
banira serve demo

banira dev <files...>

The one-command dev loop: watch and serve together, so a source edit recompiles and the browser refreshes. The served root defaults to the output directory.

Option Description
-p, --project <path> Path to a tsconfig.json whose options override the defaults
-o, --output <path> Directory to write compiled output to
-r, --root <path> Directory to serve (defaults to the output dir, or .)
--port <number> Port to listen on (default 8080)
--host <host> Host/interface to bind (default 127.0.0.1)
--ts Serve TypeScript transpiled on the fly (no separate compile step)
banira dev src/my-button.ts -o demo/dist -r demo

banira lint <files...>

Audit each component against a subset of the Gold Standard Checklist for Web Components plus documentation-coverage of its public surface. banira mounts each element in JSDOM and combines the shadow probe with manifest data to run independent, id'd rules: reflection (attributes reflect to/from their property), host-overridable (no !important on :host), and the doc-coverage rules undocumented-event / undocumented-attribute / undocumented-part / undocumented-slot (dispatched events, observed attributes, exposed part="…" elements, and rendered <slot>s that lack @fires / jsdoc / @csspart / @slot).

Option Description
--strict Treat warnings as errors and exit non-zero if any finding (for CI)
--rules <ids> Comma-separated rule ids to run (default: all)
--json Emit findings as JSON

Findings are warnings by default (exit 0); --strict makes them fail the build.

banira lint src/*.ts
banira lint src/*.ts --strict

banira test <files...>

Manifest-driven smoke test: for every custom element found in the sources, banira compiles its module, mounts it in JSDOM, and asserts the tag registers and upgrades to an HTMLElement — catching the most common breakages (a component that throws on construction, or never calls customElements.define) with no per-component test code. Exits non-zero if any element fails.

Option Description
--reflection Also round-trip each observed attribute ↔ its backing property and warn on either direction that doesn't reflect
--slots Also inject sample slotted content and warn on declared @slots with no matching <slot> (and shadow <slot>s with no @slot)

--reflection and --slots are advisory: their findings print as warnings and do not fail the command (registration failures still do), since not every attribute is meant to reflect.

banira test src/*.ts
banira test src/*.ts --reflection --slots

banira init <tag-name> [dir]

Scaffold a starter vanilla web component — a TypeScript source file (shadow DOM, an observed attribute/property, an event, and the @slot / @csspart / @cssprop / @fires jsdoc tags banira's manifest and doc tooling read) plus a demo HTML page wired for banira serve. Existing files are left untouched unless --force is given.

Option Description
--force Overwrite existing files
--form-associated Scaffold a form-associated element (static formAssociated = true + ElementInternals form/validation wiring)
--aria Scaffold an ARIA role/state-reflecting element (ElementInternals.role/ariaChecked, keyboard support; records the default role in the manifest via @role)
--hydrate Scaffold a component that hydrates a prerendered Declarative Shadow DOM root (adopt-or-render; no flash)
banira init my-button src
# a checkbox-role toggle that exposes its semantics via ElementInternals
banira init my-toggle src --aria
# a component that adopts its prerendered DSD root instead of re-rendering
banira init my-card src --hydrate

banira tokens-css <tokens.json>

Compile a W3C Design Tokens (DTCG) document into a :root CSS custom-property stylesheet. Groups become dashed name segments (color.primary--color-primary), $type is inherited from parent groups, and {alias} references are resolved. Writes to stdout unless -o is given; --selector overrides :root.

banira tokens-css design.tokens.json -o tokens.css

banira theme [dir]

Scaffold a theming starter: a theme.css light/dark contract (token sets via custom properties, switched by data-theme and prefers-color-scheme), a <theme-toggle> component that flips data-theme and persists the choice, and a demo page. With --tokens <file> the light :root set is seeded from a DTCG document. Existing files are left untouched unless --force is given.

Option Description
--force Overwrite existing files
--tag <tag-name> Tag name for the toggle component (default theme-toggle)
--tokens <tokens.json> Seed the light :root token set from a DTCG document
banira theme src/theme
banira theme src/theme --tokens design.tokens.json --tag color-scheme-switch

banira prerender <files...>

Render the components to static HTML using Declarative Shadow DOM (<template shadowrootmode="open">), so they display — shadow DOM and all — before any JavaScript runs. Components are mounted in JSDOM and their shadow root serialized. Writes to stdout unless -o is given.

A component's adopted constructable stylesheet is inlined into the DSD template as <style data-banira-critical> (constructable sheets aren't serialized otherwise), so the prerendered markup is styled before JS — FOUC-free. On hydration, hydrateShadow adopts the (deduped) sheet and drops that inline style. Pass { inlineStyles: false } to opt out.

banira prerender src/my-button.ts -o prerendered.html

For programmatic SSR, createPrerenderer(files) compiles and registers the components once and returns renderToString(tag, { attributes, children }), which meta-frameworks (Enhance/WebC/11ty/Rocket) can call to serialize a component to Declarative Shadow DOM on demand:

import { createPrerenderer } from 'banira';
const r = await createPrerenderer(['src/my-circle.ts']);
const html = await r.renderToString('my-circle', { attributes: { size: '40' }, children: 'Caption' });
r.close();

The client half is hydrateShadow(host, { template, styles }): in a component's connectedCallback it adopts a prerendered DSD shadow root as-is (no re-render, no flash) when present, or creates one and renders template otherwise — adopting the constructable stylesheet either way (DSD markup ships without it). banira init --hydrate scaffolds a component using this pattern.

Eleventy plugin

createEleventyPlugin({ files }) wires that renderer into an Eleventy build (the role WebC plays for 11ty): it adds a transform that rewrites matching custom-element tags in the generated HTML into Declarative Shadow DOM, preserving attributes and slotted children.

// .eleventy.js
import { createEleventyPlugin } from 'banira';
export default function (eleventyConfig) {
  eleventyConfig.addPlugin(createEleventyPlugin({ files: ['src/my-circle.ts'] }));
}

Development

Build, test and release instructions live in DEVELOPMENT.md.

Examples

  • my-circle — minimal hand-written component

License

MIT © Sebastian Schürmann

About

vanilla js toolkit for node and typescript

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors