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

solid-tag-runtime

v0.0.34

Published

Runtime module system for JSX modules compiled with solid-tag and executed through @solidjs/html

Readme

solid-tag-runtime

solid-tag-runtime is a runtime module system for dynamically defined Solid JSX and JavaScript modules. It compiles JSX with solid-tag, links runtime-defined modules, and can optionally integrate with browser HTML for declarative rendering.

source module
    ↓
solid-tag compiler
    ↓
pre-link compiled artifact
    ↓
solid-tag-runtime linker
    ↓
Solid runtime / browser module

The package is split into three layers:

solid-tag-runtime
    core runtime, modules, compilation and cache

solid-tag-runtime/solid
    high-level Solid family loading and createSolidRuntime()

solid-tag-runtime/html
    declarative HTML discovery, rendering and <solid-render>

Install

npm install solid-tag-runtime solid-tag

Quick start

For the common browser setup, prefer the localStorage compile-cache backend when the compiled cache is expected to fit comfortably in localStorage:

import { createCompileCache } from "solid-tag-runtime";
import { createSolidRuntime } from "solid-tag-runtime/solid";

const runtime = await createSolidRuntime({
  compileCache: createCompileCache({
    storage: "localstorage",
    name: "my-app",
  }),
});

await runtime.htmlController.process();

This:

  • resolves the coordinated Solid runtime family
  • creates the runtime
  • prepares one idle default HTML controller
  • enables persistent compile caching through localStorage
  • installs the Solid DOM integration used by declarative rendering
  • explicitly performs the initial declarative HTML processing pass

Current browser benchmarks show lower warm-cache lookup/compile latency from localStorage than IndexedDB for the tested compiled-artifact sizes. Prefer IndexedDB when larger storage capacity, asynchronous storage behavior, or application storage policy matters more than minimum lookup latency.

Persistent caching and HTML processing are both opt-in. createSolidRuntime() prepares runtime.htmlController, but it does not scan declarations or start observation until you ask it to:

const runtime = await createSolidRuntime();

// No declarative modules have been processed yet.
await runtime.htmlController.process();

For the same one-shot processing during factory creation, use the convenience option:

const runtime = await createSolidRuntime({
  processHTML: true,
});

// The same controller remains available for later work.
await runtime.htmlController.observe({ mode: "continuous" });

Injecting a compatible HTML tag

createSolidRuntime() can replace only the default export of its coordinated @solidjs/html host namespace with a caller-owned compatible tag:

import { createCompileCache } from "solid-tag-runtime";
import { createSolidRuntime } from "solid-tag-runtime/solid";
import { createLocalStorageHtml } from "solid-html-cache/cachable";

const html = createLocalStorageHtml({
  namespace: "my-app",
});

const runtime = await createSolidRuntime({
  html,
  compileCache: createCompileCache({
    storage: "localstorage",
    name: "my-app-modules",
  }),
  processHTML: true,
});

Generated runtime modules remain provider-independent and still import:

import html from "@solidjs/html";

The runtime linker resolves that static import to this runtime's host bridge, whose default export is the supplied tag. Named exports continue to come from the coordinated official @solidjs/html namespace. Changing only the injected tag does not fragment persistent syntax-compile cache identity.

The supplied tag is caller-owned: disposing the runtime does not clear, disable, flush, prune, or dispose it. Do not also provide modules["@solidjs/html"] when using html; if you need to supply the coordinated official namespace explicitly, use solid.modules. The injected tag should be built against the same compatible Solid family as the runtime.

Define and import runtime modules

runtime.define("/Greeting.jsx", `
  export default function Greeting() {
    return <p>Hello from runtime JSX</p>;
  }
`);

const module = await runtime.import("/Greeting.jsx");
module.default;

Runtime modules can import one another using normal relative module syntax.

runtime.define("/Button.jsx", `
  export function Button() {
    return <button>Hello</button>;
  }
`);

runtime.define("/App.jsx", `
  import { Button } from "./Button.jsx";

  export default function App() {
    return <Button />;
  }
`);

Private module imports

Application-owned values can also be injected as private imports for one source module without registering a runtime-global module:

const result = await fetchData(url);

runtime.define("/ui/hello.tsx", `
  import { result } from "#state";

  export default function App() {
    return <div>Hello {result.name}</div>;
  }
`, {
  imports: {
    "#state": { result },
  },
});

Private imports are importer-local host namespaces. They preserve exact JavaScript reference identity, are not returned by runtime.import("#state"), do not appear as global entries in runtime.modules(), and are not inherited by child modules. Different source modules may reuse the same private specifier with different values.

Private imports use #-prefixed specifiers in this release. If a runtime-global module has the same name, the private import shadows it only for the defining importer. The explicit resolve() hook remains the highest-priority override.

Inspect configured private imports from the host with:

const imports = runtime.getPrivateImports("/ui/hello.tsx");
imports?.get("#state");

runtime.getModuleInfo("/ui/hello.tsx")?.privateImports;

Changing only private bindings causes relinking/evaluation but does not change pre-link compile-cache identity.

Lazy private-import providers

Private imports may also be lazy sync or async providers. Providers are not invoked by define(); they run only when the owning module actually imports that private specifier, and the resolved namespace is cached for the current module generation.

runtime.define("/ui/x.jsx", `
  import { message, state } from "#private";

  export default function App() {
    return <h1>{message}: {state.count()}</h1>;
  }
`, {
  imports: {
    "#private": async ({ import: importModule }) => ({
      message: "Helloa",
      state: await importModule("@state"),
    }),
  },
});

The provider context contains runtime, importer, and an importer-aware import(specifier) helper. Imports made through that helper are recorded as dependencies of the owning source module. Replacing or invalidating a dependency such as @state therefore invalidates the importer, clears the cached private provider result, and causes the provider to run again on the next import.

Use the context import() helper when dependency tracking matters. A provider can still close over the runtime and call runtime.import() directly, but the runtime does not infer that arbitrary call as a provider dependency. Provider failures reject the owning module import, and private-provider/source dependency cycles fail with CircularDependencyError. Provider resolution remains post-compile/link-time state and does not affect compile-cache identity.

Module readiness

runtime.import(id) remains an immediate resolution operation. When a runtime-owned module will be registered later (for example by htmlController.process()), use the readiness API instead:

const statePromise = runtime.waitForModule("@state");
await runtime.htmlController.process();
const state = await statePromise;

waitForModule() waits only for registration in this runtime; it does not fall through to native/browser resolution or repeatedly invoke fallback resolvers while pending. Once the module is registered it is imported/evaluated normally, so real module errors propagate unchanged.

const state = await runtime.waitForModule("@state", {
  timeout: 5000,
  signal,
});

There is no default timeout. A timeout rejects with ModuleWaitTimeoutError; an AbortSignal or runtime disposal also cleans up an outstanding wait.

Private providers expose the tracked equivalent:

imports: {
  "#state": async ({ waitForModule }) =>
    waitForModule("@state"),
}

Provider-context waitForModule() records the resolved runtime module as a dependency of the owning importer, so replacement invalidates the importer and re-runs the provider. For one-time host-side readiness, PromiseLike private definitions are also supported:

imports: {
  "#state": runtime.waitForModule("@state"),
}

That PromiseLike convenience form does not infer a provider dependency edge. Use the provider-context form when invalidation/relink tracking matters.

The four related operations are:

runtime.import()                  import now
runtime.waitForModule()           wait for runtime registration
providerContext.import()          import now + track dependency
providerContext.waitForModule()   wait for registration + track dependency

Declarative HTML

Runtime modules can be declared directly in the document. Start the initial one-shot processing explicitly with the runtime's default controller:

const runtime = await createSolidRuntime();
await runtime.htmlController.process();

Configure the idle controller without starting processing through the factory when you need a custom root/scope/style policy:

const runtime = await createSolidRuntime({
  htmlController: {
    root: document,
    scope: "main",
    acceptUnscoped: false,
  },
});

