Packages
Gyral is a set of small packages under the @gyral scope on npm, plus create-gyral, which
sets up a new app. Every app needs @gyral/core; add the others when you need them. All ten
packages are released together with the same version number (0.3.9), are ES modules with
TypeScript types, and are published from GitHub Actions with
npm provenance.
Core
| Package | What it gives you |
|---|---|
@gyral/core |
define(), intents and on(), update and commands, subscriptions, stores, forms, subscription(), and the view layer: html, svg, css, each, keyed, raw, hooks. /vite has gyral(), the Vite plugins (template compiler, component loading, element typings), /eslint the lint rules. |
npm install @gyral/core@gyral/core/storage and @gyral/core/indexeddb are optional entry points in the same
package. They provide separate native localStorage and IndexedDB drivers.
Apps that do not import them carry no storage driver code.
Optional packages
| Package | What it gives you |
|---|---|
@gyral/http |
The http driver: get, request, submitForm, typed errors, schema decoding. @gyral/http/testing has fakeHttp. |
@gyral/router |
Typed route tables, listen, navigate, setHead, browser and memory history. |
@gyral/time |
delay, debounce and every as commands; an app that only delays doesn't bundle every. |
@gyral/ssr |
Server rendering: renderPage, renderToString, contentSecurityPolicy, styleHashes, formAction. @gyral/ssr/static prerenders and serves builds and writes component style hashes, @gyral/ssr/node mounts a fetch handler on Node's http and runs the development server. |
@gyral/testing |
step, run, viewOf, subscriptionsFor, outputsIn, mount, parse, fake drivers, virtual time, mountSsr and hydrated. @gyral/testing/vitest renders server markup for browser tests. |
@gyral/devtools |
The in-page devtools panel, for development builds. |
@gyral/mcp |
An MCP server for coding agents: it searches these docs, looks up exact API signatures, reads the examples, scaffolds components and typechecks snippets against your project. Hosted at https://mcp.gyral.dev/mcp. |
@gyral/codemod |
Rewrites an app's source for a release's API changes, such as 0.3.8's with npx @gyral/codemod 0.3.8, and lists what it leaves for you. Run it with npx; there is nothing to install. |
npm install @gyral/http @gyral/router @gyral/time
npm install @gyral/ssr
npm install -D @gyral/testing @gyral/devtoolsThe same @gyral/http installation includes optional @gyral/http/sse and
@gyral/http/websocket entries for native network streams.
The fetch entry does not include those transports.
@gyral/mcp is a stdio server your agent starts with npx -y @gyral/mcp, from your project
directory. It answers from the docs, API and examples of its own version, bundled at build time,
so it works offline. gyral-mcp http runs a read-only Streamable HTTP server instead; Gyral hosts one at
https://mcp.gyral.dev/mcp, with no token. Setup for Claude Code,
VS Code, Cursor, Codex and other clients is in
Use Gyral with coding agents.
create-gyral
npm create gyral@latest asks for a directory and a template, basic (client-rendered) or
ssr (server-rendered and hydrated), and writes an app that depends on the matching @gyral/*
release. Getting started walks through it.
Optional peer dependencies
No Gyral package asks you to install another library at runtime. The build tools Gyral plugs into are optional peer dependencies: install the ones you use.
| Package | Optional peers |
|---|---|
@gyral/core |
vite ^8 (preset and compiler), eslint 9 or 10 (the plugin), parse5 (compiler check) |
@gyral/ssr |
vite ^8 (the development server in @gyral/ssr/node) |
@gyral/testing |
vitest ^4.1 or ^5 (only for @gyral/testing/vitest) |
@gyral/mcp |
typescript 5 or 6 (for check_snippet, which uses your project's compiler) |
Validation in @gyral/core, @gyral/http and @gyral/ssr accepts any
Standard Schema library; Gyral depends only on its types.
Any custom element works next to Gyral components, including ones built with Lit; install that library yourself if you use it. Upgrading an existing app? See Versioning and stability.
One version of every package
@gyral/http, @gyral/router, @gyral/time, @gyral/ssr, @gyral/testing and
@gyral/devtools take @gyral/core as a peer dependency at their own exact version, so they
always use your app's copy of core: give every @gyral/* package the same version. Your
package manager reports a mismatch when it installs (pnpm lists the unmet peer, npm stops with
ERESOLVE), and a package that loads with another release of core throws
G0016 in development, naming both versions.
The Vite plugins
gyral() from @gyral/core/vite returns the Vite plugins every Gyral app needs. Put it in the
config's plugins, beside any plugins of your own (plugins: [gyral(), mine()]); its settings
come through the plugins themselves, so there is nothing to spread or merge:
// vite.config.ts
import { defineConfig } from 'vite';
import { gyral } from '@gyral/core/vite';
export default defineConfig({
plugins: [gyral()],
});- The template compiler. In
vite buildit checks everyhtmlandsvgtemplate, dependencies included, fails the build with a code frame on a rule violation, and replaces each template with a precompiled object, so the bundle doesn't carry the runtime template preparer. The dev server and Vitest keep the runtime path, which checks the same rules on first render; there a template-locations plugin tells the runtime where each template is written, so its errors and hydration mismatches name your file, line and column. Withparse5installed, the build also compares each template with a full HTML parser's result. - Server code under
vite dev. Vite, not Node, loads@gyral/*(and your dependencies that use them) in server code, so server rendering there gets development output and shares one copy of@gyral/core. - Pre-bundling. The dev server and Vitest pre-bundle the browser entry points of every
Gyral package installed for the app, and of your direct dependencies that use Gyral (a design
system, a widget library), all together, so they never load a second copy of
@gyral/corepartway through a run. The pre-bundle is core's development build, with every development check and full messages:gyral()gives Vite's dependency optimizer the environment's resolve conditions (yourresolve.conditions, or Vite's defaults withdevelopment), and anoptimizeDeps.rolldownOptions.resolve.conditionNamesof your own wins. Production builds are unaffected.gyral()also warns once when@gyral/coreis installed more than once (G0015): pin one version with your package manager's overrides. optimize: 'auto'points Vite's startup scan at the modules that define components, the modules listed incomponents.modules, the client build's inputs and a Vitest browser project's tests, so Vite finds everything they import before the first page or test loads instead of reloading partway through. It is on by default when Vite runs as a custom server (appType: 'custom', as the development server does); set it yourself in a Vitest browser project. A list adds dependencies to pre-bundle,gyral({ optimize: ['some-dep'] }), andoptimize: falseturns the scan off.clientOnly: true, for apps no server renders, leaves the hydration code out of the browser bundle. See Client-only builds.components: trueloads each component's module once its tag is on the page, so pages need no client entry. See Code splitting.types:gyral()writes element typings to.gyral/elements.d.tsby scanningsrc;types: falseturns that off.
Import html from @gyral/core wherever you write templates. The compiler follows that import,
and refuses an alias (const h = html) it can't follow. For Vitest browser tests, add gyral()
to each browser project's config too (Testing).
gyralVitePreset(options) returns the same plugins as { plugins, optimizeDeps }, for a config
that spreads it in. npx @gyral/codemod 0.3.8 rewrites such a config to plugins: [gyral()]
where it can prove the result the same, and lists the configs it leaves.
The ESLint plugin
@gyral/core/eslint shows the compiler's template errors in your editor, with the same
messages, checks that each rows read only their arguments, and finds intent parsers no
template names:
// eslint.config.js
import gyral from '@gyral/core/eslint';
export default [{ files: ['src/**/*.ts'], ...gyral.configs.recommended }];recommended turns on gyral/template, gyral/each-row-purity and gyral/intent-event (an
on() parser bound to another event) as errors,
and gyral/unused-intent and gyral/prefer-built-in as warnings. gyral/prefer-built-in finds
hand-written versions of what Gyral has built in and names the built-in: a dialog opened with
showModal() (?modal), .focus() in a driver on an element reached through a component's
shadowRoot or renderRoot (the focus() command; a page element outside any component stays
the driver's to focus), JSON.parse of an attribute or prop (prop.json()), a subscription
that drops repeated values by hand (equals), a record of drivers outside tests (a list), and a
parser that checks the event's type, modifier keys, Number(value) of a number input or
composedPath()[0] (on(), about 0.43 KiB gzip once per app). It works without Vite.
See Views for what the template rules catch.
Publishing a component library
A package of Gyral components, such as a design system or a set of widgets, must share the
app's copy of @gyral/core: a stylesheet from css or a store made by another copy doesn't work
in the app's. List core as a peer dependency with a caret range, and as a dev dependency at the
version you develop against:
{
"peerDependencies": { "@gyral/core": "^0.3.9" },
"devDependencies": { "@gyral/core": "0.3.9" }
}- A peer, never a dependency. A dependency pinned to one version installs a second core whenever the app has another one (G0015); a peer uses the app's.
- A caret range:
^0.3.9is 0.3.9 up to, but not including, 0.4.0. Its lower bound is the oldest core you test against. Gyral's own packages pin core exactly because they use its internals; a library uses the public API, so any core in the range works. Other Gyral packages the library imports, such as@gyral/http, are caret peers too. - Public entry points only:
@gyral/core,@gyral/core/storageand the other documented entries, never@gyral/core/internalor a file path, which change in any release. Importhtml,svgandcssfrom@gyral/coreitself where you write templates, so the app's template compiler sees them. - Templates as written. Publish ES modules with their
htmltemplates as they are. The app's build compiles every template it bundles,node_modulesincluded, and reads every installed package that lists@gyral/core, as a peer too, for the features its components use (what the build reads). - Test against the range: run the library's tests with the lowest core its range allows and
with the newest.
npx @gyral/mcp@0.3.9 skill --checkin its CI keeps the agent skill it carries on the version it develops against.
What's inside
@gyral/core has no runtime dependencies. Its view layer was written for Gyral alone, from the
platform's own primitives: <template> cloning, constructable stylesheets, Declarative Shadow
DOM. Commands run on a small built-in runtime (one AbortController per task) that handles
cancellation, concurrency lanes and retries.
Apps ship only the features they use. each, raw, hooks, commands, stores and the prop builders
register themselves when your code first calls them, so an app that never calls one doesn't
bundle it, and the hydration code is a separate chunk that only server-rendered pages fetch.
Measured on Gyral's examples at 0.3.9 (KiB gzip, production builds with gyral()):
| App | First load | All chunks |
|---|---|---|
| hello-world | 9.7 | 12.1 |
| hello-world, client-only | 8.6 | 8.6 |
| isomorphic (SSR) | 15.6 | 18.7 |
| no-js-first (SSR, forms) | 20.0 | 20.1 |
"First load" is the entry chunk and what it imports statically: what a page downloads before any
lazy import(). The no-js-first example loads its components
by what's on the page, so its first
load counts what a page with every component preloads, the hydration chunk included. Builds leave
out view transitions, the frame lane and custom states unless a module names their spec field (the
build reads your code and the packages that depend on Gyral; see what the build
reads), on() (about 0.43 KiB) is bundled only by
apps that call it,
and production builds print short
error codes instead of messages. Error handling, which every
app carries, costs about 0.75 KiB. The client-only row is the same app built with
clientOnly: true. The isomorphic example
routes, and the router's scroll and focus handling costs it
about 0.5 KiB of first load; the History API code (about 0.75 KiB) loads only in browsers without
the Navigation API.
Browser code targets Baseline "widely available" features. Newer APIs, such as the Navigation API, URLPattern, invoker commands and View Transitions, are feature-detected and used as enhancements.
Known issues
- Some markup isn't handled yet by the template checks:
<select>content under the new customizable-select parsing, CDATA in SVG, and<noscript>as raw text. Keep these out of templates, or put them inraw()on the server. - Deno and Cloudflare Workers have only had a spot check for server rendering: a
renderPagebundle ran on Deno and in a local Workers runtime, but adapters, deployed edge behavior, bindings and assets aren't tested (see Deploying). - Without a preload hint, the hydration chunk loads one round trip after the entry on
server-rendered pages. Pass
clientAssetsFromManifest()'smodulepreloadtorenderPage(see Static sites);productionServerhands it to your app, with anassets(modules)that adds a page's lazily imported route chunks.