Lightweight placeholder filling for strings, objects, and arrays.
placefill is for "fill data into templates" workflows. It is not a full template engine and it does not parse HTML into an AST.
- Deeply fills strings inside plain objects and arrays
- Supports path access like
user.name,items.0, andmatrix.1.2 - Preserves original value types for pure-placeholder strings like
{{count}} - Supports controlled synchronous function execution
- Supports lightweight tag templates for rich text assembly
- Keeps unknown tags untouched while still processing known child tags and placeholders
- Zero runtime dependencies
Use placefill when you need predictable, low-complexity template filling in production code:
- Transactional messages: order notifications, support ticket summaries, campaign copy
- Object payload generation: email payloads, push payloads, internal config objects
- CMS or content assembly: article snippets, CTA blocks, localized rich text fragments
- Compile-once render-many flows: i18n fragments, server-side repeated rendering, template registries
Skip placefill if you need:
- Conditionals, loops, async expressions, or arbitrary expression evaluation
- Full HTML parsing or AST transforms
- A full component or template language
# Before the package is published to npm
npm pack
npm install ./placefill-0.1.0.tgz
# or
pnpm add ./placefill-0.1.0.tgz
# or
bun add ./placefill-0.1.0.tgz
# After the package is published to npm
npm install placefill
# or
pnpm add placefill
# or
bun add placefillplacefill is designed for modern browser, Node.js, and edge-runtime environments that can run ES2020 output.
- No runtime dependencies
- Node.js
>=16.20 - Works well in frontend apps, server handlers, workers, and CLI scripts
- Keeps the API synchronous and predictable
import { fill } from "placefill";
const result = fill(
{
title: "Hello {{user.name}}",
firstItem: "{{items.0}}",
},
{
user: { name: "Lin" },
items: ["A", "B"],
},
);
console.log(result.value);
// {
// title: "Hello Lin",
// firstItem: "A"
// }When the same template is rendered repeatedly, use compile.
import { compile } from "placefill";
const template = compile('<Link href="/orders/{{order.id}}">{{user.name}}</Link>', {
tags: {
Link: '<a href="{{attrs.href}}">{{children}}</a>',
},
});
console.log(template.render({ order: { id: "A-100" }, user: { name: "Lin" } }).value);
console.log(template.render({ order: { id: "A-101" }, user: { name: "Qin" } }).value);
// <a href="/orders/A-100">Lin</a>
// <a href="/orders/A-101">Qin</a>This is especially useful for:
- Repeated server-side rendering
- i18n fragment reuse
- Template registries loaded at startup
- Rich text snippets rendered with different value sets
Pick the simplest entry point that matches the call pattern:
fill(objectOrArray, values): best for one-off payload assembly and deep template traversalfill(string, values): best for ergonomic one-shot string rendering; repeated string calls can reuse the shared compile cache viacacheSizecompile(template).render(values): best for hot paths, repeated rendering, tag-heavy rich text, and template registries loaded once then reused many times
If you use function-based tags and still want cache hits for fill(string, ...), provide a stable cacheKey.
import { fill } from "placefill";
const result = fill(
'Welcome <highlight>{{user.name}}</highlight>, open <link href="/orders/{{order.id}}">order</link>',
{ user: { name: "Lin" }, order: { id: "A-100" } },
{
tags: {
highlight: "<strong>{{children}}</strong>",
link: '<a href="{{attrs.href}}">{{children}}</a>',
},
},
);
console.log(result.value);
// Welcome <strong>Lin</strong>, open <a href="/orders/A-100">order</a>Tag behavior:
- Tag names are case-sensitive
- Supported names include
MyComp,ui.slot,x:label, andmy-comp - Unknown tags are preserved as-is
{{children}}and{{attrs.xxx}}always use double braces inside tag templates- If a tag fragment is malformed, the malformed fragment is preserved and other valid placeholders in the same string still continue to render
fill and compile().render() both return structured results:
| Field | Description |
|---|---|
value |
Rendered output |
ok |
true when there are no errors |
issues |
Collected warnings and errors |
stats |
Runtime stats such as placeholder count, replacements, warnings, errors, and duration |
Double braces are the default:
fill("Hello {{name}}", { name: "Lin" });double_braces keeps backward-compatible matching for direct keys such as {{full name}}, {{user:name}}, and {{用户}}.
Other formats must be enabled explicitly:
fill("Hello {name}", { name: "Lin" }, { format: "single_braces" });
fill("Hello ${name}", { name: "Lin" }, { format: "dollar_braces" });
fill("Hello %name%", { name: "Lin" }, { format: "percent" });For single_braces, dollar_braces, and percent, placeholders are restricted to path-like keys such as {user.name}, ${order.id}, or %campaign.slug%.
The repository includes realistic examples under examples:
order-confirmation-payload.ts: structured order confirmation payloadsshipment-status-rich-text.ts: shipment notifications with tag templates and attributessingle-braces-notification.ts: object-template notifications rendered with thesingle_bracesformatcampaign-banner-i18n.ts: compile once and render campaign banners for multiple audiencessupport-ticket-summary.ts: support ticket payload generation with badge strings and action routescms-article-fragment.ts: CMS article fragments, including attribute values containing>product-card-schema.ts: object templates that preserve native value typesinvoice-reminder-email.ts: invoice reminder emails with fallback defaultsonboarding-checklist-message.ts: onboarding messages with nested lists and linksunknown-tag-passthrough.ts: transparent unknown-tag passthroughdynamic-signature-block.ts: execute synchronous functions inside templates
Run them locally:
pnpm run examples
pnpm dlx tsx examples/campaign-banner-i18n.ts| Option | Type | Default | Description |
|---|---|---|---|
format |
PlaceholderFormat |
double_braces |
Placeholder syntax |
defaultValue |
string | number | boolean | null |
unset | Fallback value for missing placeholders |
onError |
collect | throw |
collect |
Error handling strategy |
executeFunctions |
boolean |
false |
Execute synchronous placeholder functions |
tags |
Record<string, TagTemplate> |
{} |
Tag template registry |
maxDepth |
number |
100 |
Maximum object/array traversal depth |
maxTagDepth |
number |
100 |
Maximum tag nesting depth |
treatUndefinedAsMissing |
boolean |
false |
Treat undefined as missing |
cacheSize |
number |
200 |
Shared string-template compile cache for fill(string, ...) |
cacheKey |
string |
unset | Explicit cache key when using function-based tags |
Notes:
valuesmust be a plain object, otherwise aFillErroris thrown- A pure placeholder string like
{{count}}can return a number, boolean, array, object,Date, or function result directly - Placeholder functions are synchronous only
- Ships both ESM and CJS builds
- Bundled output targets
ES2020 - Includes bundled type declarations
- Intended for current Node.js and evergreen browsers through modern bundlers
- No UMD or global browser build is shipped
For Node.js consumers:
- Package export maps support both
importandrequire prepublishOnlyrunscheck,test, andpack
For browser consumers:
- Use the ESM build through your bundler or native module pipeline
- The package is dependency-free, and the published output targets modern runtimes
- If you need older browser support, handle transpilation and polyfills in your own build pipeline
Benchmarks live in tests/bench/fill.bench.ts and are organized by user call pattern instead of internal function names. They use vitest bench with warmup, multi-sample runs, rotating datasets, and scenario inputs derived from the repository examples.
Included benchmark layers:
layer: micro: low-noise regression checks for short strings, fallback strings, tag-rich text, and object payloadslayer: realistic one-shot: low-reuse scenarios where each iteration includes compile cost, such as single-brace notifications, order-confirmation payloads, and invoice reminderslayer: realistic repeat: fixed-template repeated rendering scenarios such as support-ticket payloads, CMS article fragments, and campaign banners
Run benchmarks locally:
pnpm run benchHow to read the results:
- One-shot groups compare
fill(...)withcompile(...).render(...)while counting compile cost on every iteration - Repeated string groups compare
fill(cache off),fill(shared cache), andcompile.render(precompiled) - Repeated object groups compare
fill(...)withcompile.render(...)because the shared string cache does not apply to object templates
pnpm run check
pnpm test
pnpm build
pnpm run bench
pnpm run examplesMIT