From 8c1371ef3a9efbcd99f8e904c94226e913bfcd0f Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 29 Oct 2024 09:37:17 -0600 Subject: [PATCH] feat: dont use ink when size in greater than 10k (#32) * feat: dont use ink when size in greater than 10k * feat: use plain table in CI * test: bypass CI check --- src/table.tsx | 121 ++++++++++++++++++-------------------------- src/utils.ts | 25 +++++++++ test/table.test.tsx | 2 + 3 files changed, 76 insertions(+), 72 deletions(-) diff --git a/src/table.tsx b/src/table.tsx index 68045c0..002c722 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -3,6 +3,7 @@ import cliTruncate from 'cli-truncate' import {Box, Text, render} from 'ink' import {EventEmitter} from 'node:events' +import {env} from 'node:process' import {sha1} from 'object-hash' import React from 'react' import stripAnsi from 'strip-ansi' @@ -29,6 +30,7 @@ import { getHeadings, intersperse, maybeStripAnsi, + shouldUsePlainTable, sortData, } from './utils.js' @@ -436,7 +438,7 @@ export function Skeleton(props: React.PropsWithChildren & {readonly height?: num } type FakeStdout = { - lastFrame: () => string | undefined + lastFrame: () => string } & NodeJS.WriteStream /** @@ -482,99 +484,74 @@ class Output { this.stream = createStdout() } - public maybePrintLastFrame() { + public printLastFrame() { process.stdout.write(`${this.stream.lastFrame()}\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 renderPlainTable>(props: TableOptions): void { + const {columns, headings, processedData, title} = setup(props) + + if (title) console.log(title) + const headerString = columns.reduce((acc, column) => { + const {horizontalAlignment, overflow, padding, width} = column + const {marginLeft, marginRight, text} = formatTextWithMargins({ + horizontalAlignment, + overflow, + padding, + value: column.column, + width, + }) + const columnHeader = headings[text] ?? text + return `${acc}${' '.repeat(marginLeft)}${columnHeader}${' '.repeat(marginRight)}` + }, '') + console.log(headerString) + console.log('-'.repeat(headerString.length)) -function renderTableInChunks>(props: TableOptions): void { - const { - columns, - config, - dataComponent, - footerComponent, - headerComponent, - headerFooterComponent, - headingComponent, - headings, - processedData, - separatorComponent, - title, - titleOptions, - } = setup(props) + for (const row of processedData) { + const stringToPrint = columns.reduce((acc, column) => { + const {horizontalAlignment, overflow, padding, width} = column + const value = row[column.column] - 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, Math.floor(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() + if (value === undefined || value === null) { + return `${acc}${' '.repeat(width)}` + } + + const {marginLeft, marginRight, text} = formatTextWithMargins({ + horizontalAlignment, + overflow, + padding, + value, + width, + }) + + return `${acc}${' '.repeat(marginLeft)}${text}${' '.repeat(marginRight)}` + }, '') + console.log(stringToPrint) } - const footerOutput = new Output() - const footerInstance = render( - - {footerComponent({columns, data: {}, key: 'footer'})} - , - {stdout: footerOutput.stream}, - ) - footerInstance.unmount() - footerOutput.maybePrintLastFrame() + console.log() } /** - * Prints a table based on the provided options. If the data length exceeds 50,000 entries, - * the table is rendered in chunks to handle large datasets efficiently. + * Prints a table based on the provided options. If the data length exceeds 10,000 entries, + * the table is rendered in a non-styled format to avoid memory issues. * * @template T - A generic type that extends a record with string keys and unknown values. * @param {TableOptions} options - The options for rendering the table, including data and other configurations. * @returns {void} */ export function printTable>(options: TableOptions): void { - if (options.data.length > 50_000) { - renderTableInChunks(options) + const limit = Number.parseInt(env.OCLIF_TABLE_LIMIT ?? env.SF_TABLE_LIMIT ?? '10000', 10) ?? 10_000 + if (options.data.length >= limit || shouldUsePlainTable()) { + renderPlainTable(options) return } const output = new Output() const instance = render(, {stdout: output.stream}) instance.unmount() - output.maybePrintLastFrame() + output.printLastFrame() } /** @@ -635,5 +612,5 @@ export function printTables[]>( {stdout: output.stream}, ) instance.unmount() - output.maybePrintLastFrame() + output.printLastFrame() } diff --git a/src/utils.ts b/src/utils.ts index 109bb88..3792734 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import {camelCase, capitalCase, constantCase, kebabCase, pascalCase, sentenceCase, snakeCase} from 'change-case' import {orderBy} from 'natural-orderby' +import {env} from 'node:process' import stripAnsi from 'strip-ansi' import {Column, ColumnProps, Config, Sort} from './types.js' @@ -179,3 +180,27 @@ export function maybeStripAnsi[]>(data: T, noS return newData as T } + +function isTruthy(value: string | undefined): boolean { + return value !== '0' && value !== 'false' +} + +/** + * Determines whether the plain text table should be used. + * + * If the OCLIF_TABLE_SKIP_CI_CHECK environment variable is set to a truthy value, the CI check will be skipped. + * + * If the CI environment variable is set, the plain text table will be used. + * + * @returns {boolean} True if the plain text table should be used, false otherwise. + */ +export function shouldUsePlainTable(): boolean { + if (env.OCLIF_TABLE_SKIP_CI_CHECK && isTruthy(env.OCLIF_TABLE_SKIP_CI_CHECK)) return false + // Inspired by https://github.com/sindresorhus/is-in-ci + if ( + isTruthy(env.CI) && + ('CI' in env || 'CONTINUOUS_INTEGRATION' in env || Object.keys(env).some((key) => key.startsWith('CI_'))) + ) + return true + return false +} diff --git a/test/table.test.tsx b/test/table.test.tsx index d063251..25d4ca0 100644 --- a/test/table.test.tsx +++ b/test/table.test.tsx @@ -10,6 +10,8 @@ import {Cell, Header, Skeleton, Table, formatTextWithMargins, printTable} from ' config.truncateThreshold = 0 +process.env.OCLIF_TABLE_SKIP_CI_CHECK = 'true' + // Helpers ------------------------------------------------------------------- const skeleton = (v: string) => {v}