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

@fieldsync/template-engine

v1.1.0

Published

Document generation engine — DOCX, XLSX, PPTX, PDF with a pipe-chain token grammar

Readme

@fieldsync/reporting

Document generation for Node.js. Pass a template file and a JSON data object, get back a formatted report in DOCX, PDF, XLSX, or PPTX. Document structure is entirely defined by your templates — no schema required.

The rendering engine handles filtering, sorting, aggregation, formatting, and conditional logic directly from template tokens. You bring raw data and a template; no application-level preprocessing is required.


How it works

  1. Create a template — a .docx, .xlsx, or .pptx file with placeholder tokens.
  2. Prepare your data — a plain JSON object whose keys match your tokens.
  3. Render — via the CLI, the render() API, or a mapper script.

The output format is inferred from the template extension. A .docx template produces both DOCX and PDF by default.


Requirements

  • Node.js 18+
  • Yarn 4

Installation

yarn add @fieldsync/reporting

Token syntax

See SYNTAX.md for the full token and pipe reference — scalar substitution, date formatting, pipe chains, loops, conditionals, images, and format-specific behaviour.

Quick reference

| Token | Description | |---|---| | {field} | Scalar substitution | | {field\|fallback} | Fallback when null/empty | | {field:date} | Date formatting | | {field\|pipe1\|pipe2:arg} | Pipe chain | | {#array}…{/array} | Loop | | {#field}…{/field} | Conditional (truthy) | | {^field}…{/field} | Conditional (falsy) | | {%field} | Image (size hints {%field:WxH} / {%field:W}) | | {%field:native\|portrait\|landscape\|original} | Image orientation (default native) |

All pipe verbs: where sort limit slice map count sum avg first last join eq ne gt gte lt lte contains and or not upper lower trim truncate append fixed round commas percent

Image orientation: Word ignores EXIF orientation and draws stored pixels, so phone photos land sideways. The engine bakes the rotation in — default native (show as shot). Override the default for all images with the imageOrientation render option, or per-image with a {%photo:portrait} hint. See SYNTAX.md for the full reference.


CLI

yarn render --template <path> --data <json-path> [options]

Options

| Flag | Description | |---|---| | --template <path> | Template file (.docx, .xlsx, .pptx) | | --data <path> | JSON file with raw template data | | --out <dir> | Output directory (default: ./output) | | --name <stem> | Output filename stem (default: data file stem) | | --docx | Produce DOCX output | | --pdf | Produce PDF output | | --xlsx | Produce XLSX output | | --pptx | Produce PPTX output |

When no format flag is given, the default is inferred from the template extension: .docx → DOCX + PDF, .xlsx → XLSX, .pptx → PPTX.

Examples

# DOCX + PDF (default for .docx template)
yarn render --template templates/report.docx --data data/site.json

# PDF only
yarn render --template templates/report.docx --data data/site.json --pdf

# XLSX
yarn render --template templates/report.xlsx --data data/site.json

# PPTX
yarn render --template templates/report.pptx --data data/site.json

# Custom output directory and filename
yarn render \
  --template templates/report.docx \
  --data data/site.json \
  --out reports/ \
  --name water-tower-Q2-2026

Programmatic API

render — unified entry point

Pass raw data directly — no preprocessing required.

import { render } from "@fieldsync/reporting";

const written = await render({
  template: "templates/inspection-report.docx",
  data: rawPayload,                         // plain JSON, no preprocessing
  outDir: "reports/",
  outName: "water-tower-Q2-2026",
  formats: ["docx", "pdf"],                 // optional — inferred from template if omitted
});
// written: string[] of output paths

Low-level renderers

import {
  renderTemplate,      // DOCX + PDF
  renderTemplateXlsx,  // XLSX
  renderTemplatePptx,  // PPTX
} from "@fieldsync/reporting";

await renderTemplate("templates/report.docx", data, "output/report.docx");
await renderTemplate("templates/report.docx", data, "output/report.pdf", { format: "pdf" });
await renderTemplateXlsx("templates/report.xlsx", data, "output/report.xlsx");
await renderTemplatePptx("templates/report.pptx", data, "output/report.pptx");

Token engine API

The pipe evaluation engine is exported for use in tests, custom tooling, or preprocessing:

import {
  parseToken,               // parse a token string into key + pipe list + fallback
  applyPipes,               // apply a pipe chain to a value
  evaluateSection,          // resolve a section tag → loop items or boolean
  substituteText,           // substitute all {tokens} in a string
  parseSectionOpen,         // parse a {#key|pipes} tag
  parseSectionClose,        // parse a {/key} tag
  resolveKey,               // resolve a dot-notation key from a context object
  injectLoopStatusShallow,  // inject _index/_count/_first/_last into an array
  formatDateVal,            // format a timestamp using a date variant string
  makePipedGetter,          // factory for docxtemplater-compatible getters
  PIPE_VERBS,               // Set<string> of all recognized pipe verb names
} from "@fieldsync/reporting";

Examples

// Resolve a value by dot-notation
resolveKey("report.status", data);                    // "IN REVIEW"

// Parse a token string
parseToken("findings|where:severity=High|count|0");
// { key: "findings", pipes: [where…, count], fallback: "0", dateVariant: null }

// Apply a pipe chain manually (pass the data context for and/or pipes)
applyPipes(data.findings, [
  { verb: "where", args: ["severity=High"] },
  { verb: "count", args: [] },
], data);
// → 2

// Check a cross-field condition
applyPipes(data.isApproved, [
  { verb: "and", args: ["license!=null"] },
], data);
// → true or false

// Evaluate a section tag against a data context
evaluateSection("findings", [{ verb: "where", args: ["requiresRepair=true"] }], false, data);
// → { type: "loop", items: [ ...items with _index/_count/_first/_last injected ] }

// Substitute all tokens in a text string
substituteText("{site.name|upper}  —  Status: {report.status|lower}", data);
// → "WATER TOWER A  —  Status: approved"

Data-layer utilities

Filtering, sorting, aggregation, date formatting, and null fallbacks are all handled by the pipe grammar directly in the template. These four helpers cover the remaining cases that require JavaScript — structural safety at the data boundary and conditions too complex for a pipe chain.

import { ensureArray, normalizeLoops, filterWhere, addFlags } from "@fieldsync/template-engine";

ensureArray(val)

Guard a single loop target that may arrive as null, undefined, or a single object instead of an array. The engine requires arrays for loop tags and will throw on anything else.

data.findings = ensureArray(raw.findings); // null → [], single obj → [obj]

normalizeLoops(data, keys)

Shorthand for calling ensureArray on multiple keys at once.

const safe = normalizeLoops(raw, ["findings", "equipment", "photos"]);

filterWhere(arr, predicate)

Split one source array into multiple named subsets for independent loops. A single where pipe filters in-place; filterWhere lets you produce two separately-named arrays from the same source.

data.openFindings   = filterWhere(findings, f => f.resolvedDate == null);
data.closedFindings = filterWhere(findings, f => f.resolvedDate != null);
// {#openFindings}…{/openFindings} and {#closedFindings} are now independent loops

addFlags(arr, flags)

Pre-compute boolean fields on array items for conditions that go beyond what and/or pipes can express — multi-field cross-array checks, regex, date math, etc.

data.findings = addFlags(rawFindings, {
  isEscalated: f => f.severity === "High" && f.daysOpen > 30 && !f.assignee,
});
// {#isEscalated}…{/isEscalated} works in your template

Full example — raw data, all logic in the template

import { render } from "@fieldsync/reporting";
import { readFileSync } from "fs";

// Raw data — no preprocessing
const data = JSON.parse(readFileSync("data/inspection.json", "utf8"));

// Template tokens do all the work:
//   {site.name|upper}                                    → uppercase site name
//   {findings|count}                                     → total count
//   {findings|where:severity=High|count}                 → high severity count
//   {findings|where:resolvedDate=null|count|0}           → open count (fallback 0)
//   {findings|avg:elevation|fixed:1|append: ft}          → "298.5 ft"
//   {equipment|map:tenant|join}                          → comma-separated tenant list
//   {#findings|sort:severity,elevation:desc|limit:10}    → sorted + sliced loop
//   {#isApproved|and:license!=null}                      → cross-field conditional
//   {#report.status|eq:APPROVED|or:report.status=SUBMITTED}
//   {resolvedDate:date|Open}                             → date or "Open" fallback
//   {description|truncate:200}                           → truncate long text
//   {complianceRate|percent:1}                           → "87.3%"

const paths = await render({
  template: "templates/inspection-report.docx",
  data,
  outDir: "output/",
  outName: "tower-A-Q2-2026",
});
console.log("Written:", paths);

Feature showcase

The repository includes templates and payloads that exercise every supported feature across all four formats:

# Build templates
yarn template:showcase       # templates/feature-showcase.docx
yarn template:showcase-xlsx  # templates/feature-showcase.xlsx
yarn template:showcase-pptx  # templates/feature-showcase.pptx

# Render with the default payload
yarn render:showcase         # output/ ← DOCX + PDF
yarn render:showcase-xlsx    # output/ ← XLSX
yarn render:showcase-pptx    # output/ ← PPTX

# Pipe unit tests (all 95 checks)
yarn test:pipes

# Render all five data variants and validate output (59 checks)
yarn test:variants

# Run both
yarn test

Test variants in data/variants/ exercise all conditional branches:

| Variant | What it tests | |---|---| | 01-approved.json | isApproved=true; resolvedFindings loop runs; all nulls filled (no fallbacks fire); all equipment have attachments | | 02-pending-empty.json | Inverted {^report.isInReview} PENDING banner; empty arrays (loop bodies run 0 times); null fallbacks fire | | 03-high-risk.json | All 5 findings High severity + requiresRepair=true; {#requiresRepair} fires 5×; {^requiresRepair} never fires | | 04-monitor-only.json | {^requiresRepair} fires 4×; {#requiresRepair} never fires; requiresRepair=true count = 0 | | 05-mixed-equipment.json | All equipment attachment states: 4-attachment inner loop, 1-attachment loop, empty {^attachments} |

See data/feature-showcase.json, scripts/test-pipes.ts, and scripts/test-variants.ts.


Custom mappers

When your source data needs structural transformation (e.g., splitting one raw array into two differently-named arrays for separate template loops), write a mapper script that shapes the data and calls render().

import { render } from "@fieldsync/reporting";
import { readFileSync } from "fs";

const raw = JSON.parse(readFileSync("data/payload.json", "utf8"));

// Shape the data however you need before passing to render()
const data = {
  ...raw,
  openFindings:   raw.findings.filter((f: any) => !f.resolvedDate),
  closedFindings: raw.findings.filter((f: any) =>  f.resolvedDate),
};

await render({ template: "templates/report.docx", data, outDir: "output/" });

Template generation scripts

The scripts/ directory contains one-off scripts that generate the template files under templates/. Run these to regenerate a template from scratch.

yarn template                # regenerate all showcase templates
yarn template:showcase       # templates/feature-showcase.docx
yarn template:showcase-xlsx  # templates/feature-showcase.xlsx
yarn template:showcase-pptx  # templates/feature-showcase.pptx

Project structure

fieldsync-template-engine/
├── data/
│   ├── feature-showcase.json         # Default showcase payload
│   ├── sample-data.json              # Minimal sample payload
│   └── variants/                     # Test data variants (one per conditional branch)
│       ├── 01-approved.json
│       ├── 02-pending-empty.json
│       ├── 03-high-risk.json
│       ├── 04-monitor-only.json
│       └── 05-mixed-equipment.json
├── fluent-templates/                 # Feature gap analysis docs
├── output/                           # Rendered files — gitignored
├── scripts/
│   ├── create-template-feature-showcase.ts
│   ├── create-template-showcase-xlsx.ts
│   ├── create-template-showcase-pptx.ts
│   ├── render-feature-showcase.ts
│   ├── render-showcase-xlsx.ts
│   ├── render-showcase-pptx.ts
│   ├── run-sample-report.ts
│   ├── test-pipes.ts                 # Unit tests for all pipe verbs (95 checks)
│   └── test-variants.ts              # Render all variants, validate XLSX output (59 checks)
├── src/
│   ├── cli/render.ts                 # CLI entry point
│   ├── renderers/
│   │   ├── render-template.ts        # DOCX + PDF (docxtemplater + pdfkit)
│   │   ├── render-template-xlsx.ts   # XLSX (exceljs)
│   │   └── render-template-pptx.ts  # PPTX (pizzip + XML)
│   ├── token-engine.ts               # Pipe grammar, shared by all renderers
│   ├── template-utils.ts             # Optional data-layer utilities
│   ├── render.ts                     # Unified render() dispatcher
│   └── index.ts                      # Public exports
├── templates/                        # Template files — gitignored
└── web/                              # Local browser UI (see Web Studio section)

Development

yarn typecheck         # type-check without emitting
yarn lint              # lint
yarn lint:fix          # lint and auto-fix

yarn template:showcase       # (re)build feature-showcase templates
yarn render:showcase         # render DOCX/PDF showcase
yarn render:showcase-xlsx    # render XLSX showcase
yarn render:showcase-pptx    # render PPTX showcase
yarn test:pipes              # 95 pipe unit tests
yarn test:variants           # render all 5 variants × 4 formats, validate output (59 checks)
yarn test                    # run both test suites

Web Studio

The web/ directory contains a local browser UI for uploading templates and data payloads, triggering renders, and downloading the resulting files — no CLI required.

Starting the studio

cd web
yarn dev

This starts two processes concurrently:

| Process | URL | Description | |---------|-----|-------------| | API server | http://localhost:3001 | Express — handles file uploads, render jobs, and output downloads | | Client | http://localhost:5173 | Vite + React UI |

Open http://localhost:5173 in your browser.

What you can do

  • Upload a template (.docx, .xlsx, or .pptx) or select one already in templates/
  • Upload a data payload (.json) or select one already in data/
  • Choose output formats (DOCX, PDF, XLSX, PPTX — availability depends on the template type)
  • Click Render — output files appear in the Output panel and are immediately downloadable
  • Regenerate templates — the ⟳ Regenerate button in the Templates panel runs yarn template to rebuild all showcase templates from their scripts

API endpoints (port 3001)

| Method | Path | Description | |--------|------|-------------| | GET | /api/templates | List template files | | POST | /api/templates/upload | Upload a template (multipart/form-data, field file) | | DELETE | /api/templates/:name | Delete a template | | POST | /api/templates/generate | Run yarn template to regenerate showcase templates | | GET | /api/data | List data payload files | | POST | /api/data/upload | Upload a data file (multipart/form-data, field file) | | DELETE | /api/data/:name | Delete a data file | | POST | /api/render | Render a template — body: { template, dataFile, formats[] } | | GET | /api/output | List rendered output files | | DELETE | /api/output/:name | Delete an output file | | GET | /output/:name | Download a rendered file |