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

@beforesemicolon/site-builder

v1.14.1

Published

JSON and Markdown static site builder

Readme

BeforeSemicolon Site Builder

npm version License: BSD-3-Clause Build Status

JSON-first component runtime and static site builder with a Markdown documentation adapter.

Pages and reusable components are complete JSON definitions. Components can own their content, JSON CSS, external stylesheets, and scripts. The page compiler collects dependencies from rendered components, deduplicates them, emits them in the document head, and extends the page CSP for external script and stylesheet origins. It also detects external resources in rendered templates and component CSS and adds each exact origin to the narrow matching CSP directive. Generated inline style and script blocks are authorized with exact SHA-256 hashes instead of unsafe CSP keywords. Static template style attributes are compiled into generated CSS rules before output, preserving CSP-safe page documents and standalone component fragments.

Installation

Requires Node.js 22.13.0 or later and npm 10 or later. Both ESM and CommonJS entrypoints are supported.

npm install @beforesemicolon/site-builder

Build a Project

import { buildProject } from '@beforesemicolon/site-builder'

await buildProject({
    srcDir: process.cwd(),
    publicDir: 'public',
    prod: true,
})

Page data sources

Page and route data can load project-local Markdown or JSON files at build time. Source paths are relative to the project root and may use the effective page locale:

{
    "data": {
        "post": {
            "$source": "blogs/example.{locale}.md"
        }
    }
}

Locale-aware sources try the exact locale, its base locale, and then the configured default locale. Route data overrides page data, allowing many routes to reuse one page with different sources. Resolved values remain available through existing expressions such as {{data.post.metadata.title}}.

Markdown sources expose front matter as metadata, the Markdown body as raw, the selected locale and sourcePath, and sanitized content for bfs-html. Fenced code is syntax-highlighted while compiling; no browser highlighter is required. The builder also calculates metadata.readingMinutes from the localized Markdown prose and code. Templates can render it directly:

{
    "children": ["{{data.post.metadata.readingMinutes}}", " min read"]
}

Compiler-generated readingMinutes overrides any same-named front-matter field. JSON sources resolve to their parsed value and may contain nested $source descriptors. This makes a list index capable of loading the same Markdown metadata used by an article page without copying derived values:

{
    "posts": [
        {
            "href": "/blog/example",
            "article": {
                "$source": "blogs/example.{locale}.md"
            }
        }
    ]
}

Literal data continues to work unchanged. Circular nested sources produce a build diagnostic.

A source may define a build-time $mapper. The mapper is a keyed output object, so components continue to receive ordinary page data without knowing the source's original shape:

{
    "data": {
        "faq": {
            "$source": "assets/services/business-faq.json",
            "$mapper": {
                "groups": {
                    "$map": "categories",
                    "as": "category",
                    "to": {
                        "id": "{{category.id}}",
                        "title": "{{category.name}}",
                        "items": {
                            "$filter": "faqs",
                            "where": "{{item.categoryId === category.id}}",
                            "$map": {
                                "question": "{{item.question}}",
                                "answer": "{{item.answer}}"
                            }
                        }
                    }
                }
            }
        }
    }
}

Mapper strings resolve source paths with {{path.to.value}}. $map projects an array through to and may name its current value with as. $filter selects array values with a boolean path or strict ===/!== comparison, then projects each match through $map. Mapping is completed before static page rendering, and the result replaces the source descriptor at its existing data key (for example, data.faq). Mapper expressions never execute JavaScript.

Build-time expression utilities keep deterministic presentation out of browser scripts. date.long, date.medium, and date.short format values with the compile locale and UTC; url.encode prepares values for static URLs. The build year is available as {{build.year}}:

{
    "name": "time",
    "attributes": { "datetime": "{{data.post.metadata.createdAt}}" },
    "children": ["{{data.post.metadata.createdAt | date.long}}"]
}

Pages can provide arbitrary resolved metadata without a post-build HTML pass:

{
    "metadata": {
        "meta": [
            {
                "name": "author",
                "content": "{{data.post.metadata.author.name}}"
            },
            {
                "property": "article:published_time",
                "content": "{{data.post.metadata.createdAt}}"
            }
        ]
    }
}

buildProject discovers the project, validates definitions, compiles all targets, copies public assets, and writes the generated site to publicDir. It generates an llms.txt page index from routed page titles, descriptions, optional summaries, source paths, and canonical URLs. Add assets/llms.txt to provide a custom index. Package-owned assets/llms-full.txt content is copied to the public root when present.

Build Markdown Documentation

buildDocs turns Markdown files into the same locale-aware static-site output used by buildProject:

import { buildDocs } from '@beforesemicolon/site-builder'

await buildDocs()

Configure the documentation source alongside the shared site and locale fields in site.config.json:

{
    "schemaVersion": "1.0",
    "mode": "static",
    "siteUrl": "https://docs.example.com",
    "defaultLocale": "en",
    "locales": ["en", "pt"],
    "docs": {
        "sourceDir": "docs",
        "outputDir": "website",
        "template": "fading-citrus"
    },
    "deployment": {
        "targets": {
            "netlify": true,
            "cloudflareWorkers": {
                "name": "example-docs",
                "compatibilityDate": "2026-08-11"
            }
        }
    }
}

Use document for head configuration shared by every JSON and Markdown page:

{
    "document": {
        "metadata": { "robots": "index, follow" },
        "analytics": {
            "providers": [
                {
                    "type": "google-analytics",
                    "tagId": "G-EXAMPLE",
                    "enabled": true,
                    "config": { "send_page_view": true }
                }
            ]
        }
    }
}

document accepts the document-producing subset of a page definition: metadata, links, scripts, styles, csp, and analytics. Shared links, scripts, and styles are emitted before page entries. Page metadata, CSP directives, and analytics override the corresponding shared values. JSON extends remains the page-specific inheritance mechanism; document is the site-wide baseline.

Each page has one canonical Markdown source. Put translatable copy in the locale catalogs and reference it with {{t.*}} placeholders in front matter, prose, headings, and Markdown layout options:

docs/documentation/get-started.md
docs/locale/en.json
docs/locale/pt.json
---
title: '{{t.pages.getStarted.title}}'
description: '{{t.pages.getStarted.description}}'
layout: default
---

# {{t.pages.getStarted.heading}}

{{t.pages.getStarted.introduction}}

Catalog values inserted into prose may contain Markdown. Translation placeholders inside fenced or inline code are preserved as code. A missing non-default translation falls back to the default locale with a warning; a missing default translation is a production build error.

Put reusable navigation, action, label, and footer copy under common and reference it as {{t.common.*}}. Keep only page-specific copy under pages so the same translation is not repeated across Markdown files.

Page identity comes from front matter id, or from the Markdown path without .md. Catalog _routes entries use that identity to translate public routes.

The documentation build generates a concise llms.txt index and a complete llms-full.txt context file for every locale. The full file preserves code fences and uses the package's resolved Markdown documentation as its source of truth. Add docs/llms.txt or docs/llms-full.txt to replace either generated artifact with package-authored content. Both buildDocs and buildProject share locale target expansion, catalog fallback, interpolation, static locale resources, and deployment negotiation.

Authored AI files may use {{project.name}}, {{project.version}}, and the same {{t.*}} locale templates as Markdown pages. Site Builder resolves them for each locale without rewriting the package's remaining content.

Project Structure

assets/
components/
forms/
locale/
modals/
pages/
router/
scripts/
services/
state-stores/
stylesheets/
themes/
utils/
site.config.json

Files are the registry. Locale messages belong in locale/. Authored browser scripts belong in scripts/, which buildProject copies verbatim to every generated locale root. Keep Node.js authoring, migration, validation, and other build tools outside scripts/ so they are not deployed as browser assets.

Analytics services

An analytics service provides a provider-neutral website analytics interface. It exposes a public configuration object plus the standard config, page, track, identify, and consent actions. The service emits generic browser events so a website-owned analytics integration can supply its provider-specific behavior without adding provider logic to Site Builder.

Use id as the stable service reference in state-store actions, such as services.google-analytics.track. name is an optional display label. When an id is omitted, discovery continues to use name, then the service filename, for compatibility with existing definitions.

