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

@arslankhan/doc-forge

v0.1.8

Published

Document template model, JSON data binding, HTML and PDF rendering, and a visual designer for Node.js and React.

Readme

@arslankhan/doc-forge

Reusable document template library for Node.js and React. It defines a versioned template model, checks templates before they are saved or used, converts them to and from JSON, resolves bindings against JSON data, calculates invoice totals, prepares grouped reports, renders standalone HTML, and provides a visual designer.

The host application supplies a template and decides where the saved template is stored. Binding paths stay on the template. Preview data, calculated amounts, and resolved text are separate and are not written back into the saved template. PDF export is a separate Node.js entry point, @arslankhan/doc-forge/pdf, and is not part of the React designer bundle. License state is also separate from template JSON. Until a license server is configured, activation stays unconfigured and the existing features keep working.

The published package name is @arslankhan/doc-forge. The unscoped name doc-forge is too similar to the existing npm package docforge, so npm will not accept it. The website, price, and license API URL are not part of this package.

Installation

The package is ESM and requires Node.js 18 or newer. CommonJS require() is not a supported entry point. A CommonJS caller on Node.js 18 or newer can load it with await import("@arslankhan/doc-forge"). TypeScript declarations are included. Source maps point at the original files and do not embed a private key.

npm install @arslankhan/doc-forge

Install the designer peers in a React application:

npm install @arslankhan/doc-forge react react-dom

Install the PDF peers in the Node.js application that prints documents:

npm install @arslankhan/doc-forge playwright pdf-lib
npx playwright install chromium

react and react-dom are optional peers because a Node.js program can render HTML and PDF without them. playwright is an optional peer used only by @arslankhan/doc-forge/pdf. pdf-lib is a runtime dependency and is installed with the package. The React designer entry does not import Playwright. A browser build of @arslankhan/doc-forge/pdf resolves to a stub that does not import Playwright either.

Public entry points:

| Import | Runtime | | --- | --- | | @arslankhan/doc-forge | Node.js and browser-safe core: templates, binding, invoices, reports, HTML, license helpers | | @arslankhan/doc-forge/designer | React designer. Requires react and react-dom | | @arslankhan/doc-forge/license | The same license client as the package root, for hosts that do not want the document API | | @arslankhan/doc-forge/pdf | Node.js PDF export |

Deep imports of dist/ are not part of the public API.

Current capabilities

  • Document templates for generic, invoice, and report, marked with schemaVersion
  • Page setup: paper size, orientation, dimensions, margins, and fixed or flow layout
  • Elements: text, image, line, rectangle, repeating items, totals, and reportTable
  • Data binding paths such as company.name, customer.address.city, and items.0.name
  • Safe resolution of those paths against a JSON object, including fallback, prefix, and suffix
  • Validation that returns a list of errors
  • JSON serialization, deserialization, and deep cloning
  • HTML rendering of one page to a standalone document, using the same text resolver
  • Node.js PDF export through Playwright Chromium, from the same HTML pipeline (@arslankhan/doc-forge/pdf)
  • DocumentDesigner for visual editing: toolbox, page canvas, properties, sample-data preview, HTML preview, zoom, undo, and save
  • License activation and entitlement checks. Enforcement is off until a host configures it, so these features stay available

Supported schema version: 1.

Usage

The package is ESM and requires Node.js 18 or newer.

import {
  cloneTemplate,
  createTemplate,
  deserializeTemplate,
  exampleInvoiceTemplate,
  serializeTemplate,
  validateTemplate,
} from "@arslankhan/doc-forge";

const template = createTemplate(exampleInvoiceTemplate);
const json = serializeTemplate(template);
const copy = cloneTemplate(deserializeTemplate(json));

const result = validateTemplate(copy);
if (!result.valid) {
  console.error(result.errors);
}

createTemplate, serializeTemplate, and deserializeTemplate throw TemplateValidationError when the template is invalid. validateTemplate returns { valid, errors } instead of throwing.

Data binding

A text element without binding shows its text value. A text element with binding keeps that same text as the design placeholder and reads a value from JSON when you resolve or preview it.

An image element without binding uses its static src. An image element with binding keeps that src on the saved template and reads the URL from JSON instead.

{
  type: "text",
  text: "Customer Name",
  binding: {
    path: "customer.name",
    fallback: "Customer Name"
  }
}

prefix and suffix are optional. Leave them out when they are blank. fallback is optional too.

Path syntax

A path is a list of segments separated by dots. The first segment is a name such as company. Later segments are names or array indexes, such as customer.address.city or items.0.name. Indexes do not use brackets and cannot have leading zeros (items.01 is invalid).

These segments are rejected: __proto__, prototype, and constructor. Paths are read with property lookup. They are not expressions, and the library never calls eval.

Resolution rules

resolveBinding(data, path) returns { status: "found", value } or { status: "missing", reason }.

  • 0, false, and "" are found values.
  • A missing property is missing with reason absent.
  • null is missing with reason null.
  • An invalid path or a blocked segment does not read the object.

resolveTemplateData(template, data) copies the template layout, fills resolvedText on each text element, and fills resolvedSrc on each image element. It does not change the template or the data, and it does not request image URLs. The result is not a saved template: it contains preview fields, and validateTemplate rejects it.

Display conversion, with no currency or date formatting:

  • Strings are shown unchanged.
  • Numbers use their plain decimal form: 1250.5 stays 1250.5, and 0 stays 0.
  • Booleans become true or false.
  • Objects and arrays are not shown. They use the fallback, or an empty string when no fallback is set.

When the path is missing, null, or not a scalar:

  • If fallback is set, the preview shows prefix + fallback + suffix.
  • If fallback is omitted, the preview is an empty string and prefix and suffix are not applied.

A present scalar, including an empty string, is shown as prefix + value + suffix and does not use the fallback.

The same saved template can be resolved with different JSON objects. exampleBoundInvoiceTemplate binds company.name, company.address, customer.name, customer.address.city, invoice.number, invoice.date, and invoice.total.

import {
  deserializeTemplate,
  exampleBoundInvoiceTemplate,
  exampleInvoiceData,
  exampleInvoiceDataAlternate,
  resolveTemplateData,
  serializeTemplate,
} from "@arslankhan/doc-forge";

const saved = serializeTemplate(exampleBoundInvoiceTemplate);
const template = deserializeTemplate(saved);

const first = resolveTemplateData(template, exampleInvoiceData);
const second = resolveTemplateData(template, exampleInvoiceDataAlternate);

const company = first.elements.find((element) => element.id === "company-name");
if (company?.type === "text") {
  console.log(company.resolvedText); // Smartbooks
}

exampleInvoiceDataAlternate supplies a different company, customer, and invoice. Its total is 0, which is shown as 0 rather than the fallback.

Image sources

{
  type: "image",
  src: "assets/logo.png",
  binding: {
    path: "company.logo",
    fallback: "assets/logo.png"
  }
}

company.logo, customer.photo, product.image, and organization.branding.logo are normal paths. The bound value is used only when it is a non-empty string. Missing paths, null, blank strings, numbers, booleans, objects, and arrays use fallback when that fallback is a non-empty string. With no usable fallback, the image is missing. prefix and suffix are not added to an image URL.

src is the static URL. It is used only when the element has no binding. Removing the binding returns to that URL. The resolved URL is not written back onto the template.

exampleInvoiceData includes illustrative URLs such as https://example.com/smartbooks-logo.png. Those addresses document the binding; they are not claims that a file is hosted there. exampleInvoiceDataAlternate uses a different logo URL for the same company.logo binding.

Preview JSON in the designer

Open Preview data in the toolbar. Paste a JSON object, or choose Load example, then Apply preview. The canvas shows resolved text and keeps the current layout. Editing the JSON while preview is active updates the page when the JSON is valid. Invalid JSON shows an error and leaves the last valid preview on the page.

Exit preview returns to the placeholder text. Dragging, resizing, undo, and the property panel keep working during preview. Save stores the template and its binding configuration. It does not store the preview JSON or the resolved strings.

In the text properties, choose Data binding, enter a path, and set a fallback. The panel reports whether the path is valid. Static text removes the binding and leaves position, size, and formatting in place. A blank fallback, prefix, or suffix is omitted from the template.

In the image properties, choose Static URL or Bind to JSON data. The binding fields are the path, such as company.logo, and a fallback image URL. The panel reports an invalid path, a missing value, and a source the preview will not display. Static URL removes the binding and edits src. During preview, the canvas shows the resolved image. Editing mode keeps the image placeholder.

HTML rendering

renderToHTML lives on the package root, so a Node.js program can render a template without loading React. It validates the template, resolves text with resolveTemplateData, and returns an HTML string. The template and the data object are not modified.

import {
  exampleBoundInvoiceTemplate,
  exampleInvoiceData,
  renderToHTML,
} from "@arslankhan/doc-forge";

const html = renderToHTML(exampleBoundInvoiceTemplate, exampleInvoiceData, {
  imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
});
console.log(html);

example:html writes that document to examples/bound-invoice.html after building the library:

npm run example:html

The result is a complete HTML document by default. Pass { document: "fragment" } for the style element and page only. { title } sets the document title. { additionalCss } appends trusted CSS after the generated rules. That string is not checked as a stylesheet, and it is rejected when it contains <, so it cannot close the style element. Do not pass CSS from untrusted users.

Page size and coordinates

Positions, sizes, margins, border widths, and corner radii use the page unit (mm, pt, in, or px). A4 portrait is 210mm by 297mm. Landscape swaps those dimensions when the template stores the matching page size. Explicit page.dimensions are used when present. Font sizes are points, matching the designer. Coordinates start at the top-left of the page, which is also how the designer places elements. The margin rectangle is a guide; it does not shift those coordinates. @page { margin: 0 } keeps the printed page the same size as the template. The schema has one page, so the renderer emits one page. fixed and flow both place elements at their stored coordinates.

Elements

  • text: static text, bound text, fallback, prefix, and suffix. Font family, size, weight, italic, color, alignment, line height, background, border, radius, opacity, width, height, and position. Text wraps inside the box and overflow is hidden. Underline, padding, and vertical alignment are not in the schema.
  • image: static src, or a bound source from binding.path, with alt and the same box and border properties. A missing or rejected source becomes a placeholder that shows the alt text. Images use object-fit: contain. Position and size stay in the page unit.
  • line: position, span, stroke color, and stroke width. A zero width or height is a vertical or horizontal line, and the stroke width supplies the cross-axis size.
  • rectangle: position, size, background, border, radius, and opacity. Borders are solid.

An element type the renderer does not know throws RenderError. Elements are not dropped.

Missing values

The renderer uses the same rules as resolveTemplateData. For text, a missing, null, or non-scalar binding shows fallback when it is set, including its prefix and suffix. With no fallback, the text is empty. 0, false, and "" are shown. Numbers stay in plain decimal form.

For an image, only a non-empty string is a source. Anything else uses the image fallback when that fallback is a non-empty string. A bound URL that the image policy rejects also tries that fallback. If neither source is allowed, the placeholder is used and the rejected URL is not written into the HTML.

Image URLs

Relative paths such as assets/logo.png are emitted by default. https URLs and data:image/(png|jpeg|jpg|gif|webp);base64,... URLs are omitted unless the call opts in:

renderToHTML(template, data, {
  imageSources: {
    allowHttps: true,
    httpsHosts: ["cdn.example"],
    allowDataImages: true,
  },
});

javascript:, vbscript:, file:, and non-image data: URLs are never emitted, whether they come from src, a bound value, or a fallback. A javascript: value in src or in an image fallback also fails template validation. http URLs are omitted. Rejected sources are replaced with an allowed fallback or the placeholder, and the rejected URL is not copied into the HTML. Bound text is escaped and is never parsed as HTML. Image sources are escaped as attributes and are not treated as HTML. The document sets script-src 'none'. The renderer does not add event-handler attributes.

The designer HTML preview and canvas preview allow https, relative paths, and image data URLs so a bound logo can be seen. They still refuse the dangerous schemes above. A Node.js caller uses the stricter default unless imageSources opts in.

Invalid templates throw TemplateValidationError, including non-finite dimensions. Unsafe renderer options throw RenderError.

HTML preview in the designer

Choose HTML in the toolbar to show the rendered document in a sandboxed iframe. The iframe does not allow scripts or the parent origin. Design returns to the canvas without discarding edits. Preview data supplies the JSON used by both the canvas and the HTML view. Without preview data, bindings resolve as missing and fallbacks are used.

Invoices

An invoice template is a normal template with documentType: "invoice". It uses the same page, elements, and bindings. generic and report templates do not run invoice arithmetic, so they render as before.

renderToHTML prepares a copy of the JSON before resolving bindings. That copy adds calculated fields and leaves the original object unchanged. The same saved template can render different invoices by passing different JSON objects.

import {
  exampleFullInvoiceData,
  exampleFullInvoiceTemplate,
  renderToHTML,
} from "@arslankhan/doc-forge";

const html = renderToHTML(exampleFullInvoiceTemplate, exampleFullInvoiceData, {
  imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
});

exampleFullInvoiceTemplate includes a logo bound to company.logo, company and customer text, invoice number and dates, an items table, notes, and totals. exampleFullInvoiceDataAlternate is a second dataset for that template. The sample logo URLs are illustrative and are not hosted files. Smartbooks and AED are sample data, not engine defaults.

JSON contract

company, customer, and invoice are optional objects. Missing optional details stay blank. invoice.items is the line-item array. Each item needs numeric quantity and rate. description and unit are strings. An item may include taxRate and discount: { type: "percentage" | "fixed", value }. The invoice may include discount in that same shape, taxRate, currency, number, date, dueDate, and notes.

Dates used for formatting are YYYY-MM-DD strings. A percentage is a number from 0 to 100, so 10 means 10 percent. Quantities, rates, discounts, and tax rates cannot be negative. A missing quantity or rate throws InvoiceCalculationError instead of becoming zero. An empty or omitted items array is a zero invoice.

calculateInvoice(data, policy?) returns line amounts and totals. prepareInvoiceContext(data, policy?) returns the binding copy. policy.fractionDigits defaults to 2 and may be 0 through 6. A template can save the same setting as invoice.fractionDigits. renderToHTML(..., { invoice }) overrides that saved setting.

Calculated names on the copy, which are not written back to the input:

  • invoice.subtotal, invoice.discountAmount, invoice.taxableAmount, invoice.taxTotal, invoice.grandTotal, invoice.totalQuantity
  • each item's grossAmount, discountAmount, netAmount, lineTotal, taxAmount, taxableAmount

lineTotal is the line amount after a line discount and before the invoice discount and tax.

Calculation order

  1. Line gross is quantity × rate, rounded to the currency fraction digits.
  2. A line discount, when present, is applied to that gross amount.
  3. The subtotal is the sum of those net line amounts.
  4. The invoice discount is applied once to the subtotal. A fixed discount cannot exceed the subtotal, and a percentage is 0 to 100.
  5. That discount is shared across lines in proportion to their net amounts. The last line receives the rounding remainder.
  6. Tax is calculated on each line after that share. An item taxRate wins; otherwise invoice.taxRate is used; otherwise tax is 0.
  7. Grand total = subtotal − invoice discount + tax.

Rounding is half away from zero at each monetary step. This is a configurable policy, not a country tax filing rule. Amounts at or above 1,000,000,000,000 are rejected. The input JSON is not modified.

Items table and totals

Add Items or Totals from the toolbox. The items element stores itemsPath (usually invoice.items) and columns. A column header is static text. Its binding path is relative to the current item, such as description, quantity, unit, rate, or lineTotal. Column width is a relative weight. align controls horizontal alignment. Currency and decimal columns default to the right. visible: false hides a column.

The table uses the element font, border, and background. padding is cell padding in the page unit. headerStyle styles the header. A missing or empty array shows emptyText, which defaults to "No items". The table stays inside its box and does not add pages.

Totals rows bind to root paths such as invoice.grandTotal. hideWhenZero omits a row whose numeric value is 0, which is how the sample hides a zero discount or tax line.

Number and date display

A binding or column can set format:

{ style: "currency", minimumFractionDigits: 2, maximumFractionDigits: 2 }
{ style: "decimal", maximumFractionDigits: 2 }
{ style: "date", dateStyle: "medium" }

locale is optional and falls back to en when Intl rejects it. Currency style uses format.currency when set. Otherwise it reads invoice.currency from the data. A missing or invalid currency code is shown as a decimal number, without a hardcoded symbol. Formatting is display only. An unparseable date uses the binding fallback, or the original text when no fallback is set.

Designer

React 18 or newer is required. The core template API, including exampleInvoiceTemplate, stays on the package root. The designer is a separate entry so Node code can load the schema without React. Import it from @arslankhan/doc-forge/designer. @arslankhan/doc-forge/dist/designer resolves to the same files.

import { exampleInvoiceTemplate } from "@arslankhan/doc-forge";
import { DocumentDesigner } from "@arslankhan/doc-forge/designer";

const sample = {
  company: { name: "", logo: "" },
  invoice: {
    code: "",
    date: "",
    details: [{ itemName: "", qty: 1, rate: 2 }],
    totals: [{ totalExclusive: 0 }],
  },
};

export function Editor() {
  return (
    <DocumentDesigner
      template={exampleInvoiceTemplate}
      data={sample}
      onSave={(saved) => {
        console.log(saved.template);
        console.log(saved.html);
      }}
    />
  );
}

data is a sample used to build the binding dropdowns. It is not preview data and it is not written into the template. generateHTML still receives the runtime payload separately. An empty array can describe its item through bindingSchema.arrayItems, keyed by the collection path.

Save returns { template, html }. template is the editable document. html keeps each binding as a template expression, such as ${company.name} or ${invoice.details[0].description}. Image dimensions, fonts, alignment, padding, and borders come from the saved element. generateHTML(dataPayload, generatedHTML) returns the populated HTML string. It fills root expressions such as ${company.name} and repeats an items table's first body row for every record at data-items-path. Row fields such as ${description} read the current record, and a path that is not on that record is read from the document. Text is escaped. The function does not open a browser or create a PDF. Preview and PDF still use renderToHTML.

import { createDocumentSaveResult, generateHTML } from "@arslankhan/doc-forge";

const saved = createDocumentSaveResult(template);
const finalHTML = generateHTML(
  {
    company: { name: "Example Company", logo: "https://example.com/logo.png" },
    invoice: { code: "INV-001", items: [] },
  },
  saved.html,
);

The toolbar includes a compact theme control: Light, Dark, or System. System follows the operating system and updates when that preference changes. The choice is stored in localStorage under doc-forge-theme and defaults to System. It is interface state only. Switching themes does not edit the template, and the document page stays white unless the template sets another background. HTML preview and PDF export use the template colors.

The designer edits one page. The toolbar sets the document type to generic, invoice, or report. New elements are text, image placeholders, lines, rectangles, an items table, a totals block, and a report table. Save returns a validated copy of the template together with the binding HTML. Invoice preview calculates totals from the preview JSON. Report preview groups the preview JSON. Neither writes those results back to the template. With the document type set to Report, the page panel edits the report title, description, page header, page footer, report header, and report footer.

Run the local demo with npm run demo, then open the URL Vite prints.

Reports

A report is a normal template with documentType: "report". Page size, orientation, and margins stay on page. Report title, description, and header or footer bands are optional report settings. A reportTable element repeats one row per record. Schema version remains 1.

import {
  exampleJobsReportData,
  exampleJobsReportDataAlternate,
  exampleJobsReportDataLarge,
  exampleJobsReportTemplate,
  renderToHTML,
} from "@arslankhan/doc-forge";

const html = renderToHTML(exampleJobsReportTemplate, exampleJobsReportData, {
  imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
});
renderToHTML(exampleJobsReportTemplate, exampleJobsReportDataAlternate, {
  imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
});
renderToHTML(exampleJobsReportTemplate, exampleJobsReportDataLarge, {
  imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
});

The sample data object has optional report metadata, optional company details, and the record collection:

{
  "report": { "title": "Jobs Report", "generatedAt": "2026-10-14", "branchName": "Dubai Branch", "currency": "AED" },
  "data": { "jobs": [{ "jobNumber": "JOB-001", "customer": "ABC Trading", "technician": "Ahmed", "status": "Completed", "amount": 500 }] }
}

collectionPath selects the array, for example data.jobs. Column binding.path values are read from each record (jobNumber, not data.jobs.jobNumber). Text outside the table still uses root paths such as report.branchName. A missing or empty collection renders the table's empty text and omits totals. A value that is not an array, or a record that is not an object, throws ReportProcessingError.

Columns can set aggregate to sum, count, min, or max. Sum, minimum, and maximum use finite numbers. Missing and null values are skipped and are not treated as zero. A present non-number throws. Zero is included. Sums are accumulated at 8 decimal places. Values at or beyond 1e12 throw. Count counts records and is printed as an integer. A currency format uses format.currency, then invoice.currency, then report.currency. There is no built-in currency.

sort is a list of { path, direction } with asc or desc. Comparison is deterministic: numbers compare numerically, text uses English numeric ordering, and the original record order breaks ties. Missing and null sort values come last when ascending and first when descending.

groupBy supports up to three levels. The first level is the outermost group. Each level prints a heading, the rows or nested groups, then that level's subtotal when a column has an aggregate. Inner subtotals appear before outer ones. Null and missing group values share the heading (Blank). Group order follows the sorted records. Group fields must be text, numbers, or booleans. The engine does not run expressions or caller-supplied functions.

showGrandTotal adds one grand total after the groups when at least one column has an aggregate.

Header and footer bands contain static text, a data binding, or an image. They are separate:

  • report.title and report.description print once at the start.
  • report.reportHeader prints once before the body.
  • report.pageHeader repeats in each report table's thead when printing.
  • report.pageFooter is a footer at the end of the screen document and uses position: fixed when printing.
  • report.reportFooter prints once after the body.
  • report.showPageNumber adds a CSS counter(page) in the page footer.

Report HTML is one flowing document ordered from top to bottom on the page. @page carries the page size and margins. Table headers use thead { display: table-header-group }. Rows use break-inside: avoid. A group heading uses break-after: avoid so it can stay with the next row. pageBreakBefore on a report table adds break-before: page. These are hints to the browser. They do not guarantee the same page breaks in every browser, and this package does not measure rows into a page list. The screen preview does not calculate page numbers. Total page count is omitted because counter(pages) is not reliable. PDF export uses the same report processor and the same print CSS, then asks Chromium for the page count.

An items table on an invoice is unchanged: it stays inside its box. A report table on a generic or invoice document is also drawn inside its box. The flowing multi-page layout is used when documentType is report.

PDF export

PDF generation lives in @arslankhan/doc-forge/pdf. It calls the existing renderToHTML pipeline, so invoice totals and report grouping stay in their own modules. The HTML renderer does not import Playwright. The designer entry point does not either.

Install Playwright in the application that prints PDFs, then install Chromium:

npm install playwright pdf-lib
npx playwright install chromium

On a Linux server or in Docker, Chromium also needs system libraries:

npx playwright install-deps chromium

The library does not hard-code an executable path. Set launch.executablePath only when the browser is installed somewhere else. pdf-lib is used when metadata is set, to write the title, author, subject, and keywords after Chromium prints the file.

import { exampleInvoiceData, exampleInvoiceTemplate } from "@arslankhan/doc-forge";
import { closePdfBrowser, renderToPDF, renderToPdfFile } from "@arslankhan/doc-forge/pdf";

const pdf = await renderToPDF(exampleInvoiceTemplate, exampleInvoiceData);
await renderToPdfFile(exampleInvoiceTemplate, exampleInvoiceData, "invoice.pdf");
await closePdfBrowser();

renderToPDF returns a Buffer and does not write a file. renderToPdfFile writes only the path you pass. examples/render-pdf.mjs (npm run example:pdf) writes a generic document, the sample invoice, and the large jobs report under examples/output/.

Page size, margins, and CSS

Precedence for one PDF call:

  1. options.page fields that are set replace the template for both the HTML and the PDF. Named sizes, including A3, are written as explicit dimensions because the HTML renderer prefers page.dimensions. A3 is available here and is not a template paper size.
  2. Omitted page fields stay on the template.
  3. preferCssPageSize defaults to false. Chromium then uses the resolved width and height. Set it to true when a CSS @page size should win.

Margins are applied once:

  • A report uses the template margins as Chromium PDF margins. The PDF stylesheet sets @page { margin: 0 } so the HTML margin is not added again.
  • A generic document or invoice uses a zero PDF margin. Its HTML already lays the design out on the full page, and @page { margin: 0 } in that HTML matches the preview.
  • options.page.margins replaces that choice. On a fixed page, the HTML page box is reduced by those margins so the design fits inside the Chromium margin. Coordinates then start at the content box, which is inset from the HTML preview.
  • Headers and footers need at least 10mm on every edge. Smaller margins are rejected instead of being enlarged.

printBackground defaults to true. scale is 0.1 through 2. Invalid sizes, negative margins, and unsupported paper names throw PdfRenderError with code invalid_option before Chromium starts.

Page numbers

When a report has showPageNumber, or when you set headerTemplate or footerTemplate, Chromium prints the margin header and footer. The page number is <span class="pageNumber"></span> and the total is <span class="totalPages"></span>. Those values come from the PDF, not from a browser preview estimate. The in-document print footer is hidden for that PDF so the CSS counter(page) is not printed twice.

The HTML preview still has no total page count. A very tall row or group can still cross a page boundary. Content is not dropped to force a fit. Repeated table headers, break-inside: avoid, and break-after: avoid are the same print CSS the HTML renderer already emits.

Images and network

Each PDF gets a new browser context and a new page. JavaScript is disabled in that context. Requests are aborted except data: and about:. https images are allowed only when imageNetwork is true and the existing image policy allows that URL. javascript:, file:, and other schemes stay rejected. The renderer does not navigate to a caller-supplied URL.

timeout defaults to 30 seconds and must be from 1 through 120000 milliseconds. A timeout throws PdfRenderError with code timeout.

Browser lifecycle

renderToPDF reuses one Chromium process. Call closePdfBrowser() when the process should exit. pdfBrowserStatus() reports whether that process is connected. A launch option starts a separate browser for that call and closes it afterward; it does not replace the shared process. A caller-supplied browser is not closed. Launch failures throw PdfRenderError with code launch_failed and include the Chromium install command.

Pages and contexts are closed after success and after failure. Document data is not reused across requests.

Appearance

Element styles stay on the existing style object. New fields are optional, so templates saved before this change still render. Schema version remains 1.

Text, items, totals, and report tables can set font family, size, weight (normal, medium, bold), italic, alignment, vertical alignment, and decoration. Every element except a line can set colors, opacity, padding, corner radius, and a border. A border uses one color, width, and style (solid, dashed, dotted, double, or none), or a borderTop, borderRight, borderBottom, and borderLeft object when sides differ. Colors must be a hex color, an rgb/rgba function, or a plain color name. Font families must be a plain family list. Values that could change the CSS declaration are rejected and are not written into HTML.

Report cell precedence, from lowest to highest:

  1. Table text and border on the report table style.
  2. Role defaults for the header, group heading, subtotal, and grand total.
  3. headerStyle, bodyStyle, groupStyle, subtotalStyle, or grandTotalStyle.
  4. Odd and even row colors. evenRowColor falls back to alternateRowColor. stripeRows: false turns striping off.
  5. The first matching rowRules entry. A rule compares one record field with eq, neq, gt, gte, lt, lte, or blank. It is not an expression and is never evaluated as JavaScript.
  6. The column headerStyle or style. column.align applies only when that style does not set textAlign.

percent formatting treats the stored number as a fraction: 0.15 is shown as 15%. The HTML preview and PDF export use this same resolution. PDF background printing stays on unless printBackground is set to false.

Licensing

License checks are optional. A host that never configures them gets status unconfigured, and hasFeature() still returns true for every registered feature. Rendering does not call the license client, so invoices, reports, HTML, the designer, and PDF export behave as they do today.

import {
  configureLicense,
  createHttpLicenseProvider,
  exampleInvoiceTemplate,
  inspectDocumentCapabilities,
} from "@arslankhan/doc-forge";

const license = configureLicense({
  productId: "doc-forge",
  issuer: "<license-issuer>",
  audience: "<your-application>",
  keys: [{ kid: "<key-id>", spki: "<p256-public-key-spki-base64>" }],
  provider: createHttpLicenseProvider({
    activateUrl: "<https-license-api-url>/activate",
    refreshUrl: "<https-license-api-url>/refresh",
    deactivateUrl: "<https-license-api-url>/deactivate",
  }),
});

console.log(await license.getStatus());
console.log(await license.hasFeature("pdf.export"));
console.log(inspectDocumentCapabilities(exampleInvoiceTemplate));

inspectDocumentCapabilities reports which template features are in use. It does not grant or remove them. activateLicense(key) sends the key to the configured provider and then verifies the returned ES256 token. A provider response of valid is ignored unless the signature, issuer, audience, product, and expiry check out. isPremium: true is not an entitlement.

Leave enforcement at its default, "off", until the feature-to-plan mapping is decided. "enforce" makes hasFeature() depend on the signed entitlements list. The registered names are report.grouping, report.aggregates, designer.premium, styling.premium, pdf.export, tables.advanced, and support.commercial.

The private signing key and any payment-provider secret stay on the license server. This package only needs the public key. A network failure is network_unavailable, not invalid. Offline, a signature-valid token can be used until it expires, and revocation is not known until the next successful refresh. Checks that run in the browser can be bypassed; they are not source-code protection. Verify PDF entitlements in the Node.js process that calls @arslankhan/doc-forge/pdf.

npm run example:license prints the unconfigured status. It does not contact a server. The software license for the package itself is still a placeholder: see LICENSE, LICENSING.md, and RELEASE.md. Do not publish until those terms are chosen. The package does not take card payments. A paid license should be issued only after the license server verifies a signed payment webhook.

Limitations

An invoice items table repeats rows inside its box and does not flow onto another page. Report page breaks follow Chromium's print layout and can differ from the designer preview. The HTML preview does not know the total page count. The package does not connect to a database, draw charts, build pivot tables, or evaluate expressions. It does not upload images, host files, convert currencies, or apply country tax rules. Binding paths cannot contain conditionals. There is no field browser: paths are typed in. Schema version remains 1.

There is no license server in this repository. Activation does not succeed until the host supplies one. Client-side entitlement checks do not protect source code. Offline verification cannot see a revocation that happened after the token was signed. CommonJS require() is not supported. The package has not been published.

Scripts

npm install
npm test
npm run lint
npm run typecheck
npm run build
npm run package:check
npm run package:smoke
npm run demo
npm run example:html
npm run example:pdf
npm run example:license

npm test compiles the TypeScript tests and runs them with Node's built-in test runner. PDF tests launch Chromium and fail when it is missing. npm run lint is the TypeScript typecheck; this repository does not add a separate linter. npm run build emits the library to dist/, which is the package entry point. @arslankhan/doc-forge/pdf is the Node.js PDF entry. @arslankhan/doc-forge and @arslankhan/doc-forge/designer stay free of Playwright. package:check and package:smoke inspect the installable tarball. They do not publish it. The commands the owner runs to publish are in RELEASE.md.