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-compose

v0.1.0

Published

Generic JSON/JS composition engine with spread semantics

Downloads

100

Readme

gst-compose

A JavaScript library for composing JSON data. Apply a patch of new data to existing data to replace it or merge with it. Pass in a dictionary for the patch to reference things by name.

gst stands for Grand Schema Things -- a play on "grand scheme of things." The gst family of libraries builds UIs from JSON configurations rather than code. gst-compose is the foundation: the composition engine that makes layered config possible.

Two functions, zero dependencies, ~230 lines.

npm install @archduck/gst-compose
import { apply, reduce } from '@archduck/gst-compose'

Why

Config varies. A SaaS product has per-tenant settings. An app has dev/staging/prod environments. A UI has locale-specific strings. The usual options are branching logic (if tenant === 'acme') or deep merge (lodash.merge).

Branching grows with every new variant. Deep merge gives you no control -- it merges everything recursively, which isn't always what you want. Sometimes you need to replace an array, sometimes extend it. Sometimes replace a nested object, sometimes patch one field.

gst-compose puts the merge strategy in the data itself. A patch that uses "...": "^" keeps the base and overrides on top. A patch without it replaces entirely. This decision is made per-key, at any nesting depth, by the patch -- not by the code that processes it. The processing code is always just apply(base, patch).

base.json -- the default compensation package:

{
  "salary": 50000,
  "bonus": false,
  "retirement": "none",
  "insurance": "basic",
  "dental": true,
  "vision": true,
  "pto": 10,
  "sickDays": 5,
  "remote": false,
  "relocation": false,
  "parking": true,
  "laptop": "standard",
  "phone": false,
  "tuition": false,
  "gym": false
}

senior.json -- two changes:

{ "...": "^", "salary": 90000, "bonus": true }

executive.json -- three changes:

{ "...": "^", "salary": 150000, "retirement": "401k-match", "insurance": "premium" }

The JavaScript:

const config = apply(base, patch)

One line. The patch file describes everything -- what to keep, what to change. You can see at a glance what each level overrides.


Spread for JSON

JavaScript merges objects with spread:

const defaultPrefs = { lang: "en", notifications: true, theme: "light" }
const userPrefs = { ...defaultPrefs, theme: "dark" }
// { lang: "en", notifications: true, theme: "dark" }

JSON has no spread operator. gst-compose adds one. The "..." key means "spread from":

{ "...": "defaultPrefs", "theme": "dark" }

When gst-compose processes this, it finds the object called "defaultPrefs", spreads its properties in, then applies "theme": "dark" on top. Same result as the JS spread.


apply

apply(base, patch, dictionary) -- combine two objects into one.

  • base -- the existing object
  • patch -- the new object, applied on top of base
  • dictionary -- a dictionary of objects that patch can reference by key

patch replaces base:

apply(
  {
    salary: 50000,
    bonus: false,
    retirement: "none",
    insurance: "basic",
    pto: 10
  },
  { salary: 90000 }
)
// !! { salary: 90000 }

Only patch survives. To keep the rest of the record, patch spreads from base with "...": "^":

apply(
  {
    salary: 50000,
    bonus: false,
    retirement: "none",
    insurance: "basic",
    pto: 10
  },
  { "...": "^", salary: 90000 }
)
// {
//   salary: 90000,
//   bonus: false,
//   retirement: "none",
//   insurance: "basic",
//   pto: 10
// }

patch can also spread from a key in dictionary:

apply(
  {
    salary: 50000,
    bonus: false,
    retirement: "none",
    insurance: "basic",
    pto: 10
  },
  { "...": "senior", pto: 25 },
  {
    senior: {
      salary: 90000,
      bonus: true,
      pto: 20
    }
  }
)
// { salary: 90000, bonus: true, pto: 25 }

To spread from both base and a dictionary entry, use an array:

apply(
  {
    salary: 50000,
    bonus: false,
    retirement: "none",
    insurance: "basic",
    pto: 10
  },
  { "...": ["^", "senior"], pto: 25 },
  {
    senior: {
      salary: 90000,
      bonus: true,
      pto: 20
    }
  }
)
// {
//   salary: 90000,
//   bonus: true,
//   retirement: "none",
//   insurance: "basic",
//   pto: 25
// }

Spreads base first, then senior on top, then patch's own keys override both. retirement and insurance survived from base because senior doesn't touch them. pto is 25 because the patch overrode senior's 20.


Nesting

Spread corresponds to the same location in the base. A nested "^" spreads from the base's value at that same key:

apply(
  {
    salary: 50000,
    benefits: {
      insurance: "basic",
      retirement: "none"
    },
    perks: ["parking"]
  },
  {
    "...": "^",
    salary: 90000,
    benefits: {
      "...": "^",
      retirement: "401k-match"
    },
    perks: ["...", "gym", "lunch"]
  }
)
// {
//   salary: 90000,
//   benefits: {
//     insurance: "basic",
//     retirement: "401k-match"
//   },
//   perks: ["parking", "gym", "lunch"]
// }

The outer "^" keeps salary, benefits, and perks from the base. The inner "^" on benefits keeps insurance while overriding retirement. The "..." in perks splices in the base's ["parking"] and appends the new items.

Without "..." at each level, that level gets replaced entirely. Spread is explicit, not recursive.

If a nested spread doesn't correspond to anything in the base, it fails safely -- "^" spreads from an empty object, "..." splices in an empty array:

apply(
  {
    salary: 50000
  },
  {
    "...": "^",
    benefits: {
      "...": "^",
      retirement: "401k-match"
    }
  }
)
// {
//   salary: 50000,
//   benefits: {
//     retirement: "401k-match"
//   }
// }

Arrays don't support dictionary lookups. "..." in an array is always a positional marker meaning "insert the base array here."


reduce

Apply multiple patches in sequence. Each patch decides independently whether to spread from the previous result or replace it:

reduce([
  {
    salary: 50000,
    bonus: false,
    pto: 10
  },
  { "...": "^", salary: 90000 },
  { "...": "^", bonus: true },
  { "...": "^", pto: 25 }
])
// {
//   salary: 90000,
//   bonus: true,
//   pto: 25
// }

Each entry is applied on top of the accumulated result from left to right.


__source

Bring a subset of the dictionary into local scope so children don't need full paths:

const dictionary = {
  packages: {
    senior: {
      salary: 90000,
      bonus: true
    },
    executive: {
      salary: 150000,
      bonus: true,
      retirement: "401k-match"
    }
  }
}

apply(
  {},
  {
    __source: "packages",
    cto: { "...": "executive", pto: 30 }
  },
  dictionary
)
// {
//   cto: {
//     salary: 150000,
//     bonus: true,
//     retirement: "401k-match",
//     pto: 30
//   }
// }

Without __source, the child would need "...": "packages.executive". Dot notation reaches into nested dictionary paths.

Accepts a string, an array of strings, or null to block all resolution.


String shorthand

When a key and value are the same string, and that string exists in the dictionary, it resolves as a reference:

const dictionary = {
  senior: {
    salary: 90000,
    bonus: true
  },
  executive: {
    salary: 150000,
    bonus: true,
    retirement: "401k-match"
  }
}

apply(
  {},
  {
    __source: "roles",
    senior: "senior",
    executive: "executive"
  },
  { roles: dictionary }
)
// {
//   senior: { salary: 90000, bonus: true },
//   executive: { salary: 150000, bonus: true, retirement: "401k-match" }
// }

Equivalent to "senior": { "...": "senior" }.


Idempotency

apply consumes its directives ("...", __source) during resolution. The output is plain data with no directives left. Running it through apply again produces the same result:

const first = apply(
  {},
  { "...": "senior", pto: 25 },
  dictionary
)
const second = apply({}, first, dictionary)
// first and second are structurally identical

Function values pass through by reference, not by copy.


API

apply(base, patch, dictionary?) -> result

Apply patch on top of base, resolving spread and source directives against dictionary.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | base | any | -- | The existing object | | patch | any | -- | The new object to apply on top | | dictionary | object | {} | Objects that patch can reference by key |

reduce(patches, initial?, dictionary?) -> result

Apply multiple patches in sequence, left to right.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | patches | array | -- | Patches to apply in order | | initial | any | {} | Starting value | | dictionary | object | {} | Objects that patches can reference by key |

loadFile(path) -> Promise

Load a JSON or JS file. JSON files are fetched and parsed; JS files are dynamically imported.

Special keys

| Key | In objects | In arrays | |-----|-----------|-----------| | "..." | Spread from base ("^"), dictionary ("name"), or both (["^", "name"]) | Spread previous array at this position | | __source | Narrow dictionary for child lookups | N/A |

Both keys are consumed during resolution and never appear in the output.


Caveats

  • No prototype pollution guards. If the dictionary comes from untrusted input, sanitize keys.
  • No circular reference detection on objects. Circular dictionary strings are fine (path traversal only), but circular object references will overflow the stack.
  • No max depth limit. Config objects rarely go deep enough for this to matter.
  • Unresolved spread references are preserved in the output for multi-pass resolution.
  • Functions pass through by reference, not cloned.