Services can declare a csp object alongside their configuration. At build time, Site Builder merges those directives into a page only when that page reaches the service through one of its state stores. This keeps each page's CSP limited to the providers it actually uses.

{
    "id": "website-analytics",
    "name": "Website Analytics",
    "source": "analytics",
    "config": {
        "siteId": "public-site-identifier"
    },
    "csp": {
        "connect-src": ["https://analytics-provider.example"]
    },
    "methods": {
        "config": {},
        "page": {},
        "track": {},
        "identify": {},
        "consent": {}
    }
}

Analytics services can be started and stopped with their lifecycle methods. Each non-configuration action is dispatched as a bfs:analytics browser event; website-owned integration code is responsible for consuming it.

Declare page-specific scripts on the page that uses them:

{
    "id": "blog-post",
    "type": "page",
    "scripts": [
        {
            "src": "/scripts/blog-post.js",
            "defer": true
        }
    ]
}

Script descriptors emit script tags, participate in dependency deduplication, and inform CSP generation. They do not bundle npm packages or transform JavaScript imports. Bundle npm dependencies into a browser-compatible file in scripts/ before calling buildProject, or reference an external browser-compatible URL. Put a script on a component only when the component owns that behavior and must load it wherever the component is rendered; keep page-only orchestration on the page definition.

Name scripts for the behavior they implement, not the page where they first appeared. For example, a script that only submits contact-form should be named contact-form-submission.js and declared by that form component. A page name such as contact-page.js implies broader page ownership and makes reuse and dependency auditing harder.

Themes

Theme files live in themes/ and expose nested design tokens as CSS custom properties. See the complete theme reference for every standard token and a copyable full JSON example. Select a theme with defaultTheme in site.config.json:

{
    "schemaVersion": "1.0",
    "mode": "static",
    "defaultTheme": "brand",
    "themes": ["brand"]
}

defaultTheme is the one theme compiled into the project. The optional themes array exposes theme names to runtime context; it does not merge or compile additional theme files. A single theme can define both light and dark values.

New themes can start with the standard token namespaces while adding arbitrary palette entries, component values, and project-specific values:

{
    "schemaVersion": "1.0",
    "name": "brand",
    "tokens": {
        "color": {
            "background": {
                "default": "#ffffff",
                "dark": "#111111"
            },
            "foreground": {
                "default": "#172033",
                "dark": "#f7f9fc"
            },
            "primary": {
                "default": "#165ef0",
                "light": "#165ef0",
                "dark": "#8ab4ff"
            },
            "primaryForeground": "#ffffff",
            "success": {
                "default": "#16803c",
                "dark": "#4ade80"
            },
            "brand": {
                "50": "#eef4ff",
                "500": "#165ef0",
                "900": "#102a56"
            }
        },
        "typography": {
            "h1": {
                "fontFamily": "Inter, system-ui, sans-serif",
                "fontSize": "1.25rem",
                "fontWeight": 700,
                "lineHeight": 1.2,
                "letterSpacing": "-0.025em",
                "color": "var(--color-foreground)"
            },
            "paragraph": {
                "fontFamily": "Inter, system-ui, sans-serif",
                "fontSize": "1rem",
                "lineHeight": 1.625,
                "color": "var(--color-text)"
            }
        },
        "spacing": {
            "sm": "0.5rem",
            "md": "0.75rem",
            "lg": "1rem"
        },
        "radius": {
            "sm": "0.25rem",
            "md": "0.5rem",
            "full": "9999px"
        },
        "shadow": {
            "md": "0 10px 15px -3px rgb(0 0 0 / 0.1)"
        },
        "motion": {
            "duration": { "fast": "150ms", "normal": "250ms" },
            "easing": { "standard": "cubic-bezier(0.2, 0, 0, 1)" }
        },
        "component": {
            "button": { "height": "2.5rem" }
        },
        "custom": {
            "brand": { "logoSize": "2rem" }
        }
    }
}

A scalar value applies in both modes. A mode-aware value requires default and at least one of light or dark; the missing mode falls back to default. The selected theme automatically emits light and dark prefers-color-scheme rules plus explicit [data-theme="light"] and [data-theme="dark"] overrides.

Token paths are camel-case normalized and joined with hyphens:

| Token path | CSS custom property | | ---------------------------- | ------------------------------- | | color.primaryForeground | --color-primary-foreground | | color.foreground.DEFAULT | --color-foreground | | color.foreground.100 | --color-foreground-100 | | typography.body.fontFamily | --typography-body-font-family | | typography.h1.fontSize | --typography-h1-font-size | | motion.duration.fast | --motion-duration-fast | | component.button.height | --component-button-height | | custom.brand.logoSize | --custom-brand-logo-size |

The standard namespaces are color, typography, spacing, size, breakpoint, container, borderWidth, borderStyle, radius, shadow, insetShadow, dropShadow, opacity, blur, perspective, aspectRatio, zIndex, gradient, motion, component, and custom. color supplies semantic roles for surfaces, actions, focus, links, selection, code, and success/info/warning/danger feedback. Every group accepts additional nested values. Within color, an uppercase DEFAULT base plus positive integer keys creates an unsuffixed semantic property and an ordered scale; for example, color.foreground.DEFAULT and color.foreground.100 emit --color-foreground and --color-foreground-100.

The built-in light, dark, and adaptive system themes include a complete starter set. A TypeScript-authored adaptive theme can reuse the same values with createDefaultThemeTokens('system'):

import {
    createDefaultThemeTokens,
    type ThemeDefinitionV1,
} from '@beforesemicolon/site-builder'

const defaults = createDefaultThemeTokens('system')

export const theme: ThemeDefinitionV1 = {
    schemaVersion: '1.0',
    name: 'brand',
    tokens: {
        ...defaults,
        color: {
            ...defaults.color,
            primary: {
                default: '#0f766e',
                dark: '#5eead4',
            },
        },
        custom: {
            chart: { positive: '#16a34a' },
        },
    },
}

Existing v1 themes remain valid: arbitrary top-level groups, scalar values, and the legacy theme-level mode continue to compile with the same variable names and selectors when no value-level modes are present. New themes should omit theme-level mode. A nested object is treated as a mode value only when its keys are limited to default, light, and dark, and it contains the required scalar default; other objects remain ordinary token groups. Arrays and null are retained for input compatibility but do not emit declarations.

breakpoint variables are useful as shared references and from JavaScript, but native CSS custom properties cannot be substituted into media-query conditions. Author media queries with literal conditions or a CSS build step.

Forms, Modals, and Flashbars

See the forms guide for the complete form envelope, form-control composition, validation, conditional rendering, submission, script/style ownership, and page-scoping model.

Definitions in forms/ and modals/ use the same component envelope with kind: "form" or kind: "modal"; those folders are also their build-time registries. Render a form with FormRenderer; open a modal through a page handler. Components emit the page handler name and do not need to import or know about a modal controller:

{
    "handlers": {
        "openQuote": [
            {
                "use": "modal.open",
                "with": {
                    "modal": "request-quote",
                    "data": { "plan": "{{event.detail.plan}}" }
                }
            }
        ]
    },
    "content": [
        {
            "name": "FormRenderer",
            "id": "primary-contact",
            "attributes": {
                "form": "contact",
                "values": { "source": "pricing" }
            }
        },
        {
            "name": "PricingCard",
            "events": { "quote": "openQuote" }
        }
    ]
}

Forms support native server submission, emitted data, or action pipelines; field rules, conditional visibility/enabled/required state, computed output, localized labels, file limits, error summaries, and feedback are declarative. Related fields can be repeated through template repeat; the older additive groups registry remains readable during project migration. Form-control components can compose the form template and own their styles, scripts, props, events, and validation-facing native controls.

A form-specific submission adapter belongs on the form envelope, even when a page or layout places the form through FormRenderer:

{
    "schemaVersion": "1.1",
    "name": "contact-form",
    "kind": "form",
    "category": "form",
    "scripts": [
        {
            "src": "https://cdn.example.com/form-api.js",
            "defer": true
        },
        {
            "src": "/scripts/contact-form-submission.js",
            "defer": true
        }
    ],
    "content": {
        "fields": [],
        "submit": {
            "mode": "emit",
            "event": "contact-form-submit"
        }
    }
}

The descriptor order is preserved, so a third-party browser dependency may be listed before its adapter. Direct form placement and FormRenderer both carry the form's scripts and styles into the page dependency collector. Identical URLs are emitted once, and pages that cannot reach the form receive none of its resources.

Modal requests accept runtime data and resolve to either a resolved or dismissed outcome. globalThis.BFS.openModal(id, data) exposes the same promise controller for authorized page scripts.

Flashbars are page actions with optional header, description, action, type, group, position, priority, and auto-close duration. Page ui.flashbars.groups configures stacking and collapsing. A flashbar action may open a modal; the builder follows that dependency automatically.

The renderer computes a feature manifest per page. Reachable components, forms, actions, modals, triggers, and flashbars are recorded. Related browser code is emitted only when the page's reachable JSON uses it. Unreferenced files do not add HTML, CSS, JavaScript, or default UI-store artifacts. Dynamic modal names must declare their finite allowlist in page.capabilities.modals.

Locale Builds

Locale builds are opt-in. With neither defaultLocale nor a non-empty locales array in site.config.json, the existing output layout is preserved and the effective document language is page.lang ?? "en".

Declaring either field builds the complete site once per locale:

{
    "schemaVersion": "1.0",
    "mode": "static",
    "siteUrl": "https://example.com",
    "defaultLocale": "en",
    "locales": ["en", "pt"]
}
public/
  _redirects
  en/
    index.html
    about.html
    components/
    scripts/
    stylesheets/
    assets/
    llms.txt
    llms-full.txt
    robots.txt
    sitemap.xml
    data/locale.json
  pt/
    index.html
    sobre.html
    components/
    scripts/
    stylesheets/
    assets/
    llms.txt
    llms-full.txt
    robots.txt
    sitemap.xml
    data/locale.json

The compile locale is passed explicitly through compiledLocale; document language resolution is compiledLocale ?? page.lang ?? "en". The builder adds portable URL rewrites to the root _redirects file. Browser language and cookies do not select content: each public URL always resolves to one compiled locale. Authored _redirects rules remain before the generated rules, so specific project redirects can override the defaults.

Static deployment targets

Configure Netlify, Cloudflare Workers Static Assets, or both under the shared deployment field:

{
    "deployment": {
        "targets": {
            "netlify": true,
            "cloudflareWorkers": {
                "name": "example-static-site",
                "compatibilityDate": "2026-08-11",
                "notFoundHandling": "404-page",
                "htmlHandling": "none",
                "previewUrls": true
            }
        },
        "headers": [
            {
                "path": "/assets/*",
                "values": {
                    "Access-Control-Allow-Origin": "https://cms.example.com"
                }
            }
        ]
    }
}

Each target is independent. Set netlify to false for Cloudflare-only output, keep it true during a parallel proof of concept, or omit cloudflareWorkers to preserve Netlify-only behavior. The deprecated docs.generatedFiles.netlify flag remains a compatibility alias for docs projects; new configuration should use deployment.targets.netlify.

Cloudflare Worker names use lowercase letters, numbers, and interior dashes, with a maximum of 63 characters. compatibilityDate must be a real calendar date in YYYY-MM-DD format. A production build reports invalid values before deployment.

The generated output uses files understood by both static-asset platforms:

public/
  _redirects
  _headers
  .assetsignore
  wrangler.jsonc
  netlify.toml

_redirects and _headers are portable. wrangler.jsonc is generated only for a Cloudflare target and configures an assets-only Worker whose asset directory is the generated output itself; no Worker script or main entry is needed. .assetsignore keeps the Wrangler and Netlify metadata out of the uploaded asset namespace while preserving project-authored exclusions. netlify.toml is copied or generated only when Netlify is enabled.

The generated route rules proxy to exact .html files, so Cloudflare htmlHandling is fixed to none. This preserves the public route instead of letting Cloudflare canonicalize an internal locale path into a visible 307 redirect. Locale builds also expand configured public header paths to their internal locale-storage variants so the same rule is applied by both hosts.

The build creates deployment artifacts but does not authenticate, create a Cloudflare project, change DNS, or publish. Inspect locally with:

