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

@primafuture/netson

v1.2.0

Published

A reusable layered Jsonnet configuration and cascade helper library.

Readme

@primafuture/netson

A reusable layered Jsonnet configuration and cascade helper library for Node.js.

It provides:

  • ordered layered config loading
  • visible config merge with explicit delete via netson.value.unset
  • computed values that resolve against the final merged config
  • value pipelines with computed, patch, finalize, and replace
  • generic tagged values with configLayer provenance
  • helper-root composition for hidden Jsonnet helpers
  • deferred placeholder resolution
  • a built-in generic cascade engine
  • explain/debug traces
  • a public Jsonnet asset at @primafuture/netson/lib/netson.libsonnet

Install

Published package:

npm install @primafuture/netson

Official Jsonnet runtime backend:

npm install @primafuture/netson @hanazuki/node-jsonnet

Inside this local package:

npm install

Build the library:

npm run build

Typecheck only:

npm run typecheck

Run the smoke example:

npm run smoke

Run the CLI tester:

npm run cli -- --help

This package builds to:

  • CommonJS: dist/index.cjs
  • ESM: dist/index.mjs
  • Type declarations: dist/index.d.ts
  • Jsonnet asset: lib/netson.libsonnet

Quick Example

import { resolveLayeredConfig } from '@primafuture/netson';
import { createHanazukiJsonnetEvaluator } from '@primafuture/netson/hanazuki';

const jsonnet = createHanazukiJsonnetEvaluator({
  defaultImportRootDirectories: ['/path/to/project/node_modules'],
});

const result = await resolveLayeredConfig({
  jsonnet,
  layers: [
    {
      id: 'base',
      path: '/path/to/config/base.jsonnet',
    },
    {
      id: 'override',
      source: '{ featureFlags+: { debug: true } }',
    },
  ],
});

console.log(result.visibleConfig);
console.log(result.deferredResolvedConfig);

CLI Tester

The package also ships with a small CLI tester for local config experiments.

Commands:

  • netson-test resolve ... prints the merged config
  • netson-test validate ... just checks that layered evaluation and deferred processing succeed
  • netson-test explain --path ... shows how a value evolved across layers and, for deferred cascade placeholders, also prints the cascade explain trace
  • netson-test explain --format human --path ... renders the same explain data as a short human-readable terminal summary

Resolve a layered example:

npm run cli -- resolve \
  examples/layered-config/01-base.jsonnet \
  examples/layered-config/02-region.jsonnet \
  examples/layered-config/03-local.jsonnet

Resolve a tagged value example:

npm run cli -- resolve \
  examples/tagged-values/01-base.jsonnet \
  examples/tagged-values/02-child-helper.jsonnet

Validate a deferred cascade example:

npm run cli -- validate \
  examples/deferred-cascade/01-base.jsonnet \
  examples/deferred-cascade/02-dev.jsonnet

Explain why a layered value ended up as it did:

npm run cli -- explain --path features.debug \
  examples/layered-config/01-base.jsonnet \
  examples/layered-config/02-region.jsonnet \
  examples/layered-config/03-local.jsonnet

Explain why a value was deleted:

npm run cli -- explain --format human --path removedLater \
  examples/layered-config/01-base.jsonnet \
  examples/layered-config/02-region.jsonnet \
  examples/layered-config/03-local.jsonnet

Explain a deferred cascade placeholder:

npm run cli -- explain --path generated \
  examples/deferred-cascade/01-base.jsonnet \
  examples/deferred-cascade/02-dev.jsonnet

Render the explain output in a human-readable terminal format:

npm run cli -- explain --format human --path features.debug \
  examples/layered-config/01-base.jsonnet \
  examples/layered-config/02-region.jsonnet \
  examples/layered-config/03-local.jsonnet
npm run cli -- explain --format human --path generated \
  examples/deferred-cascade/01-base.jsonnet \
  examples/deferred-cascade/02-dev.jsonnet

Layered Config Example

This example shows only the visible layered merge behavior.

base.jsonnet

{
  serviceName: 'api',
  port: 8080,
  nullableSetting: 'base-value',
  features: {
    metrics: true,
    debug: false,
  },
  tags: ['base', 'http'],
  removedLater: 'remove-me',
}
import { resolveLayeredConfig } from '@primafuture/netson';
import { createHanazukiJsonnetEvaluator } from '@primafuture/netson/hanazuki';

const jsonnet = createHanazukiJsonnetEvaluator({
  defaultImportRootDirectories: ['/project/node_modules'],
});

const result = await resolveLayeredConfig({
  jsonnet,
  layers: [
    {
      id: 'base',
      path: '/project/config/base.jsonnet',
    },
    {
      id: 'override',
      source: `
local netson = import '@primafuture/netson/lib/netson.libsonnet';

{
  port: 9090,
  nullableSetting: null,
  features: {
    debug: true,
  },
  tags: ['prod'],
  removedLater: netson.value.unset,
}
`,
    },
  ],
});

result.visibleConfig

{
  "serviceName": "api",
  "port": 9090,
  "nullableSetting": null,
  "features": {
    "metrics": true,
    "debug": true
  },
  "tags": ["prod"]
}

result.deferredResolvedConfig

{
  "serviceName": "api",
  "port": 9090,
  "nullableSetting": null,
  "features": {
    "metrics": true,
    "debug": true
  },
  "tags": ["prod"]
}

What this shows:

  • objects are deep-merged, so features.metrics survives while features.debug is overridden
  • arrays are replaced as a whole, so tags becomes ["prod"]
  • null stays in the result as ordinary data
  • netson.value.unset removes removedLater
  • without deferred placeholders, visibleConfig and deferredResolvedConfig are identical

Computed Value Example

Computed values are useful when a value in a base layer must be evaluated against the final merged config, including child overrides.

local netson = import '@primafuture/netson/lib/netson.libsonnet';

{
  dev: true,
  url: netson.value.computed(function(config)
    if config.dev then 'http://dev:1234' else 'https://production.com'
  ),
}

If a child layer sets { dev: false }, result.visibleConfig.url is a pipeline placeholder, but result.deferredResolvedConfig.url becomes:

"https://production.com"

Computed functions receive a lazy resolved config proxy. They can read other computed or deferred values, and Netson resolves those dependencies on demand. Cycles fail with a config path error.

Computed values can also be used inside deferred cascade params.context. This lets a base layer define a cascade placeholder once while the context reads child layer overrides from the final merged config.

A child layer can still replace the computed field directly:

{
  url: 'forced value',
}

In that case the plain child value wins and the base computed function is not called.

Value Pipeline Example

Value pipeline helpers are useful when a later layer must change a value that is computed after visible merge.

local netson = import '@primafuture/netson/lib/netson.libsonnet';

{
  dev: true,
  connection: netson.value.computed(function(config)
    if config.dev then
      {
        type: 'dialup',
        phoneNumber: '555-1234',
      }
    else
      {
        type: 'wifi',
        ssid: 'my-dev-wifi',
      }
  ),
}

A child can patch that computed object explicitly:

local netson = import '@primafuture/netson/lib/netson.libsonnet';

{
  dev: false,
  connection: netson.value.patch({
    keepAlive: true,
  }),
}

or with Jsonnet additive inheritance sugar:

{
  connection+: {
    keepAlive: true,
  },
}

Both produce:

{
  "type": "wifi",
  "ssid": "my-dev-wifi",
  "keepAlive": true
}

netson.value.finalize(function(args) ...) receives the value built so far and can return a transformed value. netson.value.replace(value) is an explicit hard replacement and resets previous pipeline operations at that path and below it.

Tagged Value Example

Netson also supports generic tagged values that stay opaque to the core runtime and carry the owning config layer in layered results.

local netson = import '@primafuture/netson/lib/netson.libsonnet';

{
  certPath: netson.value.tag.relativeToOwningLayer('./certs/dev.pem'),
  buildArchive: netson.value.tag.relativeToLastLayer('./.wtm-build-archive'),
}

result.visibleConfig

{
  "certPath": {
    "__netson_value__": {
      "tag": "relative-to",
      "value": {
        "base": "owning-layer",
        "value": "./certs/dev.pem"
      },
      "configLayer": {
        "id": "base"
      }
    }
  }
}

What this shows:

  • Netson preserves the tagged wrapper instead of interpreting it
  • configLayer.id points to the owning config layer for that final config path
  • relativeToOwningLayer(...) and relativeToLastLayer(...) are explicit sugar over relativeTo(base, value)
  • the standard relative-to tag uses value.base to tell the host what base to resolve against
  • owning-layer means the layer identified by configLayer.id
  • last-layer means the last layer in the ordered chain being resolved
  • if a host wants to treat these values as filesystem-relative paths, it must supply a concrete resolution base for the selected mode
  • if the chosen base cannot be resolved, for example a source-backed layer without a directory mapping, the host should fail explicitly instead of guessing
  • TypeScript hosts can use getNetsonRelativeToTaggedValue(...) to parse this standard payload shape without resolving it
  • if a child layer replaces the tagged value with a plain string, the wrapper disappears entirely
  • if a child layer replaces it with another tagged value, the child becomes the new owner

Cascade Model Example

You can also use the cascade engine directly inside Jsonnet when you already have the model, rules, and context in one evaluation.

local netson = import '@primafuture/netson/lib/netson.libsonnet';

local boardModel = netson.cascade.model({
  mode: 'merge',
  dimensions: ['target', 'profile'],
  keys: {
    kind: {
      contextPath: ['target', 'kind'],
      dimensionsSpecificityByMatcher: {
        equals: { target: 10 },
      },
    },
    profiles: {
      contextPath: ['profiles'],
      dimensionsSpecificityByMatcher: {
        containsAll: { profile: 10 },
      },
    },
  },
});

local boardRules = {
  defaults: netson.cascade.rule({
    effect: netson.cascade.patch({
      enabled: true,
      replicas: 1,
      transport: 'serial',
      legacyTransport: 'udp',
    }),
  }),

  service: netson.cascade.rule({
    match: [
      netson.cascade.match.equals({
        key: 'kind',
        value: 'service',
      }),
    ],
    effect: netson.cascade.patch({
      transport: 'http',
    }),
  }),

  prod: netson.cascade.rule({
    match: [
      netson.cascade.match.containsAll({
        key: 'profiles',
        value: ['prod'],
      }),
    ],
    effect: netson.cascade.patch({
      replicas: 3,
      legacyTransport: netson.value.unset,
    }),
  }),
};

{
  resolved: netson.cascade.resolve({
    model: boardModel,
    rules: boardRules,
    context: {
      target: { kind: 'service' },
      profiles: ['prod'],
    },
    initial: {},
  }),

  explained: netson.cascade.explain({
    model: boardModel,
    rules: boardRules,
    context: {
      target: { kind: 'service' },
      profiles: ['prod'],
    },
    initial: {},
  }),
}

resolved

{
  "enabled": true,
  "replicas": 3,
  "transport": "http"
}

The matching order is:

  • defaults applies first
  • service overrides transport
  • prod overrides replicas and removes legacyTransport

explained then gives you the structured trace:

  • which rules matched and which did not
  • matcher reasons
  • specificity contributions
  • step-by-step before / after changes
  • final value origin by path

Example Folders

The repository now contains a few ready-to-run example config sets:

  • examples/layered-config
    • numbered layers for plain visible merge behavior
  • examples/computed-values
    • computed-value and value-pipeline examples for final merged config dependencies
  • examples/deferred-cascade
    • layered config with a deferred cascade placeholder
  • examples/layered-cascade-rules
    • minimal hidden cascadeRules:: inheritance across base and child layers
  • examples/cascade-model
    • direct Jsonnet usage of cascade.resolve(...) and cascade.explain(...)
  • examples/css-like-cascade
    • CSS-like cascade specificity with id > class/pseudo > element
  • examples/tagged-values
    • layered config showing __netson_value__ wrappers and configLayer.id

Jsonnet Asset

Use the public asset from Jsonnet:

local netson = import '@primafuture/netson/lib/netson.libsonnet';

The asset exposes:

  • netson.deferred.placeholder(...)
  • netson.deferred.resolveAll(...)
  • netson.value.unset
  • netson.value.omit
  • netson.value.computed(...)
  • netson.value.patch(...)
  • netson.value.finalize(...)
  • netson.value.replace(...)
  • netson.value.tag.custom(...)
  • netson.value.tag.relativeTo(...)
  • netson.value.tag.relativeToOwningLayer(...)
  • netson.value.tag.relativeToLastLayer(...)
  • netson.cascade.model(...)
  • netson.cascade.rule(...)
  • netson.cascade.resolve(...)
  • netson.cascade.explain(...)
  • netson.cascade.patch(...)
  • netson.cascade.transform(...)
  • netson.cascade.validate(...)
  • netson.cascade.match.equals(...)
  • netson.cascade.match.inSet(...)
  • netson.cascade.match.containsAll(...)
  • netson.cascade.match.containsAny(...)
  • netson.cascade.match.prefix(...)
  • netson.cascade.match.exists(...)
  • netson.cascade.match.isMissing(...)
  • netson.cascade.match.isNull(...)
  • netson.cascade.match.predicate(...)
  • netson.cascade.match.custom(...)

Runtime API

resolveLayeredConfig(options)

Low-level layered resolve API.

import type {
  NetsonConfigLayer,
  NetsonJsonnetEvaluator,
  ResolvedLayeredConfig,
} from '@primafuture/netson';

Highlights:

  • accepts file-backed and source-backed layers
  • evaluates visible config layer-by-layer
  • can run an optional postMergeVisibleConfig transform before deferred resolution
  • composes helper root internally for deferred resolution
  • resolves deferred placeholders into deferredResolvedConfig
  • resolves value pipeline placeholders into deferredResolvedConfig
  • decorates tagged values in layered results with configLayer.id
  • accepts mergePolicy for host-specific atomic marker objects while keeping Netson's pipeline-aware layered merge

loadLayeredConfig(options)

High-level sugar over resolveLayeredConfig(...) that delegates chain discovery to your host adapter.

mergeJsonObjects(base, override, options?)

Default visible merge policy:

  • object + object => deep merge
  • netson.value.unset deletes the key
  • netson.value.omit stays as a visible marker and is pruned from the resolved config
  • null stays as null
  • arrays replace whole arrays
  • scalars replace
  • tagged wrappers replace atomically
  • deferred/computed placeholders replace atomically
  • pipeline placeholders replace atomically

Hosts that need additional atomic marker objects can pass a merge policy:

import { mergeJsonObjects, type NetsonMergePolicy } from '@primafuture/netson';

const mergePolicy: NetsonMergePolicy = {
  isAtomicObject: (value) =>
    typeof value === 'object' &&
    value !== null &&
    !Array.isArray(value) &&
    '__host_marker__' in value,
};

const merged = mergeJsonObjects(base, override, { mergePolicy });

The same mergePolicy option is available on resolveLayeredConfig(...) and loadLayeredConfig(...). It only adds host-specific atomic objects; Netson's own tagged values, unset markers, omit markers, deferred placeholders, and pipeline placeholders are always atomic.

mergeVisibleConfig is still supported as a legacy escape hatch when a host must fully own visible merging, but it bypasses Netson's pipeline-aware merge orchestration and cannot be combined with mergePolicy.

Unset helpers

import {
  createNetsonUnsetValue,
  isNetsonUnsetValue,
  NETSON_UNSET_MARKER_KEY,
} from '@primafuture/netson';

These helpers let a host create and detect the explicit delete sentinel in TypeScript.

Omit helpers

import {
  createNetsonOmitValue,
  isNetsonOmitValue,
  NETSON_OMIT_MARKER_KEY,
} from '@primafuture/netson';

netson.value.omit means resolve-time absence. It can remain in visibleConfig as { "__netson_omit__": true }, but deferredResolvedConfig prunes it from object fields. Array entries become null because removing array positions would change indexes.

Tagged value helpers

import {
  getNetsonRelativeToTaggedValue,
  getNetsonTaggedValue,
  isNetsonTaggedValue,
  NETSON_TAGGED_VALUE_MARKER_KEY,
} from '@primafuture/netson';

These helpers let a host detect __netson_value__ wrappers without parsing raw marker objects manually. getNetsonRelativeToTaggedValue(...) is a convenience parser for the standard relative-to payload shape { base, value }; it returns configLayer.id when present, accepts future non-empty base strings, and does not resolve paths.

explainNetsonCascade(options)

Thin TypeScript wrapper around Jsonnet netson.cascade.explain(...).

This is useful when the model and rules are JSON-like and you want the same structured explain shape from TypeScript.

Hanazuki Adapter

The official V1 runtime backend is @hanazuki/node-jsonnet.

import { createHanazukiJsonnetEvaluator } from '@primafuture/netson/hanazuki';

const jsonnet = createHanazukiJsonnetEvaluator({
  defaultImportRootDirectories: ['/path/to/project/node_modules'],
});

Host custom cascade matchers are supported through this adapter. If you pass customMatchers to Netson APIs, use this adapter or another evaluator that implements the same host native bridge capability.

Important Behavior

  • Netson is object-rooted. Each layer must evaluate to an object.
  • Delete is explicit through netson.value.unset; null remains ordinary data.
  • Resolve-time absence is explicit through netson.value.omit; it is not a merge-time delete.
  • Visible config merge and helper-root composition are separate artifacts.
  • Helper root is internal and is not exposed in the TypeScript result API.
  • netson.value.computed(...) receives the final lazy resolved config and writes its result to deferredResolvedConfig.
  • netson.value.patch(...) deep-merges object patches over the value built so far; arrays and scalars are not patchable.
  • netson.value.patch(...) treats { key: netson.value.omit } as removing key from the patched object, and patch(function(args) netson.value.omit) as a no-op patch.
  • netson.value.finalize(...) receives { value, missing, config, path, layer } and returns the next value for that path.
  • netson.value.replace(...) is an explicit hard replacement and resets pipeline state at that path and below it.
  • field+: { ... } can act as sugar for an object patch when helper-root composition proves Jsonnet additive inheritance was used.
  • Plain field: { ... } remains a hard replacement.
  • Deferred cascade params.context can contain netson.value.computed(...) values; they are resolved before cascade matching.
  • Tagged values stay opaque to Netson; only configLayer.id is injected by layered runtime APIs.
  • The built-in deferred resolver cascade uses helper-root composition internally, so hidden helpers survive across layer inheritance.
  • customMatchers are dispatched through the host runtime bridge and require the official adapter or an equivalent capable evaluator.