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

@spintax/core

v0.1.6

Published

Framework-agnostic spintax / text-spinning engine for JS & TS — turn one LLM-drafted {a|b|c} template into endless deterministic variations. Render, validate, extract. Zero-dependency; Node, Workers, browser.

Readme

@spintax/core

npm CI types license

Framework-agnostic spintax engine for JavaScript / TypeScript — parse, render, validate, extract, analyze, and neutralize spintax templates.

Pairs naturally with LLMs. Have a model draft a spintax template once, then generate unlimited variations on-device — deterministic, free, and offline, with no per-generation API calls. The LLM handles creativity; the engine handles scale.

  • Zero runtime dependencies. Runs unchanged on Cloudflare Workers, Node 18+, and in the browser.
  • ESM-first, dual CJS. Ships .d.ts types for both.
  • Parity-verified against the Spintax WordPress plugin. An independent TypeScript implementation (not a line-by-line port) held to the plugin's behavior contract by a shared golden corpus — the same fixtures pass against both this engine and the PHP plugin (deterministic verdicts, plural buckets, conditionals, #set collapse, post-process), with no divergence.
  • MIT licensed.

Status: released & stable. Feature-complete — parse / render / validate / extract / analyze / neutralize. The deterministic golden corpus passes against both this engine and the PHP plugin, and the §9.2 API is proven by a reference Cloudflare Worker (examples/worker).

Install

npm install @spintax/core

Quick start

import { render, validate, extract } from '@spintax/core';

render('{Hello|Hi|Hey} %name%!', { context: { name: 'Ada' }, seed: 42 });
// → "Hi Ada!"  (deterministic for a given seed; post-processed by default)

validate('{a|b');          // → [{ severity: 'error', code: 'bracket.unclosed', … }]
extract('%title% {?promo?Sale}'); // → { refs: ['title', 'promo'], sets: [], includes: [] }

Use cases

Anywhere one message has to go out many times without reading like a form letter:

  • Cold email & outreach — one template, a distinct body per recipient, personalized through context.
  • SMS / push / notifications — deterministic output in a few kilobytes, no API call per send.
  • Chatbots & agents — vary canned replies so a bot doesn't repeat itself verbatim.
  • A/B and multivariate testing — enumerate copy variants in the template instead of in application code.
  • Programmatic SEO / content generation — thousands of pages from one authored source.
  • LLM output at scale — have a model draft the template once, then spin it locally, forever, for free.

The core renders a single string per call — batching is a host concern. To emit N variants, call render N times with different seeds; a seeded call is reproducible, so any variant can be regenerated later from its seed alone:

const template = '{Hi|Hello|Hey} %name%, {quick|short|small} question about {pricing|billing}';
const variants = [1, 2, 3].map((seed) => render(template, { context: { name: 'Ada' }, seed }));
// → [ 'Hello Ada, quick question about billing',
//     'Hey Ada, quick question about pricing',
//     'Hey Ada, quick question about pricing' ]   ← same as seed 2; see the caveat below

Distinct seeds are independent draws, not distinct results — like any sampling they can repeat, and the fewer combinations a template has, the more often they will. If you need N unique variants, dedupe in the host and cap the retries: a template may simply not have N combinations to give.

Spintax syntax

| Construct | Example | Meaning | | --- | --- | --- | | Enumeration | {a\|b\|c} | pick one (nestable: {a\|{b\|c}}) | | Permutation | [a\|b\|c] | pick N, shuffle, join — [<minsize=1;maxsize=2;sep=", ">a\|b\|c] | | Variable | %var% | substitute a context value | | Local set | #set %v% = value | define a variable (one line) | | Conditional | {?VAR?then\|else} | then if VAR is truthy, else else | | Plural | {plural %n%: one\|few\|many} | grammatical agreement by locale | | Include | #include "slug-or-id" | embed another template (host-resolved) | | Comment | /# … #/ | stripped before rendering |

API

All functions accept a string or a parsed Ast (from parse) as their first argument.

render(input, options?): string

Renders a template to a single string. Lenient — never throws on malformed markup (a bad block is emitted verbatim with fullwidth braces {…}). Cosmetic post-processing (spacing, capitalization, URL/email shielding) is on by default.

render(input, {
  context?:         Record<string, string>,   // variable map
  seed?:            number | string,           // deterministic RNG; omit ⇒ random
  locale?:          string,                     // plural buckets, e.g. 'ru' (3-form)
  includeResolver?: (ref: string) => string | null,  // host-injected, synchronous
  postProcess?:     boolean,                    // default true; false ⇒ raw pick
  maxDepth?:        number,                      // #include / nesting guard (default 20)
});

#include is resolved only when you pass an includeResolver; child templates inherit the runtime context but not the parent's #set locals. Circular / too-deep includes resolve to '' (lenient).

validate(input, options?): Diagnostic[]

Returns diagnostics. A template is valid ⇔ no diagnostic has severity: 'error'. An unresolved %var% is a warning, not an error.

validate(input, {
  locale?:         string,             // locale-aware plural-arity verdicts
  knownIncludes?:  readonly string[],  // enables "unknown #include target" errors
  knownVariables?: readonly string[],  // suppresses undefined-variable warnings
});
// Diagnostic: { severity, code, message, line, column, endLine?, endColumn?, data? }

extract(input): { refs, sets, includes }

Variable references (%var%, {?…}, plural counts), #set names, and #include targets.

analyze(input, options?): Analysis

extract + validate + a best-effort constructs census ({ enumeration, permutation, variable, conditional, plural, set, include }). The census counts author-visible constructs; it is not a variant-cardinality promise.

parse(input): Ast

Parses once for reuse. Ast is opaque and versioned — pass it back to the other functions, do not introspect or persist it across engine versions.

neutralize(value): string

Shields data-derived (untrusted) text so it can't be re-interpreted as spintax markup — use it on any value you inject via context that isn't author-controlled. It is text-safe (round-trips to literal glyphs in any sink), not HTML/XSS escaping.

render('%bio%', { context: { bio: neutralize('Save {50|60}% today') }, postProcess: false });
// → "Save {50|60}% today"   (the braces stay literal, not a random pick)

Determinism & RNG

With a seed, render is reproducible within this engine. Cross-engine RNG-sequence parity with the PHP plugin is a non-goal — only the deterministic behavior (validation verdicts, plural buckets, conditional truthiness, #set collapse, post-process output) is parity-gated.

Links

License

MIT. The Spintax WordPress plugin remains GPL; MIT/Expat is GPL-compatible.


Part of the 301.st toolset. Product home: spintax.net.