Compose with libraries
Use the focused ConsultChimps TypeScript packages without the CLI.
The CLI is intentionally thin. File discovery, table operations, workbook adapters, PowerPoint operations, and PDF operations live in focused packages that can be reused in a server, desktop app, workflow runner, or another CLI.
Package map
| Package | Responsibility |
|---|---|
@consultchimps/core | Shared result contracts and structured errors |
@consultchimps/files | File discovery and safe output handling |
@consultchimps/tabular | Runtime-neutral table union, grouping, and column mapping |
@consultchimps/theme | Runtime-neutral palette model and colour validation |
@consultchimps/db | Persistent SQLite/DuckDB databases, imports, and batch history |
@consultchimps/xlsx | Excel reading, writing, merging, consolidation, and splitting |
@consultchimps/pptx | PowerPoint template inspection and population |
@consultchimps/pdf | PDF splitting and merging |
@consultchimps/messages | Plain-language rendering of results and errors |
Discover inputs
import { discoverFiles } from "@consultchimps/files";
const workbooks = await discoverFiles(
["January.xlsx", "regional/", "late/*.xlsx"],
{ extensions: [".xlsx"] },
);The result contains unique, absolute, sorted paths.
Union in-memory tables
import { unionTables, type Table } from "@consultchimps/tabular";
const tables: Table[] = [
{
columns: ["Client", "Amount"],
rows: [{ Client: "Acme", Amount: 1200 }],
source: { file: "north.xlsx", sheet: "Revenue", firstDataRow: 2 },
},
{
columns: ["client", "Status"],
rows: [{ client: "Globex", Status: "Open" }],
source: { file: "south.xlsx", sheet: "Revenue", firstDataRow: 2 },
},
];
const combined = unionTables(tables);The output columns are Client, Amount, Status, and the three provenance
columns. Case-insensitive headers are aligned without changing the original
spelling of the first header encountered.
Map columns onto one schema
When the same field arrives under different headers (Case ID from one system,
Reference from another), a column mapping folds them into one canonical
column before the tables are unioned. A mapping is a versioned JSON document:
canonical columns with their aliases, optional coercions, and constant
columns. Reading the file is your job; the library takes the parsed object, so
it stays free of any filesystem dependency.
import {
applyColumnMappingToTables,
unionTables,
validateColumnMapping,
type Table,
} from "@consultchimps/tabular";
const mapping = validateColumnMapping({
version: 1,
columns: [
{ name: "Case_ID", aliases: ["Reference", "Case Number"] },
{
name: "Opened_On",
aliases: ["Run Time"],
coercion: { type: "date", format: "DD/MM/YYYY" },
},
{
name: "Amount",
aliases: ["Total"],
coercion: {
type: "number",
decimalSeparator: ",",
thousandsSeparator: ".",
},
},
],
constants: { Dataset: "quarterly" },
});
const tables: Table[] = [
{
columns: ["Case ID", "Run Time", "Total", "Region"],
rows: [
{
"Case ID": 1,
"Run Time": "09/03/2024",
Total: "1.234,50",
Region: "north",
},
],
source: { file: "north.xlsx", sheet: "Cases", firstDataRow: 2 },
},
{
columns: ["reference", "Region"],
rows: [{ reference: 2, Region: "south" }],
source: { file: "south.xlsx", sheet: "Cases", firstDataRow: 2 },
},
];
const mapped = applyColumnMappingToTables(tables, mapping);
const combined = unionTables(mapped.tables);
if (mapped.unmappedColumns.length > 0) {
console.warn(`Columns kept as-is: ${mapped.unmappedColumns.join(", ")}`);
}The first row becomes Case_ID: 1, Opened_On: "2024-03-09", Amount: 1234.5,
Region: "north", Dataset: "quarterly", plus the provenance columns. Region
is an unmapped column: it passes through under its own name and is reported
in unmappedColumns so you can warn about it: loud, but lossless.
Points worth knowing before you write a mapping:
- Aliases match by normalized column key. One alias entry catches every
case, spacing, and punctuation variant of that spelling, so
Reference,reference, andReference:all match the same entry, and a canonical column always matches its own name. Canonical names are written to the output verbatim. - Coercions are strict. A date is parsed from the format you declare
(
YYYY,MMorM,DDorD; every other character is a literal separator) into an ISO 8601 date string. A number is parsed with the separators you declare, defaulting to,for thousands and.for decimals. Blank cells stay blank, but a value that is not what its coercion declares stops the run withTABLE_MAPPING_COERCION_FAILEDnaming the row and column, rather than being written through unconverted. - Ambiguity is refused, never guessed. Two columns of one table folding into
a single canonical column raise
TABLE_MAPPING_COLUMN_COLLISIONnaming the file, sheet, both columns, and the canonical column, because merging them would drop values.validateColumnMappingrefuses a document that is ambiguous on its own terms (an unknown version, two canonical columns that normalize alike, an alias claimed twice) withTABLE_MAPPING_INVALIDand a machine-readableproblemin the error details. - Constant columns come last, after the mapped and unmapped columns.
Apply a mapping per source table, before unionTables, as above: that is what
lets a collision name the sheet it came from. applyColumnMapping does the same
for a single table.
Draft a mapping from the headers you have
suggestColumnMapping reads header lists and groups the columns whose
normalized keys already match (the spelling variants of one field), proposing
the first spelling seen as the canonical name:
import { suggestColumnMapping } from "@consultchimps/tabular";
const suggestion = suggestColumnMapping([
{ columns: ["Case ID", "Failed Checks"], file: "north.xlsx", sheet: "Cases" },
{ columns: ["Case_ID", "failed_checks"], file: "south.xlsx", sheet: "Cases" },
]);
// suggestion.mapping is a draft version 1 document to review and edit.
// suggestion.groups carries the evidence: the spellings each entry folds and
// where every one of them was seen.The suggestion uses no similarity scoring and no sample values: it groups only
deterministic normalization equivalence, and it never guesses. That is not the
same as never being wrong. Headers that differ only in punctuation, such as
A+B and A-B, normalize to the same key and will group even when they mean
different things, which is why every draft is reviewed before use. Synonyms that
share no spelling, such as Timestamp and Run Time, stay for you to add by
hand. A drafted entry carries no aliases on purpose: every spelling in a group
shares one normalized key, which the canonical name already matches. Review the
draft and apply it yourself; nothing is applied for you.
Resolve and validate a palette
@consultchimps/theme holds a runtime-neutral palette model: categorical,
sequential, and semantic colours, each with a light and a dark value, plus a
resolve API and a validation pass. It has no dependencies. A client's real
colours are supplied at runtime; the package ships neutral placeholder palettes
only.
import {
NEUTRAL_PALETTE,
resolveCategorical,
resolveSemantic,
validatePalette,
contrastRatio,
} from "@consultchimps/theme";
// Resolve a colour role to a value for a mode.
const seriesOne = resolveCategorical(NEUTRAL_PALETTE, 0, "light"); // "#2a78d6"
const critical = resolveSemantic(NEUTRAL_PALETTE, "critical", "dark");
// Validate contrast and categorical distinctness. The pass returns a report
// rather than throwing, so a caller can surface each issue.
const report = validatePalette(NEUTRAL_PALETTE, "light");
if (!report.valid) {
for (const issue of report.issues) {
console.warn(`${issue.check}: ${issue.message}`);
}
}
// The WCAG contrast ratio between two colours, for a one-off check.
console.log(contrastRatio("#0b0b0b", "#fcfcfb"));A report is { valid, issues[] }. valid is true when no issue has error
severity; a contrast shortfall against the surface is reported as a warning, so
a palette that leans on visible labels still validates. resolveCategorical
throws when a slot is past the palette's range, since categorical slots are
assigned in order rather than cycled.
Read workbooks without writing anything
The xlsx package also exposes the readers the operations are built on, for callers that want the data rather than an output file:
import {
readWorkbookTables,
readWorksheetRecords,
readWorkbookExcelTables,
readWorkbookNamedRanges,
} from "@consultchimps/xlsx";
// Every visible, non-empty worksheet as a Table (columns, rows, source).
const tables = await readWorkbookTables("clients.xlsx");
// One worksheet as records keyed by header, using Excel's stored display text.
const records = await readWorksheetRecords("clients.xlsx", {
worksheet: "Clients",
});
// Defined Excel Tables and named ranges, with their data.
const excelTables = await readWorkbookExcelTables("clients.xlsx");
const namedRanges = await readWorkbookNamedRanges("clients.xlsx");readWorkbookTables accepts the consolidator's selection options: sheets, a
one-based headerRow, and includeHiddenSheets. Without a headerRow the
header is detected the way the consolidator detects it: the first row holding
more than half as many values as the fullest of the rows below it, or at most
one value fewer, with the rows above it skipped when a blank row sets them apart
from the header or they are banners merged across columns (see
Choose a header row).
Columns that hold nothing in the header row or under it are left out as spacers,
and readWorkbookWorksheets reports both counts per worksheet as
skippedTitleRows and skippedSpacerColumns. readWorkbookTablesBytes is its
byte-level twin, for a browser that holds the workbook's bytes rather than a
path. readWorksheetRecords selects one worksheet and a headerRow, and
returns uncachedFormulas, the Sheet!B4 cells it read as empty because Excel
never calculated them; the Excel Table and named-range readers filter by
sheets plus tables or names. The writeTable counterpart writes one
in-memory Table to a new formatted workbook.
Inspect a workbook before operating on it
describeWorkbook reports what is in a file without creating anything:
worksheets with their visibility and dimensions, the header row the region
resolver picks (the one a consolidation reads from and a split keys on), Excel
Tables, named ranges, and a bounded sample of every column's values. The region
resolver and the table readers apply one header rule, so the header row and
columns the description reports are the ones readWorkbookTables returns.
Uncalculated formulas are where the two still differ, because a formula with no
cached result occupies its row for the description and holds nothing for a table
reader. A worksheet where no row holds a readable value at all is described from
its first used row, since the sheet is not empty, while the table readers yield
no table for it; and a worksheet whose header is readable but whose rows under
it hold only such formulas is described with its header, its columns, and those
rows counted as data rows, while the table readers see no values under the
header and yield no table. Either way readWorkbookWorksheetsBytes reports the
uncalculated cells beside the read. Use the description to confirm a workbook's
shape, or to build a picker for the sheets and columns another operation will
act on.
import { describeWorkbook } from "@consultchimps/xlsx";
const { description, result } = await describeWorkbook("review-log.xlsx", {
includeHiddenSheets: true,
});
for (const sheet of description.sheets) {
console.log(`${sheet.name} (${sheet.visibility})`);
console.log(` header row: ${sheet.headerRow ?? "none found"}`);
for (const column of sheet.columns) {
console.log(` ${column.header}: ${column.sampleValues.join(", ")}`);
}
}
for (const table of description.excelTables) {
console.log(`Excel Table ${table.name} on ${table.sheet} (${table.range})`);
}
console.log(result.metrics.worksheets, result.warnings);The inspection creates nothing, so the outcome pairs the description with an
OperationResult that has no artifacts: its metrics are the counts, and its
warnings name what a later operation would stumble on: hidden worksheets left
out of the description, or a worksheet with no header row to match columns by.
Sample values are deliberately bounded: at most MAX_COLUMN_SAMPLE_VALUES (5)
distinct non-empty stored values per column, in the order the rows carry them.
Pass sampleValues to ask for fewer. Nothing is type-inferred: the values are
reported exactly as the workbook stores them, so 1 and "1" stay distinct.
headerRow, includeHiddenSheets, and sheets mean what they mean to every
other reader here; naming a worksheet the workbook does not have is refused with
XLSX_WORKSHEET_NOT_FOUND rather than answered with an empty description.
describeWorkbookBytes is the byte-level twin and produces a structurally
identical description for the same workbook.
Notice cells a table cannot represent
Two kinds of cell reach a Table as something they are not.
Excel writes a formula and its last calculated result side by side, but a file written by a generator, or saved with calculation switched off, carries the formula alone. Every reader then sees those cells as empty, because empty is all the file says, and a table read from that worksheet has holes where the numbers belong.
An error cell reaches it as a number. #REF!, #DIV/0! and their kind are
stored as t="e", and the spreadsheet engine reports the internal code Excel
numbers each error by, so a #REF! arrives as 23 and a #DIV/0! as 7. That
is worse than a hole: a column of amounts still infers a numeric type, and
nothing downstream can tell those codes from data.
readWorkbookWorksheetsBytes reports each selected worksheet, whether or not it
yielded a table, with the rectangle the read covered and the count of each kind
inside it:
import { readWorkbookWorksheetsBytes } from "@consultchimps/xlsx/bytes";
for (const worksheet of await readWorkbookWorksheetsBytes({
name: file.name,
bytes,
})) {
if (worksheet.uncachedFormulaCells > 0) {
// Calculating them belongs to Excel: open the workbook, let it calculate,
// and save it again.
console.log(`${worksheet.sheet} has values the file does not carry`);
}
if (worksheet.errorCells > 0) {
// What a broken formula was meant to say is not in the file at all: fix or
// clear the errors in Excel and save it again.
console.log(`${worksheet.sheet} has values that are errors`);
}
}readWorkbookWorksheets is the file twin: both surfaces hand the same operation
the same bytes, so neither can answer differently about the same workbook.
A cell a worksheet stores as a date reads as ISO 8601 text, and that text is a
function of the workbook alone. A worksheet says a cell is a date in two ways,
and both are read: a count of days from the workbook's epoch wearing a date
number format, which is what Excel writes, and a cell that declares t="d" and
writes ISO 8601 text, which the format allows and other generators use. The
declared type is enough on its own; a cell that declares itself a date and wears
no format is still a date, and reads as one.
Neither carries a time zone, so the reader decodes what the workbook holds into
calendar components and writes them out. No Date takes part: a Date has a
local face as well as a UTC one, and text built from the local face makes the
same workbook read as a different calendar day in every time zone. The same
workbook therefore reads identically on a machine set to UTC, to UTC+4, or to
UTC-7, which is what a group key, an output filename, and a stored date all
need. A declared date whose text names no moment is carried as that text rather
than converted.
The text is always the full timestamp, 2024-01-01T00:00:00.000Z, whether or
not the cell carries a time, so a date column reads the same way down its whole
length. @consultchimps/db accepts that spelling and the bare 2024-01-01 in a
date column, so either travels.
Consolidate, a compact split and writeTable write a value in that spelling
back as an Excel date: a serial in the 1900 date system, formatted yyyy-mm-dd,
or yyyy-mm-dd hh:mm:ss when it carries a time. A date before 1900 has no
serial in that system and stays text.
A moment carries a millisecond, and a declared date written finer than that is
carried as its text rather than shortened to fit: 18:00:00.1234 and
18:00:00.1239 are not the same instant, and reading both as 18:00:00.123
would change what the cell says and let a split gather rows that are not
together. Digits past the third that are zeros lose nothing and are accepted.
The same rule covers every other shape the text can take, and the whole list is
enumerated in the reader itself, beside the grammar.
Both counts are read from the package's own document model, because the
spreadsheet engine behind the table readers drops a numeric formula cell with no
cached value while parsing and flattens an error into that code, and they are
scoped in one walk to the rectangle the table reader reported, so they describe
the read you got rather than a region resolved a second time. A cached value
counts as present when the element is there, whatever it holds, so a formula
that evaluated to an empty string or to zero has been calculated; an error
counts whether somebody typed it in or a formula left it there.
readWorkbookTablesBytes is this list with the tables taken out of it, and the
consolidation, merge, and split operations are unchanged: they go on treating an
uncalculated formula as an empty cell and an error as the code it is stored as.
A worksheet part is parsed the first time something asks for it, so a malformed
row or cell reference surfaces long after the workbook opened. Whichever step it
comes from, it is reported as XLSX_READ_FAILED, naming the workbook and, when
the failure belongs to a worksheet, that worksheet; the parser's own complaint
is the error's cause.
Remove worksheet and workbook protection
unprotectWorkbook removes ordinary worksheet (sheetProtection) and
workbook-structure (workbookProtection) protection from an .xlsx or .xlsm
workbook without needing the protection password, writing a new file and leaving
the source untouched. It edits a copy of the package, so formulas, formatting,
hidden worksheets, tables, and any macro project travel across unchanged. It is
not password cracking: a workbook encrypted to require a password to open is not
supported and is reported with XLSX_UNPROTECT_UNSUPPORTED_FILE.
import { unprotectWorkbook } from "@consultchimps/xlsx";
const result = await unprotectWorkbook({
input: "protected.xlsx",
output: "unprotected.xlsx",
});
console.log(result.metrics.sheetProtectionsRemoved); // e.g. 3
console.log(result.metrics.workbookProtectionsRemoved); // e.g. 1An output named .xlsx or .xlsm has to match the workbook's declared type: an
ordinary workbook stays .xlsx and a macro-enabled workbook stays .xlsm. A
name that claims the wrong one of the two is refused with
XLSX_UNPROTECT_PACKAGE_TYPE_MISMATCH before anything is written, because a
package whose contents and name disagree is one Excel opens with a corruption
warning. unprotectWorkbookBytes is the byte-level twin, taking and returning
in-memory bytes for callers with no filesystem.
Read what an Excel operation promises
@consultchimps/xlsx exports its conformance contract: for every workbook
structure it tracks and every operation it offers, what that operation does. The
value is preserve, fix (rewritten so it stays valid), strip-warn (removed,
with a warning in the result), or refuse. The package's corpus tests exercise
the table cell by cell, so it states behavior rather than intent.
import { CONTRACT, TRACKED_STRUCTURES } from "@consultchimps/xlsx";
const removedByADefaultSplit = TRACKED_STRUCTURES.filter(
(structure) => CONTRACT.split[structure] === "strip-warn",
);
// ["pivot-tables"]CONTRACT.split describes the default whole-workbook split, the one that edits
a copy of your workbook. The other selectors do not follow it, so do not quote
these cells at a caller who chose one:
preserveWorkbook: false,range, andsheetrebuild a single worksheet from the grouped values and carry none of these structures: no other worksheets, no charts, no macros.tableruns the preserved-table rewrite, which compacts the table's rows. Because compaction moves rows, an ordinary A1 formula inside that table stops the operation withXLSX_SPLIT_PRESERVE_FORMULAinstead of being repaired the way this table'sfixcell describes.
A structure with no entry for an operation is one the package has not decided
yet; UNDECIDED_SPLIT_STRUCTURES, UNDECIDED_MERGE_STRUCTURES,
UNDECIDED_DESCRIBE_STRUCTURES, and UNDECIDED_UNPROTECT_STRUCTURES record why
each one is still open. CONTRACT.unprotect, for example, declares every
tracked structure preserve except external-links, which stays open because
no corpus fixture can exercise it yet.
A cell states what an operation ordinarily does, not what a particular run will
do: it carries no options, no input ordering, and no mode, so a behavior that
depends on one of those cannot be read off it. merge["vba-project"] is
strip-warn, for instance, even though a macro project survives when the first
input is the only one carrying it and the output is named .xlsm. Use a cell to
tell people what usually happens and read the conditions beside it in
what the Excel operations preserve, which is
generated from this table and carries them; treating a cell as a verdict on one
run will sometimes be wrong.
Write values without losing Excel formatting
The Excel consolidation, merge, and split operations accept values: true. For
operations that preserve a source workbook, formula elements are removed
directly from the XLSX package while cached results and formatting remain in
place:
import { mergeWorkbooks } from "@consultchimps/xlsx";
await mergeWorkbooks(["north.xlsx", "south.xlsx"], "outputs/all-sheets.xlsx", {
values: true,
});Consolidation and compact data-only splits already create value cells, so the same option is accepted there to make the caller's intent consistent across all Excel operations.
Handle structured failures
import {
isConsultChimpsError,
type OperationResult,
} from "@consultchimps/core";
import { splitPdf } from "@consultchimps/pdf";
async function run(): Promise<OperationResult | undefined> {
try {
return await splitPdf({
input: "report.pdf",
outputDirectory: "outputs/pages",
});
} catch (error) {
if (isConsultChimpsError(error)) {
console.error(error.code, error.details);
return;
}
throw error;
}
}Known errors carry a stable code, a human-readable message, optional
structured details, and the original cause when available.
Preview an operation before executing it
Every file-producing operation has a plan variant that validates the inputs
and computes every intended output without writing anything. Use it to show a
confirmation screen before running the real operation.
import { planSplitPdf, splitPdf } from "@consultchimps/pdf";
const plan = await planSplitPdf({
input: "report.pdf",
outputDirectory: "outputs/pages",
});
for (const output of plan.outputs) {
console.log(output.path, output.exists ? "(would need overwrite)" : "");
}
const result = await splitPdf({
input: "report.pdf",
outputDirectory: "outputs/pages",
});The plan lists absolute input paths, every planned output with an exists flag
for collisions, deterministic metrics, and warnings such as skipped rows.
planConsolidateWorkbooks, planSplitWorkbookByColumn, planMergePdfs, and
planPopulatePowerPointTemplate follow the same shape.
Report progress and support cancellation
Operations accept an optional onProgress reporter and an AbortSignal.
Progress events are deterministic for identical inputs; aborting stops the
operation before its next unit of work with a stable OPERATION_ABORTED error.
Source files are never modified by a cancelled operation, although output files
completed before the cancellation may remain.
import { mergePdfs } from "@consultchimps/pdf";
const controller = new AbortController();
const result = await mergePdfs({
inputs: ["first.pdf", "second.pdf"],
output: "outputs/combined.pdf",
signal: controller.signal,
onProgress: ({ stage, completed, total, detail }) => {
console.log(`${stage}: ${completed}/${total} ${detail ?? ""}`);
},
});Run operations without a filesystem
Browsers and other filesystem-free environments use the byte-level entry points
@consultchimps/pdf/bytes, @consultchimps/xlsx/bytes, and
@consultchimps/pptx/bytes. Named in-memory bytes go in; the produced bytes
come out alongside the same structured result the path-based operations report,
and the import graph contains no Node built-ins:
import {
mergePdfsBytes,
planSplitPdfBytes,
splitPdfBytes,
} from "@consultchimps/pdf/bytes";
const plan = await planSplitPdfBytes({
input: { name: file.name, bytes: new Uint8Array(await file.arrayBuffer()) },
});
console.log(plan.outputs.map((output) => output.path));
const { result, outputs } = await splitPdfBytes({
input: { name: file.name, bytes: new Uint8Array(await file.arrayBuffer()) },
signal: controller.signal,
onProgress: ({ completed, total }) => updateProgress(completed / total),
});
for (const output of outputs) {
download(output.name, output.bytes, output.mediaType);
}The workbook and presentation operations follow the same shape.
splitWorkbookBytes mirrors the path-based split, defaults included: with no
worksheet, Excel Table, or named range named, it filters every worksheet that
carries the column and each output keeps the rest of the workbook intact. Naming
one of those sources, or passing preserveWorkbook: false, selects the
single-source modes, which are also the only modes where includeBlank and
includeHiddenSheets apply. strict compares values without trimming, case
folding, or numeric coercion. mergeWorkbooksBytes combines every worksheet of
every input, keeping tabs and formatting and adding the same Sheet Index;
consolidateWorkbooksBytes stacks the rows of every visible worksheet that
holds data into one table (hidden and very hidden worksheets are skipped unless
includeHiddenSheets is set) with the same normalizeHeaders,
addSourceColumns, worksheet-selection, and header-row options as the
path-based consolidation; and populatePresentationBytes fills a template
presentation from either records you already hold or the bytes of a workbook.
import {
consolidateWorkbooksBytes,
mergeWorkbooksBytes,
planSplitWorkbookBytes,
splitWorkbookBytes,
} from "@consultchimps/xlsx/bytes";
import { populatePresentationBytes } from "@consultchimps/pptx/bytes";
const { outputs } = await splitWorkbookBytes({
input: { name: file.name, bytes: new Uint8Array(await file.arrayBuffer()) },
column: "Region",
});
// One combined table from several exports of the same schema, even when the
// headers drift between them.
const consolidated = await consolidateWorkbooksBytes({
inputs: [
{ name: "north.xlsx", bytes: northBytes },
{ name: "south.xlsx", bytes: southBytes },
],
normalizeHeaders: true,
outputName: "review-log",
signal: controller.signal,
onProgress: ({ stage, completed, total }) => {
console.log(`${stage}: ${completed}/${total}`);
},
});
console.log(consolidated.result.metrics.outputRows);
download(
consolidated.outputs[0].name,
consolidated.outputs[0].bytes,
consolidated.outputs[0].mediaType,
);
const populated = await populatePresentationBytes({
template: { name: "template.pptx", bytes: templateBytes },
records: [{ client: "North", amount: "10" }],
});consolidateWorkbookSources takes the same options but holds neither the inputs
nor the output whole. Each input is a random-access source, such as blobSource
over a browser File, read in pieces rather than whole, and the workbook goes
to your output sink chunk by chunk. The sink receives the bytes
consolidateWorkbooksBytes would return, and its abort runs when the
consolidation fails or is cancelled, to discard what was written. In a Web
Worker, an Origin Private File System sync access handle makes a sink that keeps
the output on disk.
import {
blobSource,
consolidateWorkbookSources,
} from "@consultchimps/xlsx/bytes";
// Any writable destination works; this one collects Blob parts.
let parts: Uint8Array[] = [];
const { outputName } = await consolidateWorkbookSources({
inputs: files.map((file) => blobSource(file.name, file)),
output: {
write: (chunk) => {
parts.push(chunk);
},
flush: () => Promise.resolve(),
// Runs when the consolidation fails or is cancelled.
abort: async () => {
parts = [];
},
},
});
const workbook = new File(parts, outputName);A template inspection reads a slide and writes nothing, so
inspectPresentationOutcomeBytes returns the structured result beside the
placeholder report rather than beside output bytes. Its metrics are the slide's
counts, and its warnings name every condition (malformed braces, unsupported
placements, no placeholders at all) that would make a population refuse the
template:
import { inspectPresentationOutcomeBytes } from "@consultchimps/pptx/bytes";
const { inspection, result } = await inspectPresentationOutcomeBytes(
{ name: "template.pptx", bytes: templateBytes },
{ templateSlide: 2 },
);
console.log(result.metrics.placeholderOccurrences);
for (const placeholder of inspection.placeholders) {
console.log(placeholder.name, placeholder.occurrences);
}
for (const warning of result.warnings) {
console.warn(warning);
}inspectPresentationBytes remains available and returns the placeholder report
on its own, for callers that want the reading without the operation result.
A workbook inspection works the same way. describeWorkbookBytes returns the
description beside the structured result, and the bytes surface also carries
readWorkbookExcelTablesBytes and readWorkbookNamedRangesBytes, the byte
twins of the path-based Excel Table and named-range readers:
import {
describeWorkbookBytes,
readWorkbookExcelTablesBytes,
} from "@consultchimps/xlsx/bytes";
const input = {
name: file.name,
bytes: new Uint8Array(await file.arrayBuffer()),
};
const { description } = await describeWorkbookBytes(input, {
sampleValues: 3,
});
renderSheetPicker(description.sheets);
const excelTables = await readWorkbookExcelTablesBytes(input);Output names default to the input name (report.pdf becomes
report-page-001.pdf, clients.xlsx becomes clients-North.xlsx, and
template.pptx becomes template-populated.pptx; workbook merges default to
merged.xlsx, workbook consolidations to consolidated.xlsx, and PDF merges to
combined.pdf) and are sanitized to stay portable, including on Windows.
Outputs are byte-deterministic, and a cancelled byte operation returns nothing
and leaves nothing behind. Artifact paths in the structured result carry these
output names rather than filesystem paths.
Explain results to people
The @consultchimps/messages package renders any OperationResult or
structured error as detailed, plain-language text, so a desktop or web interface
can reuse one voice.
import { formatHumanError, formatHumanResult } from "@consultchimps/messages";
console.log(formatHumanResult(result));Recovery guidance is worded for the interface that shows it. By default the
explanations use interface-neutral wording that never names a flag or an
executable. Pass a MessageVocabulary to match your own interface, or pass the
exported CLI_VOCABULARY to reproduce the wording the consultchimps command
prints.
import {
CLI_VOCABULARY,
formatHumanError,
GENERIC_VOCABULARY,
type MessageVocabulary,
} from "@consultchimps/messages";
console.error(
formatHumanError("The output file already exists.", "FILES_OUTPUT_EXISTS", {
vocabulary: CLI_VOCABULARY,
}),
);
// Start a custom interface's vocabulary from the neutral defaults so no
// command-line phrasing leaks through, then override what your interface
// presents differently.
const desktopVocabulary: MessageVocabulary = {
...GENERIC_VOCABULARY,
artifactListReference: "in the results panel",
retryWithOverwrite:
"If you intentionally want to replace the existing output, switch on Replace existing files and try again.",
};Populate a PowerPoint template
import { populatePowerPointTemplate } from "@consultchimps/pptx";
const result = await populatePowerPointTemplate({
templatePath: "profile-template.pptx",
workbookPath: "companies.xlsx",
headerRow: 1,
outputPath: "company-profiles.pptx",
});Set overwrite: true only when an existing output presentation should be
replaced. The first worksheet and first template slide are used by default; set
worksheet or templateSlide to select another one. The source template and
workbook remain unchanged.