From 8bcc0f66e10851fd0ac6dd42ea53b610a3627f81 Mon Sep 17 00:00:00 2001 From: koreahghg Date: Fri, 21 Aug 2026 16:19:10 +0900 Subject: [PATCH] fix(table-core): guard process.env.NODE_ENV checks for bundler-less environments Raw process.env.NODE_ENV reads in ~14 dev-only debug/validation checks survive unguarded into the published ESM build. Any environment with no process global (e.g. vanilla JS loaded via an import map, no bundler or Node.js runtime) throws ReferenceError: process is not defined the first time one of these checks runs (GH #6078). Add a shared isDevelopmentEnv() helper that checks typeof process first, and route every call site through it. Behavior/semantics are unchanged (still === 'development', not flipped to !== 'production'). Co-Authored-By: Claude Sonnet 5 --- .changeset/guard-process-env-checks.md | 5 ++ .../src/core/columns/constructColumn.ts | 5 +- .../core/columns/coreColumnsFeature.utils.ts | 8 +- .../src/core/rows/coreRowsFeature.utils.ts | 4 +- .../src/core/table/constructTable.ts | 4 +- .../columnFilteringFeature.utils.ts | 11 ++- .../globalFilteringFeature.utils.ts | 8 +- .../rowAggregationFeature.utils.ts | 4 +- .../row-sorting/rowSortingFeature.utils.ts | 6 +- packages/table-core/src/utils.ts | 90 +++++++++++-------- .../src/worker/createWorkerRowModel.ts | 4 +- packages/table-core/tests/unit/utils.test.ts | 50 ++++++++++- 12 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 .changeset/guard-process-env-checks.md diff --git a/.changeset/guard-process-env-checks.md b/.changeset/guard-process-env-checks.md new file mode 100644 index 0000000000..1ae1d22a08 --- /dev/null +++ b/.changeset/guard-process-env-checks.md @@ -0,0 +1,5 @@ +--- +'@tanstack/table-core': patch +--- + +Guard the `process` global before reading `process.env.NODE_ENV` in development-only debug and validation checks. Raw `process.env.NODE_ENV` reads survived unguarded into the published ESM build, so any environment without a `process` global (e.g. vanilla JS loaded via an import map, or another bundler-less setup) threw `ReferenceError: process is not defined` the first time one of these checks ran. All ~14 call sites now go through a shared `isDevelopmentEnv()` helper that checks `typeof process !== 'undefined'` first. diff --git a/packages/table-core/src/core/columns/constructColumn.ts b/packages/table-core/src/core/columns/constructColumn.ts index b26f42f7d6..62fa48aa83 100644 --- a/packages/table-core/src/core/columns/constructColumn.ts +++ b/packages/table-core/src/core/columns/constructColumn.ts @@ -1,3 +1,4 @@ +import { isDevelopmentEnv } from '../../utils' import type { Table_Internal } from '../../types/Table' import type { CellData, RowData } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -74,7 +75,7 @@ export function constructColumn< for (let i = 0; i < keys.length; i++) { const key = keys[i]! result = result?.[key] - if (process.env.NODE_ENV === 'development' && result === undefined) { + if (isDevelopmentEnv() && result === undefined) { console.warn( `"${key}" in deeply nested key "${accessorKey}" returned undefined.`, ) @@ -90,7 +91,7 @@ export function constructColumn< } if (!id) { - if (process.env.NODE_ENV === 'development') { + if (isDevelopmentEnv()) { throw new Error( resolvedColumnDef.accessorFn ? `coreColumnsFeature require an id when using an accessorFn` diff --git a/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts b/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts index 7a6ea4bed7..ff4d51b194 100644 --- a/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts +++ b/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts @@ -1,4 +1,8 @@ -import { callMemoOrStaticFn, makeObjectMap } from '../../utils' +import { + callMemoOrStaticFn, + isDevelopmentEnv, + makeObjectMap, +} from '../../utils' import { table_getOrderColumnsFn } from '../../features/column-ordering/columnOrderingFeature.utils' import { constructColumn } from './constructColumn' import type { Table_Internal } from '../../types/Table' @@ -280,7 +284,7 @@ export function table_getColumn< ): Column | undefined { const column = table.getAllFlatColumnsById()[columnId] - if (process.env.NODE_ENV === 'development' && !column) { + if (isDevelopmentEnv() && !column) { console.warn(`[Table] Column with id '${columnId}' does not exist.`) } diff --git a/packages/table-core/src/core/rows/coreRowsFeature.utils.ts b/packages/table-core/src/core/rows/coreRowsFeature.utils.ts index f37ce79828..3ad680f0e6 100644 --- a/packages/table-core/src/core/rows/coreRowsFeature.utils.ts +++ b/packages/table-core/src/core/rows/coreRowsFeature.utils.ts @@ -1,4 +1,4 @@ -import { flattenBy, hasOwn, makeObjectMap } from '../../utils' +import { flattenBy, hasOwn, isDevelopmentEnv, makeObjectMap } from '../../utils' import { constructCell } from '../cells/constructCell' import type { Table_Internal } from '../../types/Table' import type { RowData } from '../../types/type-utils' @@ -348,7 +348,7 @@ export function table_getRow< if (!row) { row = table.getCoreRowModel().rowsById[rowId] if (!row) { - if (process.env.NODE_ENV === 'development') { + if (isDevelopmentEnv()) { throw new Error(`getRow could not find row with ID: ${rowId}`) } throw new Error() diff --git a/packages/table-core/src/core/table/constructTable.ts b/packages/table-core/src/core/table/constructTable.ts index 7a6519d354..130437e1e5 100644 --- a/packages/table-core/src/core/table/constructTable.ts +++ b/packages/table-core/src/core/table/constructTable.ts @@ -1,6 +1,6 @@ import { shallow } from '@tanstack/store' import { coreFeatures } from '../coreFeatures' -import { cloneState, hasOwn } from '../../utils' +import { cloneState, hasOwn, isDevelopmentEnv } from '../../utils' import { atomToStore } from '../reactivity/coreReactivityFeature.utils' import { table_syncExternalStateToBaseAtoms } from './coreTablesFeature.utils' import type { Atom } from '@tanstack/store' @@ -223,7 +223,7 @@ export function constructTable< } if ( - process.env.NODE_ENV === 'development' && + isDevelopmentEnv() && (tableOptions.debugAll || tableOptions.debugTable) ) { const features = Object.keys(table._features) diff --git a/packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts b/packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts index c0ea454943..6959d3a454 100644 --- a/packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts +++ b/packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts @@ -1,4 +1,9 @@ -import { cloneState, functionalUpdate, isFunction } from '../../utils' +import { + cloneState, + functionalUpdate, + isDevelopmentEnv, + isFunction, +} from '../../utils' import type { CellData, RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' import type { Table_Internal } from '../../types/Table' @@ -80,7 +85,7 @@ export function column_getAutoFilterFn< const filterFn = filterFns?.[filterFnName] - if (process.env.NODE_ENV === 'development' && !filterFn) { + if (isDevelopmentEnv() && !filterFn) { console.warn( `filterFn '${filterFnName}' (auto) for column '${column.id}' is not registered`, ) @@ -118,7 +123,7 @@ export function column_getFilterFn< : filterFns?.[column.columnDef.filterFn as string] if ( - process.env.NODE_ENV === 'development' && + isDevelopmentEnv() && !filterFn && column.columnDef.filterFn !== 'auto' // the auto picker warns on its own ) { diff --git a/packages/table-core/src/features/global-filtering/globalFilteringFeature.utils.ts b/packages/table-core/src/features/global-filtering/globalFilteringFeature.utils.ts index 18882cf316..f6c399eecf 100644 --- a/packages/table-core/src/features/global-filtering/globalFilteringFeature.utils.ts +++ b/packages/table-core/src/features/global-filtering/globalFilteringFeature.utils.ts @@ -1,5 +1,5 @@ import { filterFn_includesString } from '../column-filtering/filterFns' -import { cloneState, isFunction } from '../../utils' +import { cloneState, isDevelopmentEnv, isFunction } from '../../utils' import type { Column_Internal } from '../../types/Column' import type { FilterFn } from '../column-filtering/columnFilteringFeature.types' import type { CellData, RowData } from '../../types/type-utils' @@ -74,11 +74,7 @@ export function table_getGlobalFilterFn< ? table_getGlobalAutoFilterFn() : filterFns?.[globalFilterFn as string] - if ( - process.env.NODE_ENV === 'development' && - !filterFn && - globalFilterFn != null - ) { + if (isDevelopmentEnv() && !filterFn && globalFilterFn != null) { console.warn(`globalFilterFn '${String(globalFilterFn)}' is not registered`) } diff --git a/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts b/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts index 1915b5b8ba..2aa4d97395 100644 --- a/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts +++ b/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts @@ -1,4 +1,4 @@ -import { hasOwn, makeObjectMap } from '../../utils' +import { hasOwn, isDevelopmentEnv, makeObjectMap } from '../../utils' import type { Cell } from '../../types/Cell' import type { Column, Column_Internal } from '../../types/Column' import type { Row } from '../../types/Row' @@ -48,7 +48,7 @@ function isAggregationFnDescriptor( } function warn(message: string) { - if (process.env.NODE_ENV === 'development') { + if (isDevelopmentEnv()) { console.warn(message) } } diff --git a/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts b/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts index 9efe5080ab..bb22dad619 100644 --- a/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts +++ b/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts @@ -1,4 +1,4 @@ -import { cloneState, isFunction } from '../../utils' +import { cloneState, isDevelopmentEnv, isFunction } from '../../utils' import { reSplitAlphaNumeric, sortFn_basic } from './sortFns' import type { CellData, RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -144,7 +144,7 @@ export function column_getAutoSortFn< let sortFn = sortFns?.[sortFnName] if (!sortFn) { - if (process.env.NODE_ENV === 'development') { + if (isDevelopmentEnv()) { console.warn( `sortFn '${sortFnName}' (auto) for column '${column.id}' is not registered`, ) @@ -228,7 +228,7 @@ export function column_getSortFn< const sortFn = sortFns?.[column.columnDef.sortFn as string] - if (process.env.NODE_ENV === 'development' && !sortFn) { + if (isDevelopmentEnv() && !sortFn) { console.warn( `sortFn '${String(column.columnDef.sortFn)}' for column '${column.id}' is not registered`, ) diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts index ff3bda6216..62f80e0323 100755 --- a/packages/table-core/src/utils.ts +++ b/packages/table-core/src/utils.ts @@ -89,6 +89,21 @@ export function hasOwn(obj: object, key: PropertyKey): boolean { return Object.prototype.hasOwnProperty.call(obj, key) } +/** + * Reports whether the library should run its development-only debug and + * validation logic. + * + * Guards the `process` global so this is safe to call in environments with + * no bundler or Node.js runtime (e.g. an ESM build loaded directly via an + * import map), where a raw `process.env.NODE_ENV` read throws a + * `ReferenceError`. + */ +export function isDevelopmentEnv(): boolean { + return ( + typeof process !== 'undefined' && process.env.NODE_ENV === 'development' + ) +} + /** * Creates a table state updater for a single state slice. * @@ -272,7 +287,7 @@ export function tableMemo< let debug: boolean | undefined let debugCache: boolean | undefined - if (process.env.NODE_ENV === 'development') { + if (isDevelopmentEnv()) { const { debugAll } = table.options const { parentName } = getFunctionNameInfo(fnName, '.') @@ -333,44 +348,43 @@ export function tableMemo< schedule(() => untrack(() => onAfterUpdate())) } - const debugOptions = - process.env.NODE_ENV === 'development' - ? { - onBeforeCompare: () => { - if (debugCache) { - beforeCompareTime = performance.now() + const debugOptions = isDevelopmentEnv() + ? { + onBeforeCompare: () => { + if (debugCache) { + beforeCompareTime = performance.now() + } + }, + onAfterCompare: (depsChanged: boolean) => { + if (debugCache) { + afterCompareTime = performance.now() + const compareTime = + Math.round((afterCompareTime - beforeCompareTime) * 100) / 100 + if (!depsChanged) { + logTime(compareTime, depsChanged) } - }, - onAfterCompare: (depsChanged: boolean) => { - if (debugCache) { - afterCompareTime = performance.now() - const compareTime = - Math.round((afterCompareTime - beforeCompareTime) * 100) / 100 - if (!depsChanged) { - logTime(compareTime, depsChanged) - } - } - }, - onBeforeUpdate: () => { - if (debug) { - startCalcTime = performance.now() - } - }, - onAfterUpdate: () => { - if (debug) { - endCalcTime = performance.now() - const executionTime = - Math.round((endCalcTime - startCalcTime) * 100) / 100 - logTime(executionTime, true) - } - onAfterUpdateHandler() - }, - } - : { - onAfterUpdate: () => { - onAfterUpdateHandler() - }, - } + } + }, + onBeforeUpdate: () => { + if (debug) { + startCalcTime = performance.now() + } + }, + onAfterUpdate: () => { + if (debug) { + endCalcTime = performance.now() + const executionTime = + Math.round((endCalcTime - startCalcTime) * 100) / 100 + logTime(executionTime, true) + } + onAfterUpdateHandler() + }, + } + : { + onAfterUpdate: () => { + onAfterUpdateHandler() + }, + } return memo({ ...memoOptions, diff --git a/packages/table-core/src/worker/createWorkerRowModel.ts b/packages/table-core/src/worker/createWorkerRowModel.ts index 7d2e7395de..db66ecc3ed 100644 --- a/packages/table-core/src/worker/createWorkerRowModel.ts +++ b/packages/table-core/src/worker/createWorkerRowModel.ts @@ -1,4 +1,4 @@ -import { tableMemo } from '../utils' +import { isDevelopmentEnv, tableMemo } from '../utils' import { getTableWorkerBridge, syncTableWorker } from './createTableWorker' import { rebuildRowModel } from './rebuildRowModel' import { tableWorkerPipeline } from './tableWorkerProtocol' @@ -60,7 +60,7 @@ export function createWorkerRowModel( let warned = false const warnOnce = (message: string) => { - if (process.env.NODE_ENV === 'development' && !warned) { + if (isDevelopmentEnv() && !warned) { warned = true console.warn(`[table-worker] ${message}`) } diff --git a/packages/table-core/tests/unit/utils.test.ts b/packages/table-core/tests/unit/utils.test.ts index b839d48c7c..30a454edae 100644 --- a/packages/table-core/tests/unit/utils.test.ts +++ b/packages/table-core/tests/unit/utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi } from 'vitest' +import { afterEach, describe, expect, test, vi } from 'vitest' import { callMemoOrStaticFn, cloneState, @@ -6,6 +6,7 @@ import { flattenBy, functionalUpdate, getFunctionNameInfo, + isDevelopmentEnv, isFunction, tableMemo, } from '../../src/utils' @@ -52,6 +53,27 @@ describe('tableMemo', () => { expect(schedule).toHaveBeenCalledTimes(1) expect(onAfterUpdate).toHaveBeenCalledTimes(1) }) + + test('does not throw when the process global is not defined', () => { + vi.stubGlobal('process', undefined) + + const memoized = tableMemo({ + table: { + options: {}, + _reactivity: { + schedule: (fn: () => void) => fn(), + untrack: (fn: () => void) => fn(), + }, + } as any, + fnName: 'table.getValue', + fn: (value?: number) => value ?? 0, + memoDeps: (value?: number) => [value], + }) + + expect(() => memoized(1)).not.toThrow() + + vi.unstubAllGlobals() + }) }) describe('functionalUpdate', () => { @@ -165,6 +187,32 @@ describe('getFunctionNameInfo', () => { }) }) +describe('isDevelopmentEnv', () => { + afterEach(() => { + vi.unstubAllGlobals() + vi.unstubAllEnvs() + }) + + test('is true when NODE_ENV is "development"', () => { + vi.stubEnv('NODE_ENV', 'development') + + expect(isDevelopmentEnv()).toBe(true) + }) + + test('is false when NODE_ENV is not "development"', () => { + vi.stubEnv('NODE_ENV', 'production') + + expect(isDevelopmentEnv()).toBe(false) + }) + + test('is false, not throwing, when the process global is not defined', () => { + vi.stubGlobal('process', undefined) + + expect(() => isDevelopmentEnv()).not.toThrow() + expect(isDevelopmentEnv()).toBe(false) + }) +}) + describe('callMemoOrStaticFn', () => { test('prefers the instance method when present', () => { const staticFn = vi.fn(() => 'static')