Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
8c2e24f
Keep the terminal UI runtime out of the CLI startup path
isaacroldan Aug 3, 2026
ba02a02
Add changeset for CLI startup improvement
isaacroldan Aug 3, 2026
55c960f
update generated graphql files
github-actions[bot] Aug 3, 2026
d194f53
Note in --verbose help that the extra output may include sensitive data
isaacroldan Jul 30, 2026
565933b
Add a changeset for the --verbose help change
isaacroldan Jul 30, 2026
4aff9b6
Remove the changeset
isaacroldan Aug 3, 2026
55db1dc
Fix Nx caching configuration
isaacroldan Aug 3, 2026
4663c89
fix(cli-kit): allowlist analytics environment variables
isaacroldan Aug 3, 2026
0120283
[Tests] Improve coverage for archiver node utilities
gonzaloriestra Aug 3, 2026
83dc1f7
[Tests] Improve coverage for archiver node utilities
gonzaloriestra Aug 3, 2026
e5dcceb
[Tests] Improve coverage for archiver node utilities
gonzaloriestra Aug 3, 2026
d54632f
[Refactor] Introduce early return in addRecommendedExtensions
gonzaloriestra Aug 3, 2026
01ee4d6
[Refactor] Use flatMap and getArrayRejectingUndefined in info service
gonzaloriestra Jun 21, 2026
9d0d4f7
[Tests] Add comprehensive unit tests for tree-kill utility
gonzaloriestra Jul 31, 2026
d225c67
[Refactor] Add comprehensive unit tests for crypto module
gonzaloriestra Jul 31, 2026
ed44f18
[Refactor] optimize filesystem calls in getOrCreateHiddenShopifyFolder
gonzaloriestra Aug 2, 2026
88b0221
[Refactor] Optimize filesystem calls in getOrCreateHiddenShopifyFolder
gonzaloriestra Aug 2, 2026
95b43cd
[Refactor] Optimize filesystem calls in getOrCreateHiddenShopifyFolder
gonzaloriestra Aug 2, 2026
5bbac33
Show required non-interactive flags in CLI help
gonzaloriestra Jul 30, 2026
856211e
Document non-interactive app command flags
gonzaloriestra Jul 31, 2026
4983cf4
Document non-interactive theme command flags
gonzaloriestra Jul 31, 2026
242357f
Document non-interactive store command flags
gonzaloriestra Jul 31, 2026
80103fb
Avoid bundling TypeScript in the production CLI
isaacroldan Aug 4, 2026
0a3ac4a
Externalize TypeScript from the create-app bundle
isaacroldan Aug 4, 2026
226954e
Avoid app initialization for non-app commands
isaacroldan Aug 4, 2026
a634bd1
Remove changeset for internal startup refactor
isaacroldan Aug 4, 2026
3871c7e
fix(function): generate concrete GraphQL scalar types
davejcameron Aug 4, 2026
25f3388
[Tests] Add unit tests for path utility functions
gonzaloriestra Jul 27, 2026
6f395ae
[Refactor] Extract parseEnvNumber helper in environment.ts
gonzaloriestra Jul 28, 2026
373dfcb
Document other non-interactive command flags
gonzaloriestra Jul 31, 2026
9426d5f
[Tests] Add unit tests for AbortController and AbortSignal
gonzaloriestra Jul 22, 2026
3f54e24
[Tests] Add unit tests for ShopifyConfig custom oclif loader
gonzaloriestra Jul 19, 2026
00e8a65
[Refactor] Declarative createGitIgnore content construction
gonzaloriestra Jul 18, 2026
8240ce9
[Tests] Add unit tests for isTruthy
gonzaloriestra Jul 16, 2026
51e1287
[Refactor] Modernize addResolutionOrOverride in node-package-manager.ts
gonzaloriestra Jul 15, 2026
21b5b42
[Tests] Add unit tests for logs utility
gonzaloriestra Jul 15, 2026
15c0f4a
[Tests] Add unit tests for addCursorAndFiltersToAppLogsUrl
gonzaloriestra Jul 14, 2026
dde6f0c
[Refactor] output utility logic
gonzaloriestra Jul 14, 2026
3895de1
[Refactor] Use lookup map for architecture in platformAndArch
gonzaloriestra Jul 11, 2026
5796805
[Refactor] Use flatMap for flattening TOML patch entries
gonzaloriestra Jul 10, 2026
ddcca15
Add tests
gonzaloriestra Jul 10, 2026
f13dc64
[Tests] Remove filesystem mocks in doc fetch service tests
gonzaloriestra Jun 24, 2026
9bf1ec0
[Refactor] node-package-manager: modernize dependency argument genera…
gonzaloriestra Jul 16, 2026
01a9acf
Update Javy and function-runner
jeffcharles Aug 4, 2026
40b46f8
Surface the Identity error when an app automation token can't be exch…
wes-shaw Jul 29, 2026
a59db1e
Normalize the OAuth error fields before they reach any message
wes-shaw Jul 29, 2026
645069f
Keep the device-auth poll from hanging on unrecognized error codes
wes-shaw Jul 29, 2026
71a624e
Keep the exchange failure reason out of the default error output
wes-shaw Jul 30, 2026
703261c
Address review feedback
wes-shaw Aug 3, 2026
7438d40
Fix formatting
wes-shaw Aug 3, 2026
47b6c70
Always log token exchange failures
wes-shaw Aug 4, 2026
8d34cf0
Fix formatting
wes-shaw Aug 4, 2026
8465278
fix(cli-kit): allowlist packed agent analytics variables
isaacroldan Aug 4, 2026
33de97b
Merge branch 'main' into river/startup-drop-ink
isaacroldan Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/faster-cli-startup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-kit': patch
---

Speed up CLI startup by keeping the terminal UI runtime out of the boot path
3 changes: 2 additions & 1 deletion packages/cli-kit/src/private/node/ui/components/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Banner, BannerType} from './Banner.js'
import {Link} from './Link.js'
import {List} from './List.js'
import {BoldToken, InlineToken, LinkToken, TokenItem, TokenizedText} from './TokenizedText.js'
import {TokenizedText} from './TokenizedText.js'
import {TabularData, TabularDataProps} from './TabularData.js'
import {Box, Text} from 'ink'
import React, {FunctionComponent} from 'react'
import type {BoldToken, InlineToken, LinkToken, TokenItem} from './token-item.js'

export interface CustomSection {
title?: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TextInput} from './TextInput.js'
import {InlineToken, TokenItem, TokenizedText} from './TokenizedText.js'
import {TokenizedText} from './TokenizedText.js'
import {InfoTable, InfoTableProps} from './Prompts/InfoTable.js'
import {handleCtrlC, useComplete} from '../../ui.js'
import useLayout from '../hooks/use-layout.js'
Expand All @@ -11,6 +11,7 @@ import usePrompt, {PromptState} from '../hooks/use-prompt.js'
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react'
import {Box, useInput, Text} from 'ink'
import figures from 'figures'
import type {InlineToken, TokenItem} from './token-item.js'

export interface DangerousConfirmationPromptProps {
message: string
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-kit/src/private/node/ui/components/List.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {InlineToken, TokenItem, TokenizedText} from './TokenizedText.js'
import {TokenizedText} from './TokenizedText.js'
import {Box, Text, TextProps} from 'ink'
import React, {FunctionComponent} from 'react'
import type {InlineToken, TokenItem} from './token-item.js'

export interface CustomListItem {
type?: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {InlineToken, LinkToken, TokenItem, TokenizedText, UserInputToken} from '../TokenizedText.js'
import {TokenizedText} from '../TokenizedText.js'
import {Box, Text, TextProps} from 'ink'
import React, {FunctionComponent} from 'react'
import type {InlineToken, LinkToken, TokenItem, UserInputToken} from '../token-item.js'

export interface InfoMessageProps {
message: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {CustomListItem, List} from '../List.js'
import {capitalize} from '../../../../../public/common/string.js'
import {InlineToken, TokenItem, TokenizedText} from '../TokenizedText.js'
import {TokenizedText} from '../TokenizedText.js'
import {Box, Text, TextProps} from 'ink'
import React, {FunctionComponent} from 'react'
import type {InlineToken, TokenItem} from '../token-item.js'

type Items = (TokenItem<InlineToken> | CustomListItem)[]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {InfoTable, InfoTableProps} from './InfoTable.js'
import {InfoMessage, InfoMessageProps} from './InfoMessage.js'
import {InlineToken, LinkToken, TokenItem, TokenizedText} from '../TokenizedText.js'
import {TokenizedText} from '../TokenizedText.js'
import {messageWithPunctuation} from '../../utilities.js'
import {AbortSignal} from '../../../../../public/node/abort.js'
import useAbortSignal from '../../hooks/use-abort-signal.js'
Expand All @@ -9,6 +9,7 @@ import React, {ReactElement, cloneElement, useCallback, useLayoutEffect, useStat

import {Box, measureElement, Text, useStdout, DOMElement} from 'ink'
import figures from 'figures'
import type {InlineToken, LinkToken, TokenItem} from '../token-item.js'

export type Message = TokenItem<Exclude<InlineToken, LinkToken>>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {InlineToken, TokenizedText, tokenItemToString} from './TokenizedText.js'
import {TokenizedText} from './TokenizedText.js'
import {tokenItemToString, type InlineToken} from './token-item.js'
import {unstyled} from '../../../../public/node/output.js'
import {Box} from 'ink'
import React, {FunctionComponent} from 'react'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {InlineToken, TokenItem, TokenizedText} from './TokenizedText.js'
import {TokenizedText} from './TokenizedText.js'
import {TextInput} from './TextInput.js'
import {handleCtrlC, useComplete} from '../../ui.js'
import useLayout from '../hooks/use-layout.js'
Expand All @@ -10,6 +10,7 @@ import React, {FunctionComponent, useCallback, useEffect, useState} from 'react'

import {Box, useInput, Text} from 'ink'
import figures from 'figures'
import type {InlineToken, TokenItem} from './token-item.js'

export interface TextPromptProps {
message: TokenItem
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {tokenItemToString, TokenizedText} from './TokenizedText.js'
import {TokenizedText} from './TokenizedText.js'
import {tokenItemToString} from './token-item.js'
import {LinksContext, Link} from '../contexts/LinksContext.js'
import {unstyled} from '../../../../public/node/output.js'
import {render} from '../../testing/ui.js'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,7 @@ import {LinksContext} from '../contexts/LinksContext.js'
import React, {FunctionComponent, useContext} from 'react'
import {Box, Text} from 'ink'

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[]
import type {ListToken, Token, TokenItem} from './token-item.js'

type DisplayType = 'block' | 'inline'
interface Block {
Expand All @@ -75,49 +23,6 @@ function tokenToBlock(token: Token): Block {
}
}

export function tokenItemToString(token: TokenItem): string {
if (typeof token === 'string') {
return token
} else if ('command' in token) {
return token.command
} else if ('link' in token) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty label should fall through to url
return token.link.label || token.link.url
} else if ('char' in token) {
return token.char
} else if ('userInput' in token) {
return token.userInput
} else if ('subdued' in token) {
return token.subdued
} else if ('filePath' in token) {
return token.filePath
} else if ('list' in token) {
return token.list.items.map(tokenItemToString).join(' ')
} else if ('bold' in token) {
return token.bold
} else if ('info' in token) {
return token.info
} else if ('warn' in token) {
return token.warn
} else if ('error' in token) {
return token.error
} else {
return token
.map((item, index) => {
if (index !== 0 && !(typeof item !== 'string' && 'char' in item)) {
return ` ${tokenItemToString(item)}`
} else {
return tokenItemToString(item)
}
})
.join('')
}
}

export function appendToTokenItem(token: TokenItem, suffix: string): TokenItem {
return Array.isArray(token) ? [...token, {char: suffix}] : [token, {char: suffix}]
}

function splitByDisplayType(acc: Block[][], item: Block) {
if (item.display === 'block') {
acc.push([item])
Expand Down
105 changes: 105 additions & 0 deletions packages/cli-kit/src/private/node/ui/components/token-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Token shapes and the pure helpers that operate on them.
//
// These live apart from `TokenizedText.tsx` on purpose. That file is a React
// component module: importing it pulls in `react` and `ink`, and through Ink,
// `react-reconciler`, `yoga-layout` and its WebAssembly. Non-rendering modules
// such as `output.ts`, `error.ts` and `common/string.ts` need only the types
// and `tokenItemToString`, and importing them from here keeps the terminal UI
// runtime out of the startup path of every command.

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 function tokenItemToString(token: TokenItem): string {
if (typeof token === 'string') {
return token
} else if ('command' in token) {
return token.command
} else if ('link' in token) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty label should fall through to url
return token.link.label || token.link.url
} else if ('char' in token) {
return token.char
} else if ('userInput' in token) {
return token.userInput
} else if ('subdued' in token) {
return token.subdued
} else if ('filePath' in token) {
return token.filePath
} else if ('list' in token) {
return token.list.items.map(tokenItemToString).join(' ')
} else if ('bold' in token) {
return token.bold
} else if ('info' in token) {
return token.info
} else if ('warn' in token) {
return token.warn
} else if ('error' in token) {
return token.error
} else {
return token
.map((item, index) => {
if (index !== 0 && !(typeof item !== 'string' && 'char' in item)) {
return ` ${tokenItemToString(item)}`
} else {
return tokenItemToString(item)
}
})
.join('')
}
}

export function appendToTokenItem(token: TokenItem, suffix: string): TokenItem {
return Array.isArray(token) ? [...token, {char: suffix}] : [token, {char: suffix}]
}
2 changes: 1 addition & 1 deletion packages/cli-kit/src/private/node/ui/utilities.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {messageWithPunctuation} from './utilities.js'
import {tokenItemToString} from './components/TokenizedText.js'
import {tokenItemToString} from './components/token-item.js'
import {describe, expect, test} from 'vitest'

describe('messageWithPunctuation', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-kit/src/private/node/ui/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {appendToTokenItem, TokenItem, tokenItemToString} from './components/TokenizedText.js'
import {appendToTokenItem, tokenItemToString, type TokenItem} from './components/token-item.js'

export function messageWithPunctuation(message: TokenItem) {
const messageToString = tokenItemToString(message)
Expand Down
3 changes: 1 addition & 2 deletions packages/cli-kit/src/public/common/string.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {takeRandomFromArray, uniq} from './array.js'
import {unstyled} from '../node/output.js'
import {Token, TokenItem} from '../../private/node/ui/components/TokenizedText.js'

import {camelCase, capitalCase, constantCase, paramCase, snakeCase, pascalCase} from 'change-case'
import type {Token, TokenItem} from '../../private/node/ui/components/token-item.js'

const SAFE_RANDOM_BUSINESS_ADJECTIVES = [
'commercial',
Expand Down
21 changes: 15 additions & 6 deletions packages/cli-kit/src/public/node/error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {normalizePath} from './path.js'
import {OutputMessage, stringifyMessage, TokenizedString} from './output.js'
import {InlineToken, TokenItem, tokenItemToString} from '../../private/node/ui/components/TokenizedText.js'
import {tokenItemToString, type InlineToken, type TokenItem} from '../../private/node/ui/components/token-item.js'
import {hasRateLimitCode} from '../../private/node/analytics/graphql-error-codes.js'

import {Errors} from '@oclif/core'
import {ClientError} from 'graphql-request'

import type {AlertCustomSection} from './ui.js'

Expand Down Expand Up @@ -222,21 +221,31 @@ export function shouldReportErrorAsUnexpected(error: unknown): boolean {
* (HTTP 429, or a `THROTTLED`/`429` GraphQL code on any error in the response) matches the shape
* detected by `errorsIncludeStatus429` in `private/node/api.ts`.
*
* Scoped to the external `ClientError` type only — importing the cli-kit `GraphQLClientError`
* Scoped to the external `ClientError` shape only — importing the cli-kit `GraphQLClientError`
* wrapper here would create an `error.ts → headers.ts → error.ts` import cycle.
*
* Matched structurally rather than with `instanceof ClientError`, because importing the class
* pulls `graphql-request` — and through it `graphql`, `tr46` and `whatwg-url` — into the module
* graph of every command, for one type check on an error path. `ClientError` is the only error
* reaching here that carries both `response` and `request`; the cli-kit wrapper carries
* `statusCode` instead (see `private/node/api/headers.ts`).
*
* @param error - Error to be checked.
* @returns A boolean indicating if the error is a known expected API error.
*/
function isExpectedApiError(error: Error): boolean {
if (!(error instanceof ClientError)) {
const candidate = error as Error & {
response?: {status?: number; errors?: unknown}
request?: unknown
}
if (typeof candidate.response !== 'object' || candidate.response === null || candidate.request === undefined) {
return false
}
const status = error.response?.status
const status = candidate.response.status
if (status === 401 || status === 429) {
return true
}
return hasRateLimitCode(error.response?.errors)
return hasRateLimitCode(candidate.response.errors)
}

/**
Expand Down
Loading
Loading