bilig

Evaluate Excel formulas in Node.js with TypeScript

Use @bilig/headless when a Node.js program needs a small workbook model it can edit, recalculate, read, and persist. The useful case is not “open Excel on the server.” It is “run spreadsheet-shaped logic in a service and prove the result that came back.”

That usually means one of these jobs:

If the main job is styling or writing an .xlsx file, start with an XLSX library. If the main job is a long tail of arbitrary Excel edge cases with no fixture-reduction step, evaluate a mature formula engine first. If the job is a TypeScript workbook object with formulas, edits, readback, and persistence, try the WorkPaper path below.

Run a TypeScript formula smoke test

Start from an empty directory:

mkdir bilig-node-formulas
cd bilig-node-formulas
npm init -y
npm pkg set type=module
npm install @bilig/headless
npm install -D tsx typescript @types/node

Create eval-node-formulas.ts:

import {
  WorkPaper,
  createWorkPaperFromDocument,
  exportWorkPaperDocument,
  parseWorkPaperDocument,
  serializeWorkPaperDocument,
  type WorkPaperCellAddress,
} from '@bilig/headless'

type NumericCell = {
  value: number
}

function readNumber(workbook: WorkPaper, address: WorkPaperCellAddress): number {
  const value = workbook.getCellValue(address)

  if (typeof value !== 'object' || value === null || typeof (value as NumericCell).value !== 'number') {
    throw new Error(`Expected numeric cell, got ${JSON.stringify(value)}`)
  }

  return (value as NumericCell).value
}

const workbook = WorkPaper.buildFromSheets({
  Quote: [
    ['Metric', 'Value'],
    ['Seats', 12],
    ['Price', 49],
    ['Discount', 0.1],
  ],
  Summary: [
    ['Metric', 'Value'],
    ['Monthly total', '=Quote!B2*Quote!B3*(1-Quote!B4)'],
  ],
})

const quote = workbook.getSheetId('Quote')
const summary = workbook.getSheetId('Summary')

if (quote === undefined || summary === undefined) {
  throw new Error('Expected Quote and Summary sheets')
}

const total: WorkPaperCellAddress = { sheet: summary, row: 1, col: 1 }
const before = readNumber(workbook, total)

workbook.setCellContents({ sheet: quote, row: 1, col: 1 }, 20)
const afterEdit = readNumber(workbook, total)

const serialized = serializeWorkPaperDocument(exportWorkPaperDocument(workbook, { includeConfig: true }))
const restored = createWorkPaperFromDocument(parseWorkPaperDocument(serialized))
const afterRestore = readNumber(restored, total)

if (before !== 529.2 || afterEdit !== 882 || afterRestore !== afterEdit) {
  throw new Error(JSON.stringify({ before, afterEdit, afterRestore }))
}

console.log({ before, afterEdit, afterRestore, verified: true })

Run it:

npx tsx eval-node-formulas.ts

Expected output:

{ "before": 529.2, "afterEdit": 882, "afterRestore": 882, "verified": true }

That proves the Node process created a workbook, evaluated a cross-sheet formula, edited an input cell, read the dependent result, serialized the WorkPaper document, restored it, and read the same calculated value again.

Where this is different from calling formula functions

A direct formula-function library is useful when code wants to call something like SUM() as a JavaScript function. A workbook runtime is useful when the formula depends on sheet state:

That is the boundary @bilig/headless is built around.

Where this is different from writing XLSX files

An XLSX library is the right first choice when the file is the product: reports, styles, images, tables, and handoff to Excel or another spreadsheet app.

Use @bilig/headless when the service needs calculated workbook state before a person opens any file. You can still export or import at the system boundary, but the WorkPaper model is the part that calculates and verifies the values in Node.

Production checklist

Before using any headless spreadsheet engine in a service, check these items:

For @bilig/headless, start with one TypeScript fixture and keep it boring: build the workbook, edit one input, read one dependent output, persist, restore, and assert the same value after restore.

Next paths

If this is the shape of formula automation you were looking for, star the repo so the package is easier to find later: https://github.com/proompteng/bilig/stargazers.

If it almost matches but a gap blocks adoption, use the adoption blocker form: https://github.com/proompteng/bilig/discussions/new?category=general.