Docs
/

@leumas/exporters

The ONE way anything in Leumas leaves the app — serialize a value to JSON/NDJSON/CSV/text, hand it to the browser as a download, and wrap it in a versioned envelope so a file exported from one Leumas...

@leumas/exporters

The ONE way anything in Leumas leaves the app.

Before this existed, pnpm atlas find "download file blob export csv" answered "nothing like it" while twenty-odd surfaces each hand-rolled URL.createObjectURL inline. This package is what they should all have been calling.

import { exportAs, copyText } from '@leumas/exporters';

exportAs('csv', events, 'mqtt-capture');   // → "mqtt-capture_2026-08-11_14-30-05.csv"
await copyText(toJSON(result));

Three layers

SubpathNeeds a DOMWhat it is
@leumas/exporters/serializenoa value → the text of a file — toJSON toNDJSON toCSV toText serialize FORMATS
@leumas/exporters/downloadyesthat text → the user's downloads — saveAs downloadAs exportAs copyText readFileText canDownload
@leumas/exporters/envelopenothe portable wrapper — wrap read
@leumas/exporters/filenamenostampName exportFilename

The barrel (@leumas/exporters) re-exports all four. Nothing runs at module scope, so importing the barrel in Node is safe — canDownload() simply answers false and saveAs becomes a no-op.

The UI is not in here. <ExportMenu> lives in @leumas/ui; a package that renders nothing must not depend on React.

The envelope — why an export is not just JSON

A bare JSON.stringify(request) is not portable, it is a blob of JSON that happens to work today. wrap() adds the three things a payload cannot say about itself:

{ "leumas": 1, "kind": "protocols.request", "version": 1,
  "createdAt": "2026-08-11T14:30:05.000Z",
  "meta": { "transport": "mqtt" },
  "data": { }
}

read(text, 'protocols.request') returns {ok, kind, version, meta, data, error} and never throws — it is fed files a human dragged in, half of which are the wrong file. A parse error, a missing marker, the wrong kind and a newer envelope are all normal inputs, and each comes back as a sentence you can put straight in a <Notice>.

A bare payload with no envelope is rejected on purpose. Accepting it would mean guessing what it was.

The four things that were wrong in the hand-rolled copies

Each is pinned by a test in test/exporters.test.js, and none of them throws — which is why none was ever noticed by the person who wrote it.

  1. JSON.stringify on anything holding a socket, a DOM node or a parent pointer throws from inside

a click handler, so the button appears to do nothing at all. toJSON is total.

  1. The obvious circular-reference guard eats non-circular data. A WeakSet of everything seen also

fires on a reference that merely appears twice — the same headers object on the request and the response — and silently writes "[Circular]" over real data. safeReplacer tracks ancestors.

  1. CSV is not rows.map(r => r.join(',')). A value holding a comma, a quote or a newline must be

quoted with its quotes doubled (RFC 4180), or the file loses column alignment from that row on — and a spreadsheet opens it without complaining, which is worse.

  1. URL.revokeObjectURL in the same tick as the click races the browser's read of the href and

produces a zero-byte file on some versions; never revoking pins the blob for the life of the tab. Revoke on the next frame. And the anchor must be in the document — a detached <a> is silently ignored by Firefox and works fine in Chrome.

@leumas/uiExportMenu (the control) and @leumas/ui/dnd (the import direction). @leumas/devices/mediadownloadBlob is now a thin re-export of saveAs.

What it exports

42 exports: 5 components, 0 hooks, 37 helpers. Import from @leumas/exporters.

CLIPBOARD_IMAGE_TYPESENVELOPE_VERSIONENVELOPE_VERSIONFORMATSFORMATScanCopyImagecanDownloadcanDownloadcopyBlobcopyImagecopyTextcopyTextdownloadAsdownloadAsexportAsexportAsexportFilenameexportFilenameformatOfformatOfformatXmlformatXmlreadreadreadFileTextreadFileTextsaveAssaveAsserializeserializestampNamestampNametoCSVtoCSVtoJSONtoJSONtoNDJSONtoNDJSONtoTexttoTextwrapwrap
Source shared/packages/exporters/README.md (no-git)markdownjson
Generated from the Leumas repository. Every page cites the file it came from.leumas.techllms.txt