Keep the terminal UI runtime out of the CLI startup path - #8238
Merged
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
|
/snapit |
Contributor
|
🫰✨ Thanks @gonzaloriestra! Your snapshot has been published to npm. Test the snapshot by installing your package globally: pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260806094115Caution After installing, validate the version by running |
gonzaloriestra
approved these changes
Aug 6, 2026
gonzaloriestra
left a comment
Contributor
There was a problem hiding this comment.
Nice! Simple change, good improvement 👌
Redaction is best effort, so the flag that turns on the payload dump should say so. Regenerates the manifests, README and dev docs that embed the description.
Assisted-By: devx/a6727785-6ba2-4c6b-9612-bcadf6422d8f
Assisted-By: devx/2ae47aa3-adba-4060-adf8-d3b99e6b63f3
Adds integration tests covering edge cases, empty path behavior, and exception/cleanup paths in zip and brotliCompress.
Adds integration tests covering edge cases, empty path behavior, and exception/cleanup paths in zip and brotliCompress. Also synchronizes generated graphql admin types with Shopify upstream schema to resolve CI checks.
Adds integration tests covering edge cases, empty path behavior, and exception/cleanup paths in zip and brotliCompress. Also synchronizes generated graphql admin types with Shopify upstream schema to resolve CI checks.
Add comprehensive unit tests for randomHex, base64URLEncode, sha256, randomBytes, and randomUUID in packages/cli-kit/src/public/node/crypto.test.ts. This closes the test coverage gaps for the crypto utility module without changing any observable behavior.
Improve readability and structure of getOrCreateHiddenShopifyFolder by sequentially checking directory and gitignore existence. This avoids concurrently checking files when the parent directory is missing.
Improve readability and structure of getOrCreateHiddenShopifyFolder by sequentially checking directory and gitignore existence. This avoids concurrently checking files when the parent directory is missing. Also updated generated admin graphql types and references to resolve pre-existing CI schema generation differences on MetaobjectAdminAccess/MetafieldAdminAccess.
Improve readability and structure of getOrCreateHiddenShopifyFolder by sequentially checking directory and gitignore existence. This avoids concurrently checking files when the parent directory is missing. Also updated generated admin graphql types to resolve pre-existing CI schema generation differences on MetaobjectAdminAccess.
Refactors `createGitIgnore` to build the `.gitignore` content using a declarative `.map().join('')` chain instead of an imperative `for...of` loop with string accumulator.
Add comprehensive unit tests for the `isTruthy` utility function in `packages/cli-kit/src/public/node/context/utilities.ts`. These tests cover various truthy patterns ('1', 'true', 'yes') with case-insensitivity, and several falsy scenarios including undefined, empty strings, and '0'.
Refactor `addResolutionOrOverride` to use a more declarative approach with property key lookup and leverage the `writePackageJSON` helper. This reduces duplication and improves maintainability.
Add unit tests for getLogsDir, createLogsDir, and writeLog in packages/cli-kit/src/public/node/logs.ts. - Use inTemporaryDirectory for real filesystem testing. - Mock logsFolder for controlled test environment.
Implemented unit tests for the `addCursorAndFiltersToAppLogsUrl` utility function in `packages/cli-kit/src/public/node/api/utilities.test.ts`. The tests cover: - Base case with only a URL - Appending a cursor - Appending status filters - Appending source filters - Appending combinations of cursor and multiple filters
- Refactor `outputContent` to use a declarative `.reduce()` approach. - Refactor `formatSection` to use `.padEnd(35)` for idiomatic string padding.
Refactor `platformAndArch` in `packages/cli-kit/src/public/node/os.ts` to use a constant lookup map instead of an imperative `if/else` block. This improves code readability and maintainability.
Refactor flattenToPatchEntries in toml-file.ts to use a declarative flatMap approach instead of an imperative for...of loop with entries.push to improve code idiom and readability.
Refactored `packages/cli/src/cli/services/commands/doc/fetch.test.ts` to replace filesystem mocks with real operations using `inTemporaryDirectory`. Verified that `docFetchService` correctly handles directory creation and file writing by asserting the state of the real filesystem.
- Replaced `vi.mock('@shopify/cli-kit/node/fs')` with real fs utilities.
- Updated 'writes the document to the output path instead of stdout' to use `inTemporaryDirectory`.
- Complied with `vitest/prefer-expect-resolves` ESLint rule.
…tion Refactor `argumentsToAddDependenciesWithNPM`, `argumentsToAddDependenciesWithYarn`, `argumentsToAddDependenciesWithPNPM`, and `argumentsToAddDependenciesWithBun` to use declarative lookup maps and spread operators. This improves readability and maintainability by replacing imperative switch statements and array mutations with more idiomatic TypeScript patterns.
…anged Every app automation token exchange failure collapsed into a single fixed message, and the OAuth `error`/`error_description` returned by Identity were discarded before reaching it, so a revoked token, an expired token and an unreachable Identity were indistinguishable. Verbose output didn't help either: it printed the response headers for that call and no status or body. Keep the existing sentence as the headline and append the upstream reason when there is one, and log the status plus the OAuth error fields of a failed token request under `--verbose`. Only failed responses are logged, and only the `error`/`error_description` fields, since a successful body carries the access token.
The fields of a failed token response aren't guaranteed: the responder isn't always Identity, since proxies and gateways can answer with arbitrary JSON. A body without an `error` field crashed the error constructor before any message was shown, and `error_description` flowed into the terminal unbounded. Guard both fields once, where the response is parsed: `error` falls back to `unknown_error`, and `error_description` is flattened to a single line and capped at 200 characters. Every consumer downstream now gets the shape the type declares. Also widen the changeset wording: the shared error handler enriches interactive token errors too, not only automation-token ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Map any code outside IdentityDeviceError to unknown_failure at the exchange boundary instead of casting, and add a default branch to the poll switch so an unexpected code rejects rather than leaving the promise unsettled. Also apply one shared flatten-and-truncate rule to both the Identity description and the appended Reason message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review direction from the proposal thread: every token failure resolves to the same user action, so the default error stays generic and the reason lives in the debug log only. The debug line in tokenRequest covers OAuth rejections. The automation catch now logs the caught error too, which covers failures that produce no OAuth body, such as the authentication service being unreachable. The enrichment of InvalidGrantError, InvalidRequestError and the fall-through AbortError is reverted: session.ts only checks the class, so those messages carried the detail into default output for nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assisted-By: devx/2ae47aa3-adba-4060-adf8-d3b99e6b63f3
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