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

@archduck/gst-core

v0.1.1

Published

Compositional config-driven rendering engine

Readme

gst-core

Config-driven rendering engine. Describe UI as plain objects -- in JavaScript, JSON, or both -- and get reactive DOM with automatic updates, dynamic properties, scoped styles, and layered configuration loading.

gst stands for Grand Schema Things -- a play on "grand scheme of things." Forms, pages, and UIs are defined as schemas (JSON configurations) rather than code. The name captures the idea that these config-driven "things" participate in a larger system of data, interaction, and workflow.

Generic by design. No opinions about forms, pages, or any domain. Domain behavior is injected through options; gst-forms is one such plugin.

Built on @vue/reactivity and gst-compose.

npm install @archduck/gst-core

Quick start

import { mount } from '@archduck/gst-core'

mount(document.getElementById('root'), {
  layout: [
    { component: 'h1', innerText: 'Hello' },
    { component: 'input', name: 'email', type: 'email' },
    { component: 'p', innerText: '{{greeting}}' },
  ],
  registries: {
    functions: {
      greeting(context) {
        return context.record.data.email
          ? `You entered: ${context.record.data.email}`
          : 'Type an email above'
      }
    }
  }
}, {
  record: { key: null, data: { email: '' } }
})

That's a reactive UI from a plain object. The <input> is two-way bound to record.data.email. The paragraph re-renders whenever the email changes, because greeting reads from the reactive record. No JSX, no templates, no build step required for the config itself.


The config object

Everything converges to a single shape before rendering:

{
  meta: {},
  layout: [],
  registries: { fields, buttons, components, functions, optionSets },
  styles: null,
  actions: {}
}

How you build this object is up to you.

Write it directly in JS when you want full control and type checking:

const form = {
  layout: [
    { component: 'h1', innerText: 'Hello' },
    { component: 'input', name: 'email' },
  ],
  registries: {
    fields: {
      email: { component: 'input', type: 'email', label: 'Email' }
    },
    functions: {
      greeting(context) { return `Hi, ${context.record.data.email}` }
    }
  }
}

mount(document.getElementById('root'), form)

Load it from JSON files when the config needs to live outside code:

import { loadJsonConfig } from '@archduck/gst-core'

const form = await loadJsonConfig('./UserView', { patches: ['uchealth'], schema })
mount(document.getElementById('root'), form)

Mix both -- load structure from JSON, define functions in JS:

const form = await loadJsonConfig('./UserView', { schema })
form.registries.functions = {
  ...form.registries.functions,
  customValidation(context) { /* ... */ }
}
mount(document.getElementById('root'), form)

Dynamic properties

Any property can be a static value or a {{functionName}} reference. Functions receive (context, event) with this bound to an element-specific stage object. They re-evaluate automatically when reactive dependencies change.

{
  show: '{{isAdmin}}',
  label: '{{getLabel}}',
  disabled: '{{isLocked}}',
  innerText: '{{summary}}'
}

There's no special list of "properties that can be dynamic." If you can set it statically, you can compute it with a function.

show conditionally renders an element. This works on fields, groups, buttons -- anything in the layout.

Pass arguments with {{functionName:arg}}:

{
  show: '{{hasRole:editor}}',
  className: '{{getStepClass:3}}'
}

The arg is always a string. Parse it in the function if you need another type.


Layout

Layout is an array of groups. Groups contain rows; rows contain field names (looked up in registries.fields) or inline element definitions:

layout: [
  {
    title: 'Contact Info',
    rows: [
      ['firstName', 'lastName'],    // side by side
      ['email'],                     // full width
    ]
  },
  {
    title: 'Preferences',
    show: '{{hasAccount}}',
    rows: [['theme', 'language']]
  }
]

Bare arrays are sugar for { rows: [...] }. String items in rows are registry lookups; object items render inline.


Registries and spread

Registries are named definition collections. Definitions inherit from each other using gst-compose spread syntax:

registries: {
  fields: {
    email: { component: 'input', type: 'email', placeholder: 'Email' },
    workEmail: { '...': 'email', placeholder: 'Work email' },
    personalEmail: { '...': 'email', placeholder: 'Personal email' },
  }
}

workEmail spreads everything from email, then overrides placeholder. Cross-registry references work too: "...": "fields.email" from inside a different registry.


Config loading

Load config from a directory of JSON/JS files. Files are discovered by naming convention and merged using gst-compose:

const form = await loadJsonConfig('./UserView', { schema })

// With variant overlays (each variant stacks on top)
const form = await loadJsonConfig('./UserView', { patches: ['uchealth', 'admin'], schema })

File naming: property[-variant].{json|js}. A form directory might contain:

UserView/
  fields.json            # base field definitions
  fields-uchealth.json   # uchealth variant (merges with base)
  layout.json            # layout structure
  functions.js           # custom functions (JS, not JSON)
  styles.json            # scoped styles

The loader finds all files, resolves variants in order, applies gst-compose spread semantics, and returns the unified config object.


LiveConfig

Patch configuration at runtime without reloading:

import { LiveConfig } from '@archduck/gst-core'

const liveConfig = new LiveConfig({ schema })
await liveConfig.load('./UserView')

liveConfig.patch('fields', { email: { required: true } })
const form = liveConfig.get()

Patches cascade through dependencies. If workEmail spreads from email, patching email re-resolves workEmail too.


Scoped styles

styles: {
  scoped: true,
  rules: {
    '&': { maxWidth: '48em', marginInline: 'auto' },
    '& input': { padding: '0.5em', border: '1px solid #ccc' },
  }
}

& refers to the mount container. Styles are written in JS object syntax (camelCase properties), injected as a <style> element, and cleaned up on unmount. Multiple mount instances on the same page don't collide.


Extending gst-core

Custom components

Register renderers in the component registry:

registries: {
  components: {
    StarRating: {
      _adapter: {
        mount(container, { def, store }) {
          // create DOM, wire events, return { unmount() {} }
        }
      }
    }
  }
}

Components are referenced by name in definitions: { component: 'StarRating', max: 5 }.

Define your own domain vocabulary

createTLPSchema lets you declare what top-level properties exist, which are registries, and how they depend on each other:

import { createTLPSchema } from '@archduck/gst-core'

const schema = createTLPSchema({
  functions:   { registry: true, dependencies: [] },
  widgets:     { registry: true, dependencies: ['functions'] },
  dataSources: { registry: true, dependencies: [] },
  layout:      { dependencies: ['widgets', 'dataSources'], default: [] },
  settings:    { dependencies: [] },
})

The dependencies array drives resolution order via topological sort. Config loading, variant layering, spread syntax, and cross-references all work with your vocabulary. gst-forms defines its own schema (fields, buttons, actions, hooks); you could define one for a dashboard engine, page builder, or anything else.

Inject domain behavior

mount() is a generic renderer. It turns config into reactive DOM but has no opinion about what that DOM means. Domain-specific behavior goes through options:

mount(container, config, {
  record: { key: null, data: {} },
  components: {},
  initialState: {},
  effects: [(store) => effect(() => { /* ... */ })],
  setupActions: (store, options) => { /* ... */ },
  onMount: (store, controller) => {},
  onUnmount: (store) => {},
  onHide: (def, store) => {},
})

gst-forms injects dirty tracking, field errors, action hooks, and confirmation dialogs through this interface. A different plugin could inject drag-and-drop, real-time collaboration, or analytics.


API reference

mount(container, config, options?) -> controller

Render config into a DOM container. Returns a controller:

  • controller.update(record) -- update record data reactively
  • controller.executeAction(name, extra) -- trigger an action
  • controller.unmount() -- clean up and remove from DOM
  • controller.store -- the reactive store (read-only access)

createTLPSchema(declarations) -> schema

Build a schema from top-level property declarations. Returns an object with resolutionOrder, dependencies, registryMapping, validRegistries, and getDefault(property).

loadJsonConfig(path, options?) -> Promise<config>

Load and merge config files from a namespace directory. Options include schema (required), patches (variant array), scope, registries, functions.

LiveConfig

Stateful config manager with runtime patching.

  • new LiveConfig({ schema })
  • liveConfig.load(path, options?) -- load from files
  • liveConfig.from(config, options?) -- initialize from existing config
  • liveConfig.get() -- get resolved config (cached)
  • liveConfig.patch(property, patch) -- merge patch, re-resolve dependents
  • liveConfig.set(property, value) -- replace property, re-resolve dependents

Config merging utilities

  • mergeConfigs(...configs) -- deep merge with array spread support
  • mergeRegistries(...registries) -- merge registry objects
  • applyMap(config, map) -- apply string replacement map (i18n)
  • mergeMaps(...maps) -- merge translation maps

Reactivity (re-exported from @vue/reactivity)

reactive, effect, computed, ref, toRaw, stop, pauseTracking, enableTracking

Style processing

  • processStyles(styles, scopeId) -- convert JS style objects to scoped CSS
  • injectStyles(css, id) / removeStyles(id) -- manage <style> elements
  • generateScopeId() -- generate unique scope ID