npx wrangler dev --config public/wrangler.jsonc --persist-to .wrangler-state
npx netlify dev --dir public

Keep .wrangler-state out of source control. Supplying a persistence directory outside public/ also prevents Wrangler's local state files from triggering the static-asset watcher.

When the output has been verified, an external deploy workflow can publish it with npx wrangler deploy --config public/wrangler.jsonc. Keeping deploy execution outside the builder lets the CMS use the same build result while choosing Netlify, Cloudflare, or both per project.

Redirect compatibility

When locale builds are enabled, pages exist below locale directories such as public/en/index.html; the builder does not emit a duplicate public/index.html. Do not keep redirects from a non-localized deployment that target root page files:

# Remove these when enabling locale builds.
/          /index.html  200
/about     /about.html  200
/*         /index.html  200

Keep only project-specific exceptions in the source _redirects file. They will be copied ahead of the generated locale rewrites:

# Project-specific redirects must precede the locale rewrites generated by
# @beforesemicolon/site-builder.
/admin    https://cms.example.com    301

Keep portable header rules in deployment.headers or a source _headers file. Do not duplicate page or catch-all redirects in netlify.toml. Cloudflare does not support Netlify's forced status suffix (200! or 301!), domain-level source redirects, source query matching, or country, language, and cookie conditions in _redirects. When Cloudflare is enabled, a production build fails with a targeted diagnostic if an authored rule uses those forms.

After buildProject or buildDocs, the emitted public/_redirects contains the authored exceptions followed by internal 200 rewrites from every public route to its file below public/[locale]/. It contains no generated cross-locale redirects or request-language conditions. Edit the source _redirects, not the generated copy in public/.

If multiple locales declare the same route, the default locale keeps the clean path and non-default locales receive an unambiguous locale-prefixed path. For example, a shared /blog route becomes /blog for English and /pt/blog for Portuguese. A translated route such as /sobre remains unprefixed.

Set siteUrl to the public origin used for canonical URLs, alternate-language links, sitemaps, and locale-specific robots.txt sitemap references. Every localized document receives its own canonical URL plus hreflang links for all configured locales and x-default. Standalone component artifacts are compiled with the same selected catalog as their locale root.

Each locale root also receives data/locale.json as an optional static locale resource. Locale messages used by HTML are resolved during compilation. If an authored browser interaction needs localized copy, render only those values into its component attributes instead of shipping the complete catalog globally.

Locale catalogs may define translated public routes by stable route ID:

{
    "_routes": {
        "about": "/sobre",
        "contact": "/contacto"
    }
}

Missing entries fall back to the route definition's path. Templates should use the injected route map instead of hardcoded internal paths, including inside nested attribute data:

{
    "name": "a",
    "attributes": {
        "href": "{{routes.about}}"
    },
    "children": ["About"]
}

Components

Every component has a required name. kind, category, and runtime default to custom, layout, and static. Kinds describe renderer semantics; categories are open classification metadata for libraries and authoring tools. The built-in kinds are custom, form, form-control, and modal, but a project can register another kind parser.

Props use the shared data-type model. A finite choice uses a first-class enum so renderers and visual editors can expose the valid options:

{
    "type": {
        "type": "enum",
        "options": ["button", "submit", "reset"],
        "default": "button"
    }
}

Static components render their content without an invented wrapper. Client components get one configurable host and server-render their content inside it. Web Components compile to @beforesemicolon/web-component; prefix plus name produces the tag (bfs + Button becomes bfs-button). Only pages that render a Web Component receive its module tag. Generated modules share one bundled browser runtime instead of duplicating the library.

{
    "schemaVersion": "1.1",
    "name": "Button",
    "description": "Submits a form or triggers an action.",
    "icon": "/assets/icons/button.svg",
    "prefix": "bfs",
    "aliases": ["ActionButton"],
    "kind": "form-control",
    "category": "form",
    "runtime": "web-component",
    "config": {
        "role": "button",
        "webComponent": {
            "shadow": { "mode": "open", "delegatesFocus": true },
            "formAssociated": true
        },
        "formControl": { "valueProp": "value" }
    },
    "props": {
        "label": { "type": "string", "default": "Submit" },
        "value": { "type": "string", "default": "" }
    },
    "state": {
        "busy": { "type": "boolean", "default": false }
    },
    "slots": {
        "default": {
            "description": "Button label",
            "multiple": true,
            "fallback": [{ "name": "span", "children": ["{{props.label}}"] }]
        }
    },
    "events": {
        "activate": { "type": "CustomEvent", "bubbles": true, "composed": true }
    },
    "handlers": {
        "activate": [
            {
                "use": "emit",
                "with": { "name": "activate", "detail": "{{input}}" }
            }
        ]
    },
    "listeners": {
        "keydown": {
            "handler": "activate",
            "options": { "capture": true }
        }
    },
    "lifecycle": {
        "mount": [{ "use": "debug.log", "with": "mounted" }],
        "destroy": [{ "use": "debug.log", "with": "destroyed" }],
        "formReset": [
            { "use": "component.state.set", "path": "busy", "value": false }
        ]
    },
    "styles": [
        {
            ":host": { "display": "inline-block" },
            "button": {
                "display": "inline-flex",
                "align-items": "center"
            }
        }
    ],
    "dependencies": { "components": ["Icon"] },
    "content": [
        {
            "name": "button",
            "attributes": { "type": "button", "value": "{{props.value}}" },
            "events": { "click": "activate" },
            "children": [{ "name": "slot" }]
        }
    ]
}

Lifecycle callbacks are available only for the web-component target: mount, update, destroy, adopt, error, formAssociated, formDisabled, formReset, and formStateRestore. Form callbacks require config.webComponent.formAssociated: true. Native listeners are separate from emitted custom-event definitions. JSON CSS supports declarations, nesting, conditional rules, keyframes, descriptor rules, and repeated values.

content remains kind-owned and open. Custom and form-control components use template-node arrays. Form and modal components use their specialized content objects and automatically receive the semantic <form> or <dialog> base. Open config namespaces allow libraries to add metadata without changing the builder schema. config.host supports a client host tag and default attributes; config.webComponent.importSource can override the generated shared runtime import when a project supplies its own module.

Parse APIs

import { parseComponent, parsePage } from '@beforesemicolon/site-builder'

parsePage renders a complete page document.

parseComponent accepts output: 'both' | 'document' | 'fragment' and defaults to both. Document output places dependencies in the document head. Fragment output emits dependency tags immediately before the rendered component.

Both parsers can lazily load locale catalogs alongside their other resources. The hook returns parsed catalog JSON and automatically loads the default, base, and exact locale fallback chain before rendering:

await parsePage(page, {
    graph,
    locale: 'pt-BR',
    hooks: {
        fetchLocale: async (locale) => {
            const response = await fetch(`/locale/${locale}.json`)
            return response.ok ? response.json() : null
        },
    },
})

buildProject and buildDocs accept the same hooks.fetchLocale option. Catalogs already discovered from locale/ or docs/locale/ take precedence; the hook only fetches missing catalogs and each fetched catalog is cached in the project graph.

Canonical subpath imports are also available:

import { parsePage } from '@beforesemicolon/site-builder/parse-page'
import { parseComponent } from '@beforesemicolon/site-builder/parse-component'

Browser Bundle

The browser build exposes the promoted parser API without a version namespace:

<script src="https://unpkg.com/@beforesemicolon/site-builder/dist/client.js"></script>
<script>
    const { parsePage, parseComponent } = window.BFS.SITE_BUILDER
</script>

Runtime Specification

See docs/spec.md for page and component definitions, data flow, compile targets, dependency handling, and CSP behavior.

Development

Use Node.js ^22.13.0 or >=24 for development (required by ESLint 10), with npm 10 or later. The published package requires Node.js 22.13.0 or later.

After building, run npm run test:package to verify a clean, engine-strict production install, public ESM/CommonJS exports, and browser CSP hashes. CI runs these checks on the exact Node.js 22.13.0 minimum and current Node 22/24.

npm ci
npm run test
npm run test:watch
npm run lint
npm run build

License

BSD-3-Clause. See LICENSE.