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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@prompteng/core

v0.1.0

Published

Core package for PromptEng toolkit

Readme

@prompteng/core

PromptEng Core is a type-safe, testable prompt templating and rendering engine powered by LiquidJS. It is designed for safe user-authored templates, strong extensibility (custom tags/filters), and first-class testing with Vitest.

  • Safe, logic-light templating via LiquidJS
  • Custom tags/filters in TypeScript (e.g., sections, constraints)
  • Multi-output rendering (e.g., { system, prompt }) using {% section %}
  • Simple prompt tests (.ptest) with contains/not_contains assertions
  • Type generation from template frontmatter

Install

npm i @prompteng/core
# or
pnpm add @prompteng/core

Requires Node 18+.

Cloudflare Workers / Edge Runtimes

Use the worker-friendly engine that reads .ptemplate files from Cloudflare Static Assets. The DX is the same as Node: you pass a directory path.

Minimal Wrangler setup

Place your templates under public/prompts/templates/ and configure assets in wrangler.toml:

# wrangler.toml
name = "your-worker"
main = "src/index.ts"
compatibility_date = "2024-06-01"

[assets]
directory = "public"

Folder layout:

public/
  prompts/
    templates/
      with-system.ptemplate
      prompteng.manifest.json

Hono + Cloudflare Workers (recommended integration pattern)

import { Hono } from 'hono';
import { WorkerEngine, registerCloudflareAssets } from '@prompteng/core/worker';
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';

type Bindings = {
  ASSETS: { fetch: (r: Request) => Promise<Response> };
  OPENAI_API_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

// Middleware: register assets/manifest and attach engine to context
app.use('*', async (c, next) => {
  registerCloudflareAssets(c.env.ASSETS, (globalThis as any).__STATIC_CONTENT_MANIFEST);
  const engine = new WorkerEngine('/prompts/templates');
  c.set('prompteng', engine);
  await next();
});

// Example endpoint: render sections and call AI SDK
app.get('/some-endpoint', async (c) => {
  const engine = c.get('prompteng') as WorkerEngine;
  const sections = await engine.renderMulti('with-system', { name: 'Bob' });

  const openai = createOpenAI({ apiKey: c.env.OPENAI_API_KEY });
  const { text } = await generateText({
    model: openai('gpt-4o-mini'),
    system: sections.system,
    prompt: sections.prompt,
  });

  return c.json({ res: text });
});

export default app;

Notes:

  • Templates are NOT public by default. They’re accessible to your Worker code via env.ASSETS. Don’t proxy user requests to ASSETS.fetch unless you intend to expose assets.
  • If Wrangler doesn’t expose __STATIC_CONTENT_MANIFEST (local dev), create prompteng.manifest.json as shown above so the engine can enumerate your .ptemplate files.
  • Optional: if you don’t use CF assets (tests, special runtimes), you can register a virtual FS:
import { WorkerEngine, registerVfs } from '@prompteng/core/worker';

registerVfs({
  '/prompts/templates/greet.ptemplate': `---\nname: greet\nvariables: []\n---\nHello!`
});

const engine = new WorkerEngine('/prompts/templates');

If you prefer to access __STATIC_CONTENT directly (e.g., in production workers), use registerStaticContent(env.__STATIC_CONTENT, __STATIC_CONTENT_MANIFEST) instead of registerCloudflareAssets.

Local Hono (Node) example

  • examples/hono-basic/ shows how to use PromptEngine in a standard Hono app running under Node.
  • examples/worker-basic/ and examples/worker-hono/ demonstrate Cloudflare Workers setups.

Templates (.ptemplate)

Each template file uses YAML frontmatter for metadata and variables, followed by Liquid content. You can define multiple named outputs using the section block.

---
name: "email-welcome"
description: "Welcome email generator"
variables:
  - name: recipientName
    type: string
    required: true
  - name: productName
    type: string
    required: true
---
{% section 'system' %}
You are a helpful assistant writing concise, friendly emails.
{% endsection %}

{% section 'prompt' %}
Write a short welcome email for {{ recipientName }} to introduce {{ productName }}.
End with a friendly call-to-action.
{% endsection %}

Tests (.ptest)

Prompt tests are YAML files that reference a template by name and define test cases. Assertions validate the final rendered output returned by the provider.

template: "email-welcome"
description: "Welcome email basic test"

providers:
  - name: "mock"
    model: "mock-1"

test_cases:
  - name: "Simple welcome"
    variables:
      recipientName: "Ada"
      productName: "Acme"
    assertions:
      - contains: "Acme"
      - not_contains: "unsubscribe"

Usage

Render a single output string:

import { PromptEngine } from '@prompteng/core';

const engine = new PromptEngine('prompts/templates');
const text = await engine.render('email-welcome', {
  recipientName: 'Ada',
  productName: 'Acme'
});

Render multiple sections with metadata (e.g., { system, prompt }):

const { sections } = await engine.renderWithMeta('email-welcome', {
  recipientName: 'Ada',
  productName: 'Acme'
});
// sections.system, sections.prompt

Or directly get a name->text map:

const out = await engine.renderMulti('email-welcome', { recipientName: 'Ada', productName: 'Acme' });
// { system: string, prompt: string }

Which API to use

  • Use engine.render(name, vars) when your template does not define sections or you only need the combined text output.
  • Use engine.renderWithMeta(name, vars) when you need both the combined text and metadata:
    • returns { text, sections, constraints }
    • sections is a map of named outputs captured via {% section %}...{% endsection %}
    • constraints contains structured info for testing/validation
  • Use engine.renderMulti(name, vars) when your caller expects a map of sections and you don't need constraints:
    • always returns a map and falls back to { prompt: text } if no sections are present.

Templating guide (self‑contained)

PromptEng uses a Liquid-compatible syntax, but you don’t need the Liquid docs to get started. This guide summarizes what you’ll use 90% of the time when writing .ptemplate files.

Cheatsheet

{{ variable }}                  # print a variable
{{ var | default: 'fallback' }} # filters mutate output
{%- assign x = 1 -%}            # assign a value (whitespace-trimmed)
{%- capture buf -%}...{%- endcapture -%}  # capture rendered text into a variable

{% if cond %}...{% elsif other %}...{% else %}...{% endif %}
{% case kind %}{% when 'a' %}...{% when 'b' %}...{% else %}...{% endcase %}
{% for item in list %}{{ forloop.index }}. {{ item }}{% endfor %}

{% section 'name' %}...{% endsection %}  # capture a named output (e.g., system, prompt)
{% render 'partial.liquid' %}            # include a partial file (optional)

Whitespace control: use -{% and %}- (or {{-/-}}) to trim spaces/newlines around tags.

Variables and filters

Hello, {{ userName | default: 'friend' }}!
You picked: {{ items | uniq | join: ', ' }}
Count: {{ items | length }}
Lower: {{ brand | lower }}  Upper: {{ brand | upper }}

Built‑in helper filters (shipped by PromptEng):

  • join, uniq, default, length, lower, upper, compact, sort

Control flow

{% if plan == 'pro' %}
  Thanks for being a Pro user!
{% elsif plan == 'free' %}
  You’re on the free plan.
{% else %}
  Welcome!
{% endif %}

{% case locale %}
  {% when 'en' %}Hello
  {% when 'es' %}Hola
  {% else %}Hi
{% endcase %}

assign and capture

{%- assign tone = tone | default: 'friendly' -%}

{%- capture style -%}
{% case tone %}
  {% when 'friendly' %}Warm and concise.
  {% when 'professional' %}Precise and respectful.
  {% else %}Neutral.
{% endcase %}
{%- endcapture -%}

Style: {{ style | strip }}

Loops and helpers

{% for w in words %}
  {{ forloop.index }}. {{ w }}
{% endfor %}

{%- assign top3 = words | uniq | sort -%}
Top picks: {{ top3 | join: ', ' }}

Partials (optional)

If you organize fragments into partial files, you can include them:

{% render 'partials/header.liquid' %}

Sections (multi‑output)

Use sections to produce multiple named outputs in one render (e.g., system, prompt). They do not emit inline text; the engine captures their content.

{% section 'system' %}
System instruction here.
{% endsection %}

{% section 'prompt' %}
User‑facing prompt here.
{% endsection %}

Read them back with renderWithMeta() or renderMulti().

Constraint tags

PromptEng includes an example constraint tag you can use and extend:

  • {% must_include_each words %}
    • Adds a clear instruction for the model.
    • Records the requirement in metadata so test tools can validate later.

Example:

{% must_include_each primaryTerms %}

Custom filters and tags

Register filters:

engine.registerFilter('truncate_words', (s: unknown, n = 12) => {
  const words = String(s ?? '').split(/\s+/);
  return words.length <= n ? words.join(' ') : words.slice(0, n).join(' ') + '…';
});

Register class‑based tags (extends Tag):

import { Liquid, Tag, TagToken, TopLevelToken, Context, Value } from 'liquidjs';

class UpperTag extends Tag {
  private value: Value;
  constructor(token: TagToken, remain: TopLevelToken[], liquid: Liquid) {
    super(token, remain, liquid);
    this.value = new Value(token.args, liquid);
  }
  *render(ctx: Context) {
    const v = yield this.value.value(ctx);
    return String(v).toUpperCase();
  }
}

engine.registerTag('upper', UpperTag);

Quick Reference

  • Variables

    • Print: {{ name }}
    • With default: {{ name | default: 'N/A' }}
    • Assign: {% assign x = 1 %} (use {%-/-%} to trim whitespace)
    • Capture: {% capture buf %}...{% endcapture %}
  • Control flow

    • If: {% if cond %}...{% elsif other %}...{% else %}...{% endif %}
    • Case: {% case kind %}{% when 'a' %}...{% else %}...{% endcase %}
    • Loop: {% for it in list %}{{ forloop.index }}. {{ it }}{% endfor %}
  • Sections (multi-output)

    • {% section 'system' %}...{% endsection %}
    • Read via engine.renderWithMeta() or engine.renderMulti().
  • Built-in helper filters

    • default, join, uniq, length, lower, upper, compact, sort
  • Constraints (example)

    • {% must_include_each words %} — instructs and records metadata for tests.
  • Partials (optional)

    • {% render 'partials/header.liquid' %}

Cookbook

1) Tone/style switch with assign + capture + case/when

{%- assign tone = tone | default: 'friendly' -%}
{%- capture style -%}
{% case tone %}
  {% when 'friendly' %}Warm and concise.
  {% when 'professional' %}Precise and respectful.
  {% else %}Neutral.
{% endcase %}
{%- endcapture -%}
Style: {{ style | strip }}

2) Multi-output prompts (system + prompt)

{% section 'system' %}
You are a helpful assistant.
{% endsection %}

{% section 'prompt' %}
Write a short note for {{ userName | default: 'friend' }}.
{% endsection %}
const out = await engine.renderMulti('template-name', { userName: 'Ada' });
// out.system, out.prompt

3) List processing (uniq, sort, join)

{%- assign picks = terms | default: [] | uniq | sort -%}
Use these: {{ picks | join: ', ' }}

4) Partials

{% render 'partials/header.liquid' %}
Main content...
{% render 'partials/footer.liquid' %}

5) Simple .ptest for contains/not_contains

template: "email-welcome"
description: "Smoke test"
providers:
  - name: "mock"
    model: "mock-1"
test_cases:
  - name: "Basic"
    variables: { recipientName: "Ada", productName: "Acme" }
    assertions:
      - contains: "Acme"
      - not_contains: "unsubscribe"

Generate Types

From template frontmatter, PromptEng can generate TypeScript types for variables:

import { PromptEngine } from '@prompteng/core';
const engine = new PromptEngine('prompts/templates');
const dts = engine.generateTypeDefinitions();

Testing (Vitest)

pnpm -F @prompteng/core test

Internally, tests use a TestRunner that:

  • Loads .ptest files
  • Renders templates with a provider (mock or real)
  • Validates contains and not_contains assertions

Publishing

This repo uses a unified CI & Release workflow with Changesets (see .github/workflows/ci-release.yml). When a Changesets “Version Packages” PR is merged into main, the pipeline will:

  • Install deps, build the workspace, and run tests (Vitest)
  • Version packages and generate changelogs via Changesets
  • Publish updated packages to npm (@prompteng/core, @prompteng/cli, etc.)

Setup required:

  • Add NPM_TOKEN as a repository secret for publishing.

Developer flow:

  • Run pnpm changeset to create a changeset describing your changes and bump type.
  • Commit and push; CI opens/updates a “Version Packages” PR.
  • Merge that PR to main to publish to npm.

License

MIT