Skip to content

Keep the terminal UI runtime out of the CLI startup path - #8238

Draft
isaacroldan wants to merge 2 commits into
mainfrom
river/startup-drop-ink
Draft

Keep the terminal UI runtime out of the CLI startup path#8238
isaacroldan wants to merge 2 commits into
mainfrom
river/startup-drop-ink

Conversation

@isaacroldan

@isaacroldan isaacroldan commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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 --version loaded the terminal UI runtime. The error path also loaded graphql-request for one type check.

This work made startup slower before a command could run.

What

  • Move the token types and pure formatting helpers into a dependency-free module.
  • Keep React and Ink imports in rendering modules.
  • Replace the eager ClientError import 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.
  • JavaScript loaded at startup: 5.31 MB to 4.16 MB.

Testing

  • Type-check, lint, Knip, bundle, unit, and E2E checks pass.
  • Output is unchanged for --version, --help, app --help, and commands.
  • Existing tests use real ClientError instances and pass with the structural check.

Context: https://shopify.slack.com/archives/C0AG0L37Q4C/p1785681072538939

@isaacroldan isaacroldan self-assigned this Aug 3, 2026
@github-actions github-actions Bot added cla-needed Area: @shopify/cli @shopify/cli package issues labels Aug 3, 2026
isaacroldan and others added 2 commits August 3, 2026 17:57
`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
isaacroldan force-pushed the river/startup-drop-ink branch from c18b75c to ba02a02 Compare August 3, 2026 15:57
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Differences in type declarations

We 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:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/private/node/ui/components/token-item.d.ts
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;

Existing type declarations

packages/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;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant