diff --git a/examples/sf-specific/deploy.ts b/examples/sf-specific/deploy.ts index c471176..d7b9722 100644 --- a/examples/sf-specific/deploy.ts +++ b/examples/sf-specific/deploy.ts @@ -941,7 +941,7 @@ const deployResult = [ ] const deploy: TableOptions<(typeof deployResult)[number]> = { - borderStyle: 'vertical', + borderStyle: 'vertical-with-outline', columns: [ 'state', 'fullName', @@ -949,15 +949,20 @@ const deploy: TableOptions<(typeof deployResult)[number]> = { { key: 'filePath', name: 'Path', - overflow: 'truncate', + overflow: 'wrap', + }, + { + key: 'filePath', + name: 'Path', + overflow: 'wrap', }, ], data: deployResult, filter: (row) => row.state === 'Changed' && row.type.startsWith('A'), headerOptions: { - color: 'white', + // color: 'white', formatter: 'capitalCase', - inverse: true, + // inverse: true, }, maxWidth: '100%', overflow: 'truncate', diff --git a/examples/very-tall.ts b/examples/very-tall.ts new file mode 100644 index 0000000..5c27174 --- /dev/null +++ b/examples/very-tall.ts @@ -0,0 +1,15 @@ +import {printTable} from '../src/index.js' + +const height = 100_000 +const data = Array.from({length: height}, (_, i) => ({age: i, name: `Foo ${i}`})) + +printTable({ + columns: ['name', 'age'], + data, + headerOptions: { + formatter: 'capitalCase', + }, + horizontalAlignment: 'center', + title: 'Very Tall', + titleOptions: {bold: true}, +}) diff --git a/src/table.tsx b/src/table.tsx index 0d54e32..23c01c3 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -184,7 +184,7 @@ export function formatTextWithMargins({ } } -export function Table>(props: TableOptions) { +function setup>(props: TableOptions) { const { data, filter, @@ -264,6 +264,38 @@ export function Table>(props: TableOptions) skeleton: BORDER_SKELETONS[config.borderStyle].separator, }) + return { + columns, + config, + dataComponent, + footerComponent, + headerComponent, + headerFooterComponent, + headingComponent, + headings, + processedData, + separatorComponent, + title, + titleOptions, + } +} + +export function Table>(props: TableOptions) { + const { + columns, + config, + dataComponent, + footerComponent, + headerComponent, + headerFooterComponent, + headingComponent, + headings, + processedData, + separatorComponent, + title, + titleOptions, + } = setup(props) + return ( {title && {title}} @@ -395,7 +427,12 @@ class Stream extends WriteStream { private frames: string[] = [] public lastFrame(): string | undefined { - return this.frames.filter((f) => stripAnsi(f) !== '').at(-1) + return this.frames + .filter((f) => { + const stripped = stripAnsi(f) + return stripped !== '' && stripped !== '\n' + }) + .at(-1) } write(data: string): boolean { @@ -413,18 +450,93 @@ class Output { public maybePrintLastFrame() { if (this.stream instanceof Stream) { - process.stdout.write(`${this.stream.lastFrame()}\n`) + process.stdout.write(`${this.stream.lastFrame()}`) } else { process.stdout.write('\n') } } } +function chunk(array: T[], size: number): T[][] { + return array.reduce((acc, _, i) => { + if (i % size === 0) acc.push(array.slice(i, i + size)) + return acc + }, [] as T[][]) +} + +function renderTableInChunks>(props: TableOptions): void { + const { + columns, + config, + dataComponent, + footerComponent, + headerComponent, + headerFooterComponent, + headingComponent, + headings, + processedData, + separatorComponent, + title, + titleOptions, + } = setup(props) + + const headerOutput = new Output() + const headerInstance = render( + + {title && {title}} + {headerComponent({columns, data: {}, key: 'header'})} + {headingComponent({columns, data: headings, key: 'heading'})} + {headerFooterComponent({columns, data: {}, key: 'footer'})} + , + {stdout: headerOutput.stream}, + ) + headerInstance.unmount() + headerOutput.maybePrintLastFrame() + + const chunks = chunk(processedData, Math.max(1, process.stdout.rows / 2)) + for (const chunk of chunks) { + const chunkOutput = new Output() + const instance = render( + + {chunk.map((row, index) => { + // Calculate the hash of the row based on its value and position + const key = `row-${sha1(row)}-${index}` + + // Construct a row. + return ( + + {separatorComponent({columns, data: {}, key: `separator-${key}`})} + {dataComponent({columns, data: row, key: `data-${key}`})} + + ) + })} + , + {stdout: chunkOutput.stream}, + ) + instance.unmount() + chunkOutput.maybePrintLastFrame() + } + + const footerOutput = new Output() + const footerInstance = render( + + {footerComponent({columns, data: {}, key: 'footer'})} + , + {stdout: footerOutput.stream}, + ) + footerInstance.unmount() + footerOutput.maybePrintLastFrame() +} + /** * Renders a table with the given data. * @param options see {@link TableOptions} */ export function printTable>(options: TableOptions): void { + if (options.data.length > 50_000) { + renderTableInChunks(options) + } + const output = new Output() const instance = render(, {stdout: output.stream}) instance.unmount() @@ -443,6 +555,10 @@ export function printTables[]>( tables: {[P in keyof T]: TableOptions}, options?: Omit, ): void { + if (tables.some((table) => table.data.length > 50_000)) { + throw new Error('The data is too large to print multiple tables. Please use `printTable` instead.') + } + const output = new Output() const leftMargin = options?.marginLeft ?? options?.margin ?? 0 const rightMargin = options?.marginRight ?? options?.margin ?? 0