From 48d4ede286c5425660e65a1126301858080115f5 Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:30:23 +0000 Subject: [PATCH] [Performance] Memoize isTerminalInteractive Memoize the `isTerminalInteractive` function in `@shopify/cli-kit` to avoid redundant environment variable and stream property lookups. This optimization improves efficiency in hot paths where terminal interactivity is checked frequently. A `_resetIsTerminalInteractiveCache` utility was added and integrated into unit tests to ensure proper test isolation. Expected impact: Measurable reduction in redundant lookups for `process.stdout.isTTY`, `process.env.TERM`, and `process.env.CI`. --- .../src/public/node/context/local.test.ts | 34 +++++++++++++++++++ .../cli-kit/src/public/node/context/local.ts | 16 ++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/cli-kit/src/public/node/context/local.test.ts b/packages/cli-kit/src/public/node/context/local.test.ts index abe71bb52b7..05e0aae81c0 100644 --- a/packages/cli-kit/src/public/node/context/local.test.ts +++ b/packages/cli-kit/src/public/node/context/local.test.ts @@ -4,6 +4,7 @@ import { isDevelopment, isShopify, isTerminalInteractive, + _resetIsTerminalInteractiveCache, isUnitTest, analyticsDisabled, cloudEnvironment, @@ -25,6 +26,7 @@ describe('isTerminalInteractive', () => { const originalEnv = {...process.env} afterEach(() => { + _resetIsTerminalInteractiveCache() process.stdout.isTTY = originalIsTTY process.env.TERM = originalEnv.TERM if (originalEnv.CI === undefined) { @@ -68,6 +70,38 @@ describe('isTerminalInteractive', () => { process.env.TERM = 'xterm-256color' expect(isTerminalInteractive()).toBe(false) }) + + test('memoizes the result', () => { + // Given + process.stdout.isTTY = true + delete process.env.CI + process.env.TERM = 'xterm-256color' + const firstResult = isTerminalInteractive() + + // When + process.stdout.isTTY = false + const secondResult = isTerminalInteractive() + + // Then + expect(firstResult).toBe(true) + expect(secondResult).toBe(true) + }) + + test('resets the cache', () => { + // Given + process.stdout.isTTY = true + delete process.env.CI + process.env.TERM = 'xterm-256color' + isTerminalInteractive() + + // When + _resetIsTerminalInteractiveCache() + process.stdout.isTTY = false + const secondResult = isTerminalInteractive() + + // Then + expect(secondResult).toBe(false) + }) }) describe('isUnitTest', () => { diff --git a/packages/cli-kit/src/public/node/context/local.ts b/packages/cli-kit/src/public/node/context/local.ts index ad816399db9..2a0e6f15dc2 100644 --- a/packages/cli-kit/src/public/node/context/local.ts +++ b/packages/cli-kit/src/public/node/context/local.ts @@ -16,13 +16,27 @@ async function lazyExec(command: string, args: string[]): Promise { await exec(command, args) } +/** + * Memoized value for the terminal interactive check. + */ +let memoizedIsTerminalInteractive: boolean | undefined + /** * It returns true if the terminal is interactive. * * @returns True if the terminal is interactive. */ export function isTerminalInteractive(): boolean { - return Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb' && !('CI' in process.env)) + return (memoizedIsTerminalInteractive ??= Boolean( + process.stdout.isTTY && process.env.TERM !== 'dumb' && !('CI' in process.env), + )) +} + +/** + * Resets the memoized value for the terminal interactive check. + */ +export function _resetIsTerminalInteractiveCache(): void { + memoizedIsTerminalInteractive = undefined } /**