Keep the terminal UI runtime out of the CLI startup path - #8238
Draft
isaacroldan wants to merge 2 commits into
Draft
Keep the terminal UI runtime out of the CLI startup path#8238isaacroldan wants to merge 2 commits into
isaacroldan wants to merge 2 commits into
Conversation
`output.ts` and `error.ts` imported `tokenItemToString` from
`private/node/ui/components/TokenizedText.tsx`, and `error.ts` imported
`ClientError` from graphql-request for a single `instanceof` check.
TokenizedText is a React component module, so those two imports pulled
react-reconciler (341 KB), yoga-layout and its WebAssembly (125 KB), ink
and react into the module graph of every command, plus graphql (254 KB),
tr46 (231 KB) and whatwg-url. Practically everything imports output.ts,
so `shopify --version` was loading a terminal UI renderer and a GraphQL
client to print a version string.
Moves the token types, tokenItemToString and appendToTokenItem into
token-item.ts, which imports nothing, and repoints every import site.
TokenizedText.tsx keeps the component and imports the types. Replaces the
`instanceof ClientError` check with a structural match on the same shape;
ClientError is the only error reaching that path carrying both `response`
and `request`, and the cli-kit wrapper carries `statusCode` instead.
No behaviour change. Measured on the bundled CLI, clean rebuilds on both
sides, cachegrind instruction counts under `node --predictable`:
before after delta
shopify --version 1,543,699,375 737,632,410 -52.2%
shopify --help 1,606,749,593 809,187,564 -49.6%
--version wall 174 ms 99 ms -43%
JS loaded at boot 5.31 MB 4.16 MB -22%
Output is byte-identical for --version, --help, app --help and commands.
Co-authored-by: Isaac Roldan <isaac.roldan@shopify.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
isaacroldan
force-pushed
the
river/startup-drop-ink
branch
from
August 3, 2026 15:57
c18b75c to
ba02a02
Compare
Contributor
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationspackages/cli-kit/dist/private/node/ui/components/token-item.d.tsexport interface LinkToken {
link: {
label?: string;
url: string;
};
}
export interface UserInputToken {
userInput: string;
}
export interface ListToken {
list: {
title?: TokenItem<InlineToken>;
items: TokenItem<InlineToken>[];
ordered?: boolean;
};
}
export interface BoldToken {
bold: string;
}
export type Token = string | {
command: string;
} | LinkToken | {
char: string;
} | UserInputToken | {
subdued: string;
} | {
filePath: string;
} | ListToken | BoldToken | {
info: string;
} | {
warn: string;
} | {
error: string;
};
export type InlineToken = Exclude<Token, ListToken>;
export type TokenItem<T extends Token = Token> = T | T[];
export declare function tokenItemToString(token: TokenItem): string;
export declare function appendToTokenItem(token: TokenItem, suffix: string): TokenItem;
Existing type declarationspackages/cli-kit/dist/public/common/string.d.ts@@ -1,4 +1,4 @@
-import { Token, TokenItem } from '../../private/node/ui/components/TokenizedText.js';
+import type { Token, TokenItem } from '../../private/node/ui/components/token-item.js';
export type RandomNameFamily = 'business' | 'creative';
/**
* Generates a random name by combining an adjective and noun.
packages/cli-kit/dist/public/node/error.d.ts@@ -1,5 +1,5 @@
import { OutputMessage } from './output.js';
-import { InlineToken, TokenItem } from '../../private/node/ui/components/TokenizedText.js';
+import { type InlineToken, type TokenItem } from '../../private/node/ui/components/token-item.js';
import type { AlertCustomSection } from './ui.js';
export { ExtendableError } from 'ts-error';
export declare enum FatalErrorType {
packages/cli-kit/dist/public/node/ui.d.ts@@ -6,7 +6,7 @@ import { AlertOptions } from '../../private/node/ui/alert.js';
import { CustomSection } from '../../private/node/ui/components/Alert.js';
import ScalarDict from '../../private/node/ui/components/Table/ScalarDict.js';
import { TableColumn, TableProps } from '../../private/node/ui/components/Table/Table.js';
-import { Token, InlineToken, LinkToken, ListToken, TokenItem } from '../../private/node/ui/components/TokenizedText.js';
+import { type InlineToken, type LinkToken, type ListToken, type Token, type TokenItem } from '../../private/node/ui/components/token-item.js';
import { DangerousConfirmationPromptProps } from '../../private/node/ui/components/DangerousConfirmationPrompt.js';
import { SelectPromptProps } from '../../private/node/ui/components/SelectPrompt.js';
import { Task } from '../../private/node/ui/components/Tasks.js';
packages/cli-kit/dist/private/node/ui/utilities.d.ts@@ -1,16 +1,16 @@
-import { TokenItem } from './components/TokenizedText.js';
-export declare function messageWithPunctuation(message: TokenItem): string | {
+import { type TokenItem } from './components/token-item.js';
+export declare function messageWithPunctuation(message: TokenItem): string | import("./components/token-item.js").LinkToken | import("./components/token-item.js").UserInputToken | import("./components/token-item.js").ListToken | {
command: string;
-} | import("./components/TokenizedText.js").LinkToken | {
+} | {
char: string;
-} | import("./components/TokenizedText.js").UserInputToken | {
+} | {
subdued: string;
} | {
filePath: string;
-} | import("./components/TokenizedText.js").ListToken | import("./components/TokenizedText.js").BoldToken | {
+} | import("./components/token-item.js").BoldToken | {
info: string;
} | {
warn: string;
} | {
error: string;
-} | import("./components/TokenizedText.js").Token[];
\ No newline at end of file
+} | import("./components/token-item.js").Token[];
\ No newline at end of file
packages/cli-kit/dist/private/node/ui/components/Alert.d.ts@@ -1,7 +1,7 @@
import { BannerType } from './Banner.js';
-import { BoldToken, InlineToken, LinkToken, TokenItem } from './TokenizedText.js';
import { TabularDataProps } from './TabularData.js';
import { FunctionComponent } from 'react';
+import type { BoldToken, InlineToken, LinkToken, TokenItem } from './token-item.js';
export interface CustomSection {
title?: string;
body: TabularDataProps | TokenItem;
packages/cli-kit/dist/private/node/ui/components/DangerousConfirmationPrompt.d.ts@@ -1,7 +1,7 @@
-import { InlineToken, TokenItem } from './TokenizedText.js';
import { InfoTableProps } from './Prompts/InfoTable.js';
import { AbortSignal } from '../../../../public/node/abort.js';
import { FunctionComponent } from 'react';
+import type { InlineToken, TokenItem } from './token-item.js';
export interface DangerousConfirmationPromptProps {
message: string;
confirmation: string;
packages/cli-kit/dist/private/node/ui/components/List.d.ts@@ -1,6 +1,6 @@
-import { InlineToken, TokenItem } from './TokenizedText.js';
import { TextProps } from 'ink';
import { FunctionComponent } from 'react';
+import type { InlineToken, TokenItem } from './token-item.js';
export interface CustomListItem {
type?: string;
item: TokenItem<InlineToken>;
packages/cli-kit/dist/private/node/ui/components/TabularData.d.ts@@ -1,4 +1,4 @@
-import { InlineToken } from './TokenizedText.js';
+import { type InlineToken } from './token-item.js';
import { FunctionComponent } from 'react';
export interface TabularDataProps {
tabularData: InlineToken[][];
packages/cli-kit/dist/private/node/ui/components/TextPrompt.d.ts@@ -1,6 +1,6 @@
-import { InlineToken, TokenItem } from './TokenizedText.js';
import { AbortSignal } from '../../../../public/node/abort.js';
import { FunctionComponent } from 'react';
+import type { InlineToken, TokenItem } from './token-item.js';
export interface TextPromptProps {
message: TokenItem;
onSubmit: (value: string) => void;
packages/cli-kit/dist/private/node/ui/components/TokenizedText.d.ts@@ -1,42 +1,5 @@
import { FunctionComponent } from 'react';
-export interface LinkToken {
- link: {
- label?: string;
- url: string;
- };
-}
-export interface UserInputToken {
- userInput: string;
-}
-export interface ListToken {
- list: {
- title?: TokenItem<InlineToken>;
- items: TokenItem<InlineToken>[];
- ordered?: boolean;
- };
-}
-export interface BoldToken {
- bold: string;
-}
-export type Token = string | {
- command: string;
-} | LinkToken | {
- char: string;
-} | UserInputToken | {
- subdued: string;
-} | {
- filePath: string;
-} | ListToken | BoldToken | {
- info: string;
-} | {
- warn: string;
-} | {
- error: string;
-};
-export type InlineToken = Exclude<Token, ListToken>;
-export type TokenItem<T extends Token = Token> = T | T[];
-export declare function tokenItemToString(token: TokenItem): string;
-export declare function appendToTokenItem(token: TokenItem, suffix: string): TokenItem;
+import type { TokenItem } from './token-item.js';
interface TokenizedTextProps {
item: TokenItem;
}
packages/cli-kit/dist/private/node/ui/components/Prompts/InfoMessage.d.ts@@ -1,6 +1,6 @@
-import { InlineToken, LinkToken, TokenItem, UserInputToken } from '../TokenizedText.js';
import { TextProps } from 'ink';
import { FunctionComponent } from 'react';
+import type { InlineToken, LinkToken, TokenItem, UserInputToken } from '../token-item.js';
export interface InfoMessageProps {
message: {
title: {
packages/cli-kit/dist/private/node/ui/components/Prompts/InfoTable.d.ts@@ -1,7 +1,7 @@
import { CustomListItem } from '../List.js';
-import { InlineToken, TokenItem } from '../TokenizedText.js';
import { TextProps } from 'ink';
import { FunctionComponent } from 'react';
+import type { InlineToken, TokenItem } from '../token-item.js';
type Items = (TokenItem<InlineToken> | CustomListItem)[];
export interface InfoTableSection {
color?: TextProps['color'];
packages/cli-kit/dist/private/node/ui/components/Prompts/PromptLayout.d.ts@@ -1,9 +1,9 @@
import { InfoTableProps } from './InfoTable.js';
import { InfoMessageProps } from './InfoMessage.js';
-import { InlineToken, LinkToken, TokenItem } from '../TokenizedText.js';
import { AbortSignal } from '../../../../../public/node/abort.js';
import { PromptState } from '../../hooks/use-prompt.js';
import { ReactElement } from 'react';
+import type { InlineToken, LinkToken, TokenItem } from '../token-item.js';
export type Message = TokenItem<Exclude<InlineToken, LinkToken>>;
interface PromptLayoutProps {
message: Message;
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every CLI invocation loads common output code. That code imported a small text-formatting helper from a React and Ink component, so even
shopify --versionloaded the terminal UI runtime. The error path also loadedgraphql-requestfor one type check.This work made startup slower before a command could run.
What
ClientErrorimport with a structural check that preserves the existing expected-error cases.No output changes are intended.
Impact
On clean bundled builds with the Nx cache disabled:
shopify --version: 174 ms to 99 ms, with 52% fewer instructions.shopify --help: 50% fewer instructions.Testing
--version,--help,app --help, andcommands.ClientErrorinstances and pass with the structural check.Context: https://shopify.slack.com/archives/C0AG0L37Q4C/p1785681072538939