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

@12-apps/thermal-printing

v1.0.0

Published

Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), the ESC/POS encoder with its CP850 code page (./escpos), the same lines as a print-ready HTML document for a printer

Readme

@12-apps/thermal-printing

Receipt printing for the two ways a thermal printer is attached, behind one fixed-width document.

pnpm add @12-apps/thermal-printing

The problem it solves

A receipt printer reaches a host one of two ways, and the host does not get to choose which one the shop bought:

| | Network (Wi-Fi / Ethernet) | Local (USB cable) | |---|---|---| | how it is reached | raw ESC/POS over a TCP socket, :9100 by convention | the browser on the machine it is plugged into, printing through the OS | | who drives it | the server — ./net | a tab — ./html | | with nothing open | still prints | stops, silently: a USB device has no address | | what a success means | the bytes left the process | the document reached the operating system |

The naive shape is two code paths, and the cost shows up the first time a store swaps one kind of printer for the other: the ticket changes, because a second path is a second layout. So the split here is one document, two encoders.

       host builds            this package encodes
   ┌──────────────────┐      ┌─────────────────────┐
   │  TicketLine[]    │─────▶│ ./escpos → bytes    │──▶ socket (./net)
   │  (line, field,   │      │ ./html   → document │──▶ a tab prints it
   │   rule, wrap)    │      └─────────────────────┘
   └──────────────────┘

The layout is authoritative for both. ./html renders the same lines in a monospace column of exactly the same width rather than laying out a <table> — that is the divergence the package exists to refuse.

What it does not do

It carries no copy. Not one sentence, in any language. A failed send returns a reason code and the host:port it was aimed at; the host turns that into words, in the language of whoever is reading the screen. Two hosts can say "the printer at 10.0.0.9 did not answer" and "the kitchen printer is offline" from the same event.

It does not compose your ticket. What a ticket says, in what order, is the host's product decision — and it is where the host's own vocabulary, currency and time zone live. This package gives you the arithmetic underneath it: column counts, wrapping with a hanging indent, dividers, Label: value fields that disappear when there is no value.

It owns no models and no routes. There is no Prisma partial and nothing to mount.

Surfaces

| Export | What it is | |---|---| | @12-apps/thermal-printing | The line model — TicketLine, columnsFor, wrap, line, centered, rule, field, PAPER_WIDTHS_MM. Pure, isomorphic. | | …/escpos | encodeTicket(lines)Uint8Array. Initialise, CP850, align, emphasise, double height, feed, partial cut. | | …/html | renderTicketHtml(lines, paperWidthMm, lang?) → a standalone document sized in ch, with @page { margin: 0 }. | | …/net | sendToNetworkPrinter(host, port, bytes, options?). Node-only (node:net), never throws, structured failures. | | …/routing | printerRoute(printers) / printerFor(route, destinationId) — per-destination with a default, generic over your own printer rows. |

./net sits behind its own subpath so that importing the encoders into a browser bundle never drags node:net in.

The three things that are easy to get wrong

The code page. A printer does not speak UTF-8 — it prints the glyph at each byte. Sending an accented word as UTF-8 puts two characters where one belongs, on every printer, looking exactly like a font problem. ./escpos selects CP850 and maps the Latin-script characters a ticket carries; anything outside the table loses its diacritic before it falls back to ?, because a word somebody can still read beats a ? mid-word.

"Sent" is not "printed". There is no protocol above a raw :9100 socket: no handshake, no acknowledgement, no status frame. An empty roll, a jam, and a perfect ticket are the same success — and window.print() returns when the dialog is dismissed, which is no better. A host that reports "printed" is making a promise the transport never made. Give the operator a reprint instead.

A destination that owns a printer is answered by it or by nothing. Switching one room's printer off must stop that room's tickets, not quietly reroute them somewhere else — a ticket coming out in the wrong room is one nobody in that room ever sees. ./routing keeps inactive printers in the map precisely so that "no printer here" and "the printer here is off" stay different answers.

Adoption

See ADOPTING.md.