createSolidRuntime({ processHTML: true }) is shorthand for the same initial processing step. The controller remains available afterward for observation, additions, root changes, subscriptions, and other HTML lifecycle operations.

For example, declarations can live directly in the page:

<div id="app"></div>

<script
  type="solid-jsx"
  module="/App.jsx"
  render="#app"
>
  export default function App() {
    return <h1>Hello</h1>;
  }
</script>

Bare render mounts in place without adding a wrapper:

<script type="solid-jsx" module="/Message.jsx" render>
  export default function Message() {
    return <button onClick={() => console.log("clicked")}>Click</button>;
  }
</script>

For an additional explicitly managed controller, use the HTML entrypoint directly:

import { createHTMLRuntime } from "solid-tag-runtime/html";

const preview = createHTMLRuntime(runtime, {
  root: previewRoot,
  scope: "preview",
});

await preview.process();

Explicit controllers are independent by default. With a low-level createRuntime() instance, pass asDefault: true when that controller should become runtime.htmlController.

<solid-render>

Reusable module instances can be rendered with the custom element:

<script type="solid-jsx" module="/Counter.jsx">
  export default function Counter(props) {
    return <button>Count: {props.initial}</button>;
  }
</script>

<solid-render
  module="/Counter.jsx"
  prop:initial="{100}"
></solid-render>

Always use an explicit closing tag. Custom elements are not HTML void elements.

Declarative props

Plain prop:* values remain strings:

<solid-render prop:label="123"></solid-render>

One outer {...} pair opts an individual prop into safe typed-data parsing:

<solid-render
  prop:count="{3}"
  prop:enabled="{true}"
  prop:items="{[1, 2, 3]}"
  prop:options="{{ compact: true }}"
></solid-render>

Bulk structured props use the safe JSON5-style data syntax:

<solid-render
  module="/Counter.jsx"
  props="{ initial: 100, options: { theme: 'dark' } }"
></solid-render>

Declarative prop syntax is data-only and never evaluates JavaScript expressions. Programmatic .props remains available for functions, signals, services, class instances and other identity-sensitive JavaScript values.

Precedence is:

props
  < prop:*
  < element.props

Avoiding initial <solid-render> FOUC

The HTML runtime hides pending <solid-render> content automatically after the runtime has initialized. JavaScript cannot prevent already-parsed light-DOM children from painting before the library itself loads.

For strict zero-FOUC initial markup, put the preflight rule in <head>:

<style>
  solid-render:not([show-until-ready]):not(:defined),
  solid-render:not([show-until-ready])[data-solid-render-state="pending"] {
    visibility: hidden;
  }
</style>

If the application owns that rule, duplicate package style injection can be disabled:

const runtime = await createSolidRuntime({
  processHTML: {
    injectSolidRenderStyles: false,
  },
});

show-until-ready intentionally keeps fallback children visible while the component is pending.

HTML processing compatibility names

htmlController.register(), registerHTML(runtime), and createSolidRuntime({ registerHTML: ... }) remain available as deprecated compatibility aliases in 0.0.31. New code should use process(), processHTML(), and processHTML:.

Persistent compile cache

Persistent caching stores pre-link compiler artifacts. Runtime-specific linked URLs and evaluated module namespaces are not persisted.

For typical browser workloads, prefer localStorage when the compiled cache is expected to fit comfortably within its storage limits:

import { createCompileCache } from "solid-tag-runtime";
import { createSolidRuntime } from "solid-tag-runtime/solid";

const runtime = await createSolidRuntime({
  compileCache: createCompileCache({
    name: "my-app",
    version: "2",
    storage: "localstorage",
  }),
});

Current browser benchmarks show lower warm-cache latency for localStorage than IndexedDB across the tested small, large, and stress artifacts. The tradeoff is that localStorage is synchronous and usually offers less capacity. IndexedDB remains useful when larger persistent caches or asynchronous storage behavior are more important.

The shortest convenience form is still available:

const runtime = await createSolidRuntime({
  compileCache: true,
});

compileCache: true currently uses the existing IndexedDB-backed default for backward compatibility. cache: true is also accepted as a convenience alias, while compileCache remains the canonical option name.

The low-level localStorage configuration is also available:

import { createLocalStorageCompileCache } from "solid-tag-runtime";

const runtime = await createSolidRuntime({
  compileCache: {
    store: createLocalStorageCompileCache({
      prefix: "my-app-runtime:",
    }),
    namespace: "main",
    version: "2",
  },
});

Supported built-in stores include localStorage, IndexedDB and memory. Memory is the lowest-overhead non-persistent backend. Cache failures remain optimization failures and normal compilation continues whenever possible.

Observation strategies

If runtime declarations may be inserted outside explicit runtime APIs, the HTML controller supports observation strategies:

const html = runtime.htmlController;

// One repeatable one-shot pass, then explicit runtime APIs.
await html.process();

// Observe startup activity, then disconnect after relevant activity settles.
await html.observe({ mode: "bootstrap", idleMs: 50 });

// Observe unknown external DOM mutations throughout the page lifetime.
await html.observe({ mode: "continuous" });

Controlled dynamic module loading does not require a persistent MutationObserver. Explicit APIs such as append() and addModule() register exact declarations directly.

For a known raw-DOM mutation when observation is disabled, run another one-shot pass. You can limit it to the changed subtree without changing the controller's configured root:

root.insertAdjacentHTML("beforeend", markup);
await html.process({ root });

When an observer is connected, flush() first drains queued observer work and then forces a discovery scan of the observer root.

Scoped HTML runtimes

Multiple runtimes can share a document using scopes:

const html = createHTMLRuntime(runtime, {
  scope: "main",
  acceptUnscoped: false,
});
<script
  type="solid-jsx"
  data-solid-runtime="main"
  module="/App.jsx"
>
  ...
</script>

For <solid-render>, scope is a shorter alias for the same routing target:

<solid-render scope="main" module="/App.jsx"></solid-render>

data-solid-runtime remains the canonical cross-element attribute and wins if both are present. An unscoped default controller does not consume declarations explicitly assigned to another scope.

Host modules

Application values can be exposed as normal module namespaces:

const runtime = await createSolidRuntime({
  modules: {
    "@app/config": {
      apiUrl: "/api",
    },
  },
});

Runtime source can then import them:

import { apiUrl } from "@app/config";

Low-level runtime

createRuntime() remains synchronous, provider-agnostic and DOM-independent:

import * as Solid from "solid-js";
import * as SolidWeb from "@solidjs/web";
import html from "@solidjs/html";
import { createRuntime } from "solid-tag-runtime";

const runtime = createRuntime({
  modules: {
    "solid-js": Solid,
    "@solidjs/web": SolidWeb,
    "@solidjs/html": { default: html },
  },
});

If you want provider-aware Solid loading but still want to construct the runtime yourself:

import { createRuntime } from "solid-tag-runtime";
import { loadSolidModules } from "solid-tag-runtime/solid";

const solidModules = await loadSolidModules({
  source: "auto",
  fallbackProvider: "esm.sh",
});

const runtime = createRuntime({
  modules: {
    ...solidModules,
    "@app/state": state,
  },
});

Fallback resolution for unresolved packages

resolve() remains the override hook. fallbackResolve() is a lower-priority hook that runs only for an unresolved bare specifier after registered modules, absolute URLs, and virtual paths have had a chance to resolve.

const runtime = await createSolidRuntime({
  fallbackResolve(specifier, importer) {
    return `https://esm.sh/${specifier}`;
  },
});

A runtime module can then use a bare package import without pre-registering it:

import confetti from "canvas-confetti";

Registered runtime modules still win, so an existing @state module is never captured by a generic CDN fallback. Absolute URLs such as https://esm.sh/pkg also bypass fallbackResolve() entirely. Missing local imports such as ./Button.jsx and /ui/Button.jsx remain runtime-resolution errors and are never converted into CDN requests.

If fallbackResolve() returns undefined, an unresolved bare specifier proceeds to the normal native/import-map fallback when allowNativeImports is enabled. A fallback result is resolved without recursively invoking either resolver hook.

See Runtime modules and resolution for the complete precedence rules.

Browser import maps

When loading directly in a browser, the package root mapping is enough if you only import solid-tag-runtime itself. If your source uses bare package subpaths such as solid-tag-runtime/solid or solid-tag-runtime/html, add a trailing-slash package-prefix mapping as well:

{
  "imports": {
    "solid-tag-runtime": "https://esm.sh/[email protected]",
    "solid-tag-runtime/": "https://esm.sh/[email protected]/"
  }
}

That prefix keeps clean source specifiers such as:

import { createSolidRuntime } from "solid-tag-runtime/solid";
import { createHTMLRuntime } from "solid-tag-runtime/html";

Subpath mapping and version pinning are separate concerns: the prefix mapping is needed for bare subpath imports, while the explicit @0.0.32 version is only needed when you want a pinned release. When pinning, keep the root and prefix on the same version.

The Solid helper reads existing resolution/import-map information but does not mutate import maps.

Documentation

Engineering invariants and design decisions are documented in ARCHITECTURE.md.

Current limitations

  • runtime-defined source-module cycles are not supported yet
  • generated browser modules require a compatible CSP for the selected module URL backend
  • persistent compile caching is an optimization, not a security sandbox
  • already-held namespace/component references are not mutated by module updates
  • TypeScript/TSX support depends on the configured compiler
  • the built-in provider layer currently supports esm.sh and jsDelivr; unsafe mixed-provider cases fail rather than silently loading a second Solid runtime

0.0.31 component-event compiler integration and package hygiene

  • Uses [email protected] and compiler ABI solid-tag-runtime:[email protected]:compiler:v1.
  • Fixes non-inline component on* expressions such as onChange={handler} after browser conformance showed the previous zero-argument getter form was delivered as the callback itself.
  • Source distribution includes examples/getting-started.html with the idiomatic onChange / onReady API restored.
  • npm/bun pack excludes repository-only examples, benchmarks, architecture, tests, handoff files, and plans; public docs remain published.
  • Source ZIP packaging removes historical tarballs and embeds only the current release tarball.

Runtime-local npm package versions (0.0.33 candidate)

Both the synchronous base runtime and /solid support optional exact npm package pins. They do not modify the browser's import map. One root pin implicitly covers subpath imports.

import { createRuntime } from "solid-tag-runtime";
const runtime = createRuntime({
  packages: { provider: "esm.sh", versions: { "is-odd": "3.0.1", "lodash-es": "4.17.21" } },
});
runtime.resolve("lodash-es/debounce"); // https://esm.sh/[email protected]/debounce
import { createSolidRuntime } from "solid-tag-runtime/solid";
const runtime = await createSolidRuntime({
  solid: { source: "esm.sh", version: "2.0.0-rc.13" },
  packages: { provider: "esm.sh", versions: { "is-odd": "3.0.1" } },
});

An explicit Solid version no longer silently uses a conflicting host mapping. Pinned Solid currently allows esm.sh only; jsDelivr pinned families and same-document cross-version delegation are not verified. Pinning applies to direct runtime-managed imports, not arbitrary transitive CDN imports. See package version resolution and Solid version resolution.

Package sources and independent runtime versions (0.0.34)

packages.versions accepts plain npm versions and provider/source-prefixed strings per entry:

const a = createRuntime({ packages: { versions: { versioning: "esm.sh:gh/mizulu/[email protected]" } } });
const b = createRuntime({ packages: { versions: { versioning: "esm.sh:gh/mizulu/[email protected]", "lodash-es": "jsdelivr:4.17.21" } } });
const [one, two] = await Promise.all([a.import("versioning"), b.import("versioning")]);
// one.VERSION === "0.0.1", two.VERSION === "0.0.2" (when the CDN imports succeed)

See Package sources and versions, or open the Advanced → Runtime examples → Package sources & providers section of examples/getting-started.html.