• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

humanspeak / svelte-headless-table / 26344760811

23 May 2026 10:05PM UTC coverage: 80.596%. First build
26344760811

push

github

web-flow
perf: instrument view-model + drop Map detour in getColumnedBodyRows (#216)

* perf(bench): add headless-chromium perf-bench harness + baseline

Mirrors the svelte-markdown perf-bench pattern (fixture page + headless
runner) to give the table a structured, commit-attributable surface for
performance work. Eight scenarios cover the row-side, column-side, and
plugin-interaction hot paths called out in
.notes/performance-optimization-plan.md: rows-1k, rows-10k, columns-50,
column-reorder-1k, group-by-1k, sort-cycle-1k, subrows-tree-1k, and
kitchen-sink-1k.

The fixture exposes per-scenario build/render timings, the
vm._debug.derivationCalls breakdown (the non-wall-clock signal that
doesn't move with hardware noise), interaction-paint latency for sort
cycle / column reorder / expand-all, and the same rolling-10s observer
aggregates the markdown bench uses. The runner does COLD + WARM passes
with --enable-precise-memory-info and dumps a JSON blob suitable for
piping into scripts/perf-baseline.json for before/after diffs in PR
bodies.

- scripts/perf-bench.mjs — port of svelte-markdown's runner with
  table-shaped stat parser, defaults to http://localhost:8417.
- src/routes/test/perf-bench/+page.svelte — eight scenarios, all
  building fresh tables + columns + view-models and snapshotting
  derivation counters per run; _PerfTable.svelte is keyed so each
  scenario remounts cleanly.
- scripts/perf-baseline.json — initial baseline capture so the next
  perf commit can show a delta in its PR description.
- package.json — pnpm perf:bench script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(debug): expose per-derivation timings on vm._debug

Adds `derivationTimings` (parallel to `derivationCalls`) plus
`getTotalMs()` on the view-model's debug surface so we can attribute a
scenario's render budget to a specific derivation, not just the
aggregated `firstPaintMs`. Each `derived(...)` body in
createViewModel.ts now wraps its work in ... (continued)

479 of 648 branches covered (73.92%)

Branch coverage included in aggregate %.

25 of 26 new or added lines in 2 files covered. (96.15%)

1332 of 1599 relevant lines covered (83.3%)

1506.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.65
/src/lib/createViewModel.ts
1
import { BodyRow, DataBodyRow, getBodyRows, getColumnedBodyRows } from '$lib/bodyRows.js'
2
import { FlatColumn, getFlatColumns, type Column } from '$lib/columns.js'
3
import type { Table } from '$lib/createTable.js'
4
import { getHeaderRows, HeaderRow } from '$lib/headerRows.js'
5
import type {
6
    AnyPlugins,
7
    DeriveFlatColumnsFn,
8
    DeriveFn,
9
    DeriveRowsFn,
10
    PluginStates
11
} from '$lib/types/TablePlugin.js'
12
import { finalizeAttributes } from '$lib/utils/attributes.js'
13
import { nonUndefined } from '$lib/utils/filter.js'
14
import { derived, readable, writable, type Readable, type Writable } from 'svelte/store'
15

16
/**
17
 * HTML attributes for the table element.
18
 *
19
 * @template Item - The type of data items in the table.
20
 * @template Plugins - The plugins used by the table.
21
 */
22
/* trunk-ignore(eslint/no-unused-vars,eslint/@typescript-eslint/no-unused-vars) */
23
export type TableAttributes<Item, Plugins extends AnyPlugins = AnyPlugins> = Record<
24
    string,
25
    unknown
26
> & {
27
    role: 'table'
28
}
29

30
/**
31
 * HTML attributes for the table head element.
32
 *
33
 * @template Item - The type of data items in the table.
34
 * @template Plugins - The plugins used by the table.
35
 */
36
/* trunk-ignore(eslint/no-unused-vars,eslint/@typescript-eslint/no-unused-vars) */
37
export type TableHeadAttributes<Item, Plugins extends AnyPlugins = AnyPlugins> = Record<
38
    string,
39
    unknown
40
>
41

42
/**
43
 * HTML attributes for the table body element.
44
 *
45
 * @template Item - The type of data items in the table.
46
 * @template Plugins - The plugins used by the table.
47
 */
48
/* trunk-ignore(eslint/no-unused-vars,eslint/@typescript-eslint/no-unused-vars) */
49
export type TableBodyAttributes<Item, Plugins extends AnyPlugins = AnyPlugins> = Record<
50
    string,
51
    unknown
52
> & {
53
    role: 'rowgroup'
54
}
55

56
/**
57
 * Debug information for performance analysis of the table view model.
58
 * Provides metrics about derivation chains and call counts.
59
 */
60
export interface ViewModelDebug {
61
    /** Number of plugins active */
62
    pluginCount: number
63
    /** Names of active plugins */
64
    pluginNames: string[]
65
    /** Number of derived stores in each chain */
66
    derivedStoreCount: {
67
        tableAttrs: number
68
        tableHeadAttrs: number
69
        tableBodyAttrs: number
70
        visibleColumns: number
71
        rows: number
72
        pageRows: number
73
    }
74
    /** Counters that increment on each derivation execution */
75
    derivationCalls: {
76
        tableAttrs: number
77
        tableHeadAttrs: number
78
        tableBodyAttrs: number
79
        visibleColumns: number
80
        columnedRows: number
81
        rows: number
82
        injectedRows: number
83
        pageRows: number
84
        injectedPageRows: number
85
        headerRows: number
86
    }
87
    /**
88
     * Per-derivation cumulative wall-clock in milliseconds, accumulated
89
     * via `performance.now()` deltas inside each `derived(...)` body.
90
     * Mirrors `derivationCalls` so the perf bench can attribute a
91
     * scenario's render budget to a specific derivation rather than the
92
     * aggregated `firstPaintMs`. Reset by `resetCounters()`.
93
     */
94
    derivationTimings: {
95
        tableAttrs: number
96
        tableHeadAttrs: number
97
        tableBodyAttrs: number
98
        visibleColumns: number
99
        columnedRows: number
100
        rows: number
101
        injectedRows: number
102
        pageRows: number
103
        injectedPageRows: number
104
        headerRows: number
105
    }
106
    /** Reset all derivation call counters and timings to 0 */
107
    resetCounters: () => void
108
    /** Get total derivation calls since last reset */
109
    getTotalCalls: () => number
110
    /** Get total derivation wall-clock (ms) since last reset */
111
    getTotalMs: () => number
112
}
113

114
/**
115
 * The view model for a table, containing all reactive stores and state.
116
 * Created by `createViewModel` and used to render the table.
117
 *
118
 * @template Item - The type of data items in the table.
119
 * @template Plugins - The plugins used by the table.
120
 */
121
export interface TableViewModel<Item, Plugins extends AnyPlugins = AnyPlugins> {
122
    flatColumns: FlatColumn<Item, Plugins>[]
123
    tableAttrs: Readable<TableAttributes<Item, Plugins>>
124
    tableHeadAttrs: Readable<TableHeadAttributes<Item, Plugins>>
125
    tableBodyAttrs: Readable<TableBodyAttributes<Item, Plugins>>
126
    visibleColumns: Readable<FlatColumn<Item, Plugins>[]>
127
    headerRows: Readable<HeaderRow<Item, Plugins>[]>
128
    originalRows: Readable<BodyRow<Item, Plugins>[]>
129
    rows: Readable<DataBodyRow<Item, Plugins>[]>
130
    pageRows: Readable<DataBodyRow<Item, Plugins>[]>
131
    pluginStates: PluginStates<Plugins>
132
    /** Debug information for performance analysis (always available) */
133
    _debug: ViewModelDebug
134
}
135

136
/**
137
 * A type that can be either Readable or Writable.
138
 *
139
 * @template T - The type of the store value.
140
 */
141
export type ReadOrWritable<T> = Readable<T> | Writable<T>
142

143
/**
144
 * The table state passed to plugins during initialization.
145
 * Contains references to all table stores before plugin states are available.
146
 *
147
 * @template Item - The type of data items in the table.
148
 * @template Plugins - The plugins used by the table.
149
 */
150
export interface PluginInitTableState<Item, Plugins extends AnyPlugins = AnyPlugins> extends Omit<
151
    TableViewModel<Item, Plugins>,
152
    'pluginStates' | '_debug'
153
> {
154
    /** The data source for the table. */
155
    data: ReadOrWritable<Item[]>
156
    /** The column definitions. */
157
    columns: Column<Item, Plugins>[]
158
}
159

160
/**
161
 * The complete table state including plugin states.
162
 * Available to plugins and components after initialization.
163
 *
164
 * @template Item - The type of data items in the table.
165
 * @template Plugins - The plugins used by the table.
166
 */
167
export interface TableState<Item, Plugins extends AnyPlugins = AnyPlugins> extends Omit<
168
    TableViewModel<Item, Plugins>,
169
    '_debug'
170
> {
171
    /** The data source for the table. */
172
    data: ReadOrWritable<Item[]>
173
    /** The column definitions. */
174
    columns: Column<Item, Plugins>[]
175
}
176

177
/**
178
 * Options for creating a table view model.
179
 *
180
 * @template Item - The type of data items in the table.
181
 */
182
export interface CreateViewModelOptions<Item> {
183
    /** Optional function to generate a unique ID for each data item. */
184
    /* trunk-ignore(eslint/no-unused-vars) */
185
    rowDataId?: (item: Item, index: number) => string
186
}
187

188
/**
189
 * Creates a view model for rendering a table.
190
 * The view model contains all reactive stores for the table, headers, and rows.
191
 *
192
 * @template Item - The type of data items in the table.
193
 * @template Plugins - The plugins used by the table.
194
 * @param table - The table instance created by `createTable`.
195
 * @param columns - The column definitions.
196
 * @param options - Optional configuration options.
197
 * @returns A TableViewModel containing all reactive stores for rendering.
198
 */
199
export const createViewModel = <Item, Plugins extends AnyPlugins = AnyPlugins>(
21✔
200
    table: Table<Item, Plugins>,
201
    columns: Column<Item, Plugins>[],
202
    { rowDataId }: CreateViewModelOptions<Item> = {}
151✔
203
): TableViewModel<Item, Plugins> => {
204
    const { data, plugins } = table
151✔
205

206
    // Initialize derivation call counters for debug instrumentation
207
    const derivationCalls = {
151✔
208
        tableAttrs: 0,
209
        tableHeadAttrs: 0,
210
        tableBodyAttrs: 0,
211
        visibleColumns: 0,
212
        columnedRows: 0,
213
        rows: 0,
214
        injectedRows: 0,
215
        pageRows: 0,
216
        injectedPageRows: 0,
217
        headerRows: 0
218
    }
219
    // Per-derivation cumulative ms, populated alongside derivationCalls.
220
    // Each `derived(...)` body wraps its work in performance.now() pairs
221
    // so the perf bench can attribute a scenario's render budget to a
222
    // specific derivation. `rows` / `pageRows` stay at 0 — they're
223
    // plugin-pipeline pass-throughs that don't run a body of their own.
224
    const derivationTimings = {
151✔
225
        tableAttrs: 0,
226
        tableHeadAttrs: 0,
227
        tableBodyAttrs: 0,
228
        visibleColumns: 0,
229
        columnedRows: 0,
230
        rows: 0,
231
        injectedRows: 0,
232
        pageRows: 0,
233
        injectedPageRows: 0,
234
        headerRows: 0
235
    }
236

237
    const $flatColumns = getFlatColumns(columns)
151✔
238
    const flatColumns = readable($flatColumns)
151✔
239

240
    const originalRows = derived([data, flatColumns], ([$data, $flatColumns]) => {
151✔
241
        return getBodyRows($data, $flatColumns, { rowDataId })
142✔
242
    })
243

244
    // _stores need to be defined first to pass into plugins for initialization.
245
    const _visibleColumns = writable<FlatColumn<Item, Plugins>[]>([])
151✔
246
    const _headerRows = writable<HeaderRow<Item, Plugins>[]>()
151✔
247
    const _rows = writable<DataBodyRow<Item, Plugins>[]>([])
151✔
248
    const _pageRows = writable<DataBodyRow<Item, Plugins>[]>([])
151✔
249
    const _tableAttrs = writable<TableAttributes<Item>>({
151✔
250
        role: 'table' as const
251
    })
252
    const _tableHeadAttrs = writable<TableHeadAttributes<Item>>({})
151✔
253
    const _tableBodyAttrs = writable<TableBodyAttributes<Item>>({
151✔
254
        role: 'rowgroup' as const
255
    })
256
    const pluginInitTableState: PluginInitTableState<Item, Plugins> = {
151✔
257
        data,
258
        columns,
259
        flatColumns: $flatColumns,
260
        tableAttrs: _tableAttrs,
261
        tableHeadAttrs: _tableHeadAttrs,
262
        tableBodyAttrs: _tableBodyAttrs,
263
        visibleColumns: _visibleColumns,
264
        headerRows: _headerRows,
265
        originalRows,
266
        rows: _rows,
267
        pageRows: _pageRows
268
    }
269

270
    const pluginInstances = Object.fromEntries(
151✔
271
        Object.entries(plugins).map(([pluginName, plugin]) => {
272
            const columnOptions = Object.fromEntries(
185✔
273
                $flatColumns
274
                    .map((c) => {
275
                        const option = c.plugins?.[pluginName]
277✔
276
                        if (option === undefined) return undefined
277✔
277
                        return [c.id, option] as const
27✔
278
                    })
279
                    .filter(nonUndefined)
280
            )
281
            return [
185✔
282
                pluginName,
283
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
284
                plugin({ pluginName, tableState: pluginInitTableState as any, columnOptions })
285
            ]
286
        })
287
    ) as {
288
        [K in keyof Plugins]: ReturnType<Plugins[K]>
289
    }
290

291
    const pluginStates = Object.fromEntries(
151✔
292
        Object.entries(pluginInstances).map(([key, pluginInstance]) => [
185✔
293
            key,
294
            pluginInstance.pluginState
295
        ])
296
    ) as PluginStates<Plugins>
297

298
    const tableState: TableState<Item, Plugins> = {
151✔
299
        data,
300
        columns,
301
        flatColumns: $flatColumns,
302
        tableAttrs: _tableAttrs,
303
        tableHeadAttrs: _tableHeadAttrs,
304
        tableBodyAttrs: _tableBodyAttrs,
305
        visibleColumns: _visibleColumns,
306
        headerRows: _headerRows,
307
        originalRows,
308
        rows: _rows,
309
        pageRows: _pageRows,
310
        pluginStates
311
    }
312

313
    const deriveTableAttrsFns: DeriveFn<TableAttributes<Item>>[] = Object.values(pluginInstances)
151✔
314
        .map((pluginInstance) => pluginInstance.deriveTableAttrs)
185✔
315
        .filter(nonUndefined)
316
    let tableAttrs = readable<TableAttributes<Item>>({
151✔
317
        role: 'table'
318
    })
319
    deriveTableAttrsFns.forEach((fn) => {
151✔
320
        tableAttrs = fn(tableAttrs)
7✔
321
    })
322
    const finalizedTableAttrs = derived(tableAttrs, ($tableAttrs) => {
151✔
323
        const _t0 = performance.now()
3✔
324
        derivationCalls.tableAttrs++
3✔
325
        const $finalizedAttrs = finalizeAttributes($tableAttrs) as TableAttributes<Item>
3✔
326
        _tableAttrs.set($finalizedAttrs)
3✔
327
        derivationTimings.tableAttrs += performance.now() - _t0
3✔
328
        return $finalizedAttrs
3✔
329
    })
330

331
    const deriveTableHeadAttrsFns: DeriveFn<TableHeadAttributes<Item>>[] = Object.values(
151✔
332
        pluginInstances
333
    )
334
        .map((pluginInstance) => pluginInstance.deriveTableBodyAttrs)
185✔
335
        .filter(nonUndefined)
336
    let tableHeadAttrs = readable<TableHeadAttributes<Item>>({})
151✔
337
    deriveTableHeadAttrsFns.forEach((fn) => {
151✔
338
        tableHeadAttrs = fn(tableHeadAttrs)
7✔
339
    })
340
    const finalizedTableHeadAttrs = derived(tableHeadAttrs, ($tableHeadAttrs) => {
151✔
341
        const _t0 = performance.now()
1✔
342
        derivationCalls.tableHeadAttrs++
1✔
343
        const $finalizedAttrs = finalizeAttributes($tableHeadAttrs) as TableHeadAttributes<Item>
1✔
344
        _tableHeadAttrs.set($finalizedAttrs)
1✔
345
        derivationTimings.tableHeadAttrs += performance.now() - _t0
1✔
346
        return $finalizedAttrs
1✔
347
    })
348

349
    const deriveTableBodyAttrsFns: DeriveFn<TableBodyAttributes<Item>>[] = Object.values(
151✔
350
        pluginInstances
351
    )
352
        .map((pluginInstance) => pluginInstance.deriveTableBodyAttrs)
185✔
353
        .filter(nonUndefined)
354
    let tableBodyAttrs = readable<TableBodyAttributes<Item>>({
151✔
355
        role: 'rowgroup'
356
    })
357
    deriveTableBodyAttrsFns.forEach((fn) => {
151✔
358
        tableBodyAttrs = fn(tableBodyAttrs)
7✔
359
    })
360
    const finalizedTableBodyAttrs = derived(tableBodyAttrs, ($tableBodyAttrs) => {
151✔
361
        const _t0 = performance.now()
2✔
362
        derivationCalls.tableBodyAttrs++
2✔
363
        const $finalizedAttrs = finalizeAttributes($tableBodyAttrs) as TableBodyAttributes<Item>
2✔
364
        _tableBodyAttrs.set($finalizedAttrs)
2✔
365
        derivationTimings.tableBodyAttrs += performance.now() - _t0
2✔
366
        return $finalizedAttrs
2✔
367
    })
368

369
    const deriveFlatColumnsFns: DeriveFlatColumnsFn<Item>[] = Object.values(pluginInstances)
151✔
370
        .map((pluginInstance) => pluginInstance.deriveFlatColumns)
185✔
371
        .filter(nonUndefined)
372

373
    let visibleColumns = flatColumns
151✔
374
    deriveFlatColumnsFns.forEach((fn) => {
151✔
375
        // Variance of generic type here is unstable. Not sure how to fix.
376
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
377
        visibleColumns = fn(visibleColumns as any) as any
11✔
378
    })
379

380
    const injectedColumns = derived(visibleColumns, ($visibleColumns) => {
151✔
381
        const _t0 = performance.now()
158✔
382
        derivationCalls.visibleColumns++
158✔
383
        _visibleColumns.set($visibleColumns)
158✔
384
        derivationTimings.visibleColumns += performance.now() - _t0
158✔
385
        return $visibleColumns
158✔
386
    })
387

388
    const columnedRows = derived(
151✔
389
        [originalRows, injectedColumns],
390
        ([$originalRows, $injectedColumns]) => {
391
            const _t0 = performance.now()
142✔
392
            derivationCalls.columnedRows++
142✔
393
            const result = getColumnedBodyRows(
142✔
394
                $originalRows,
395
                $injectedColumns.map((c) => c.id)
197✔
396
            )
397
            derivationTimings.columnedRows += performance.now() - _t0
142✔
398
            return result
142✔
399
        }
400
    )
401

402
    const deriveRowsFns: DeriveRowsFn<Item>[] = Object.values(pluginInstances)
151✔
403
        .map((pluginInstance) => pluginInstance.deriveRows)
185✔
404
        .filter(nonUndefined)
405

406
    let rows = columnedRows
151✔
407
    deriveRowsFns.forEach((fn) => {
151✔
408
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
409
        rows = fn(rows as any) as any
97✔
410
    })
411

412
    const pluginEntries = Object.entries(pluginInstances)
151✔
413

414
    const injectedRows = derived(rows, ($rows) => {
151✔
415
        const _t0 = performance.now()
142✔
416
        derivationCalls.injectedRows++
142✔
417
        $rows.forEach((row) => {
142✔
418
            row.injectState(tableState)
2,908✔
419
            row.cells.forEach((cell) => cell.injectState(tableState))
4,852✔
420
            for (const [pluginName, pluginInstance] of pluginEntries) {
2,908✔
421
                const trHook = pluginInstance.hooks?.['tbody.tr']
8,456✔
422
                if (trHook !== undefined) {
8,456✔
423
                    row.applyHook(pluginName, trHook(row))
595✔
424
                }
425
                const tdHook = pluginInstance.hooks?.['tbody.tr.td']
8,456✔
426
                if (tdHook !== undefined) {
8,456✔
427
                    row.cells.forEach((cell) => cell.applyHook(pluginName, tdHook(cell)))
3,721✔
428
                }
429
            }
430
        })
431
        _rows.set($rows)
142✔
432
        derivationTimings.injectedRows += performance.now() - _t0
142✔
433
        return $rows
142✔
434
    })
435

436
    const derivePageRowsFns: DeriveRowsFn<Item>[] = Object.values(pluginInstances)
151✔
437
        .map((pluginInstance) => pluginInstance.derivePageRows)
185✔
438
        .filter(nonUndefined)
439

440
    // Must derive from `injectedRows` instead of `rows` to ensure that `_rows` is set.
441
    let pageRows = injectedRows
151✔
442
    derivePageRowsFns.forEach((fn) => {
151✔
443
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
444
        pageRows = fn(pageRows as any) as any
37✔
445
    })
446

447
    // Page rows are a subset of the same object references already processed
448
    // by injectedRows — no need to re-inject state or re-apply hooks.
449
    const injectedPageRows = derived(pageRows, ($pageRows) => {
151✔
450
        const _t0 = performance.now()
42✔
451
        derivationCalls.injectedPageRows++
42✔
452
        _pageRows.set($pageRows)
42✔
453
        derivationTimings.injectedPageRows += performance.now() - _t0
42✔
454
        return $pageRows
42✔
455
    })
456

457
    const headerRows = derived(injectedColumns, ($injectedColumns) => {
151✔
458
        const _t0 = performance.now()
11✔
459
        derivationCalls.headerRows++
11✔
460
        const $headerRows = getHeaderRows(
11✔
461
            columns,
462
            $injectedColumns.map((c) => c.id)
12✔
463
        )
464
        $headerRows.forEach((row) => {
11✔
465
            row.injectState(tableState)
10✔
466
            row.cells.forEach((cell) => cell.injectState(tableState))
12✔
467
            for (const [pluginName, pluginInstance] of pluginEntries) {
10✔
468
                const trHook = pluginInstance.hooks?.['thead.tr']
10✔
469
                if (trHook !== undefined) {
10✔
470
                    row.applyHook(pluginName, trHook(row))
2✔
471
                }
472
                const thHook = pluginInstance.hooks?.['thead.tr.th']
10✔
473
                if (thHook !== undefined) {
10!
474
                    row.cells.forEach((cell) => cell.applyHook(pluginName, thHook(cell)))
12✔
475
                }
476
            }
477
        })
478
        _headerRows.set($headerRows)
11✔
479
        derivationTimings.headerRows += performance.now() - _t0
11✔
480
        return $headerRows
11✔
481
    })
482

483
    const _debug: ViewModelDebug = {
151✔
484
        pluginCount: Object.keys(plugins).length,
485
        pluginNames: Object.keys(plugins),
486
        derivedStoreCount: {
487
            tableAttrs: deriveTableAttrsFns.length + 1, // +1 for finalized
488
            tableHeadAttrs: deriveTableHeadAttrsFns.length + 1,
489
            tableBodyAttrs: deriveTableBodyAttrsFns.length + 1,
490
            visibleColumns: deriveFlatColumnsFns.length + 1, // +1 for injected
491
            rows: deriveRowsFns.length + 2, // +2 for columned + injected
492
            pageRows: derivePageRowsFns.length + 1 // +1 for injected
493
        },
494
        derivationCalls,
495
        derivationTimings,
496
        resetCounters: () => {
497
            Object.keys(derivationCalls).forEach((key) => {
2✔
498
                derivationCalls[key as keyof typeof derivationCalls] = 0
20✔
499
                derivationTimings[key as keyof typeof derivationTimings] = 0
20✔
500
            })
501
        },
502
        getTotalCalls: () => {
503
            return Object.values(derivationCalls).reduce((sum, count) => sum + count, 0)
40✔
504
        },
505
        getTotalMs: () => {
NEW
506
            return Object.values(derivationTimings).reduce((sum, ms) => sum + ms, 0)
×
507
        }
508
    }
509

510
    return {
151✔
511
        tableAttrs: finalizedTableAttrs,
512
        tableHeadAttrs: finalizedTableHeadAttrs,
513
        tableBodyAttrs: finalizedTableBodyAttrs,
514
        visibleColumns: injectedColumns,
515
        flatColumns: $flatColumns,
516
        headerRows,
517
        originalRows,
518
        rows: injectedRows,
519
        pageRows: injectedPageRows,
520
        pluginStates,
521
        _debug
522
    }
523
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc