Skip to content

Reduce dependencies loaded during CLI startup - #8249

Draft
isaacroldan wants to merge 1 commit into
mainfrom
isaac/reduce-cli-startup-dependencies
Draft

Reduce dependencies loaded during CLI startup#8249
isaacroldan wants to merge 1 commit into
mainfrom
isaac/reduce-cli-startup-dependencies

Conversation

@isaacroldan

Copy link
Copy Markdown
Contributor

Why

Every CLI command loaded large session and process utility graphs before it ran. Successful commands also initialized Bugsnag, and notification cache checks loaded UI, schema, HTTP, and process execution code even when those features were not needed.

This made small commands pay for unrelated features during startup.

What changed

  • Give BaseCommand lightweight session-alias and terminal dependencies while preserving the existing exports.
  • Initialize Bugsnag and register plugin stack cleanup only when an error is reported. Registration is idempotent.
  • Load notification UI, schema validation, HTTP, and process execution code only when each path needs it.

For shopify version, the static command graph falls from 3,169,523 bytes across 39 files to 1,985,821 bytes across 30 files, a 37% reduction. A local Node 26 comparison reduced median process time from 355 ms to 340 ms.

Testing

  • 172 focused CLI Kit tests
  • CLI Kit type-check
  • CLI Kit lint
  • Knip
  • Production CLI bundle

No changeset is needed because this is an internal performance refactor with no user-visible behavior change.

@isaacroldan
isaacroldan requested a review from a team as a code owner August 4, 2026 13:22
@github-actions github-actions Bot added the no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. label Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 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/notifications-schema.d.ts
import { zod } from '../../public/node/schema.js';
export declare const NotificationSchema: zod.ZodObject<{
    id: zod.ZodString;
    message: zod.ZodString;
    type: zod.ZodEnum<["info", "warning", "error"]>;
    frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
    ownerChannel: zod.ZodString;
    cta: zod.ZodOptional<zod.ZodObject<{
        label: zod.ZodString;
        url: zod.ZodString;
    }, "strip", zod.ZodTypeAny, {
        url: string;
        label: string;
    }, {
        url: string;
        label: string;
    }>>;
    title: zod.ZodOptional<zod.ZodString>;
    minVersion: zod.ZodOptional<zod.ZodString>;
    maxVersion: zod.ZodOptional<zod.ZodString>;
    minDate: zod.ZodOptional<zod.ZodString>;
    maxDate: zod.ZodOptional<zod.ZodString>;
    commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
    surface: zod.ZodOptional<zod.ZodString>;
}, "strip", zod.ZodTypeAny, {
    id: string;
    type: "info" | "error" | "warning";
    message: string;
    frequency: "always" | "once" | "once_a_day" | "once_a_week";
    ownerChannel: string;
    title?: string | undefined;
    cta?: {
        url: string;
        label: string;
    } | undefined;
    minVersion?: string | undefined;
    maxVersion?: string | undefined;
    minDate?: string | undefined;
    maxDate?: string | undefined;
    commands?: string[] | undefined;
    surface?: string | undefined;
}, {
    id: string;
    type: "info" | "error" | "warning";
    message: string;
    frequency: "always" | "once" | "once_a_day" | "once_a_week";
    ownerChannel: string;
    title?: string | undefined;
    cta?: {
        url: string;
        label: string;
    } | undefined;
    minVersion?: string | undefined;
    maxVersion?: string | undefined;
    minDate?: string | undefined;
    maxDate?: string | undefined;
    commands?: string[] | undefined;
    surface?: string | undefined;
}>;
export type Notification = zod.infer<typeof NotificationSchema>;
export declare const NotificationsSchema: zod.ZodObject<{
    notifications: zod.ZodArray<zod.ZodObject<{
        id: zod.ZodString;
        message: zod.ZodString;
        type: zod.ZodEnum<["info", "warning", "error"]>;
        frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
        ownerChannel: zod.ZodString;
        cta: zod.ZodOptional<zod.ZodObject<{
            label: zod.ZodString;
            url: zod.ZodString;
        }, "strip", zod.ZodTypeAny, {
            url: string;
            label: string;
        }, {
            url: string;
            label: string;
        }>>;
        title: zod.ZodOptional<zod.ZodString>;
        minVersion: zod.ZodOptional<zod.ZodString>;
        maxVersion: zod.ZodOptional<zod.ZodString>;
        minDate: zod.ZodOptional<zod.ZodString>;
        maxDate: zod.ZodOptional<zod.ZodString>;
        commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
        surface: zod.ZodOptional<zod.ZodString>;
    }, "strip", zod.ZodTypeAny, {
        id: string;
        type: "info" | "error" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        title?: string | undefined;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }, {
        id: string;
        type: "info" | "error" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        title?: string | undefined;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }>, "many">;
}, "strip", zod.ZodTypeAny, {
    notifications: {
        id: string;
        type: "info" | "error" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        title?: string | undefined;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }[];
}, {
    notifications: {
        id: string;
        type: "info" | "error" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        title?: string | undefined;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }[];
}>;
export type Notifications = zod.infer<typeof NotificationsSchema>;
packages/cli-kit/dist/private/node/session-alias.d.ts
/**
 * Finds a stored Shopify account session by alias without changing the current session.
 *
 * @param alias - The account alias to find.
 * @returns The matching session ID, or undefined if no session matches.
 */
export declare function findSessionIdByAlias(alias: string): Promise<string | undefined>;
/**
 * Selects a stored Shopify account session by alias for the current command process.
 *
 * @param alias - The account alias to select. Passing undefined clears the command selection.
 */
export declare function setCurrentSessionAlias(alias?: string): Promise<void>;
packages/cli-kit/dist/private/node/terminal.d.ts
/**
 * Check if the standard input and output streams support prompting.
 *
 * @returns True if the standard input and output streams support prompting.
 */
export declare function terminalSupportsPrompting(): boolean;
packages/cli-kit/dist/private/node/session/command-session.d.ts
export declare function getCommandSessionId(): string | undefined;
export declare function setCommandSessionId(sessionId: string | undefined): void;

Existing type declarations

packages/cli-kit/dist/private/node/session.d.ts
@@ -1,4 +1,5 @@
 import { AdminSession } from '../../public/node/session.js';
+export { setCommandSessionId } from './session/command-session.js';
 /**
  * A scope supported by the Shopify Admin API.
  */
@@ -90,7 +91,6 @@ export declare function setLastSeenUserIdAfterAuth(id: string): void;
  */
 export declare function getLastSeenAuthMethod(): Promise<AuthMethod>;
 export declare function setLastSeenAuthMethod(method: AuthMethod): void;
-export declare function setCommandSessionId(sessionId: string | undefined): void;
 export interface EnsureAuthenticatedAdditionalOptions {
     noPrompt?: boolean;
     forceRefresh?: boolean;
@@ -104,5 +104,4 @@ export interface EnsureAuthenticatedAdditionalOptions {
  * @param options - Optional extra options to use.
  * @returns An instance with the access tokens organized by application.
  */
-export declare function ensureAuthenticated(applications: OAuthApplications, _env?: NodeJS.ProcessEnv, { forceRefresh, noPrompt, forceNewSession }?: EnsureAuthenticatedAdditionalOptions): Promise<OAuthSession>;
-export {};
\ No newline at end of file
+export declare function ensureAuthenticated(applications: OAuthApplications, _env?: NodeJS.ProcessEnv, { forceRefresh, noPrompt, forceNewSession }?: EnsureAuthenticatedAdditionalOptions): Promise<OAuthSession>;
\ No newline at end of file
packages/cli-kit/dist/public/node/error-handler.d.ts
@@ -8,7 +8,7 @@ export declare function errorHandler(error: Error & {
  *
  * @returns the reported error (this may have been tweaked for better reporting), and a bool to indicate if the error was actually submitted or not
  */
-export declare function sendErrorToBugsnag(error: unknown, exitMode: Omit<CommandExitMode, 'ok'>): Promise<{
+export declare function sendErrorToBugsnag(error: unknown, exitMode: Omit<CommandExitMode, 'ok'>, config?: Interfaces.Config): Promise<{
     reported: false;
     error: unknown;
     unhandled: unknown;
@@ -31,9 +31,5 @@ export declare function cleanStackFrameFilePath({ currentFilePath, projectRoot,
         pluginPath: string;
     }[];
 }): string;
-/**
- * Register a Bugsnag error listener to clean up stack traces for errors within plugin code.
- *
- */
 export declare function registerCleanBugsnagErrorsFromWithinPlugins(config: Interfaces.Config): Promise<void>;
 export declare function addBugsnagMetadata(event: any, config: Interfaces.Config): Promise<void>;
\ No newline at end of file
packages/cli-kit/dist/public/node/notifications-system.d.ts
@@ -1,162 +1,5 @@
-import { zod } from './schema.js';
-declare const NotificationSchema: zod.ZodObject<{
-    id: zod.ZodString;
-    message: zod.ZodString;
-    type: zod.ZodEnum<["info", "warning", "error"]>;
-    frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
-    ownerChannel: zod.ZodString;
-    cta: zod.ZodOptional<zod.ZodObject<{
-        label: zod.ZodString;
-        url: zod.ZodString;
-    }, "strip", zod.ZodTypeAny, {
-        url: string;
-        label: string;
-    }, {
-        url: string;
-        label: string;
-    }>>;
-    title: zod.ZodOptional<zod.ZodString>;
-    minVersion: zod.ZodOptional<zod.ZodString>;
-    maxVersion: zod.ZodOptional<zod.ZodString>;
-    minDate: zod.ZodOptional<zod.ZodString>;
-    maxDate: zod.ZodOptional<zod.ZodString>;
-    commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
-    surface: zod.ZodOptional<zod.ZodString>;
-}, "strip", zod.ZodTypeAny, {
-    id: string;
-    type: "info" | "error" | "warning";
-    message: string;
-    frequency: "always" | "once" | "once_a_day" | "once_a_week";
-    ownerChannel: string;
-    title?: string | undefined;
-    cta?: {
-        url: string;
-        label: string;
-    } | undefined;
-    minVersion?: string | undefined;
-    maxVersion?: string | undefined;
-    minDate?: string | undefined;
-    maxDate?: string | undefined;
-    commands?: string[] | undefined;
-    surface?: string | undefined;
-}, {
-    id: string;
-    type: "info" | "error" | "warning";
-    message: string;
-    frequency: "always" | "once" | "once_a_day" | "once_a_week";
-    ownerChannel: string;
-    title?: string | undefined;
-    cta?: {
-        url: string;
-        label: string;
-    } | undefined;
-    minVersion?: string | undefined;
-    maxVersion?: string | undefined;
-    minDate?: string | undefined;
-    maxDate?: string | undefined;
-    commands?: string[] | undefined;
-    surface?: string | undefined;
-}>;
-export type Notification = zod.infer<typeof NotificationSchema>;
-declare const NotificationsSchema: zod.ZodObject<{
-    notifications: zod.ZodArray<zod.ZodObject<{
-        id: zod.ZodString;
-        message: zod.ZodString;
-        type: zod.ZodEnum<["info", "warning", "error"]>;
-        frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
-        ownerChannel: zod.ZodString;
-        cta: zod.ZodOptional<zod.ZodObject<{
-            label: zod.ZodString;
-            url: zod.ZodString;
-        }, "strip", zod.ZodTypeAny, {
-            url: string;
-            label: string;
-        }, {
-            url: string;
-            label: string;
-        }>>;
-        title: zod.ZodOptional<zod.ZodString>;
-        minVersion: zod.ZodOptional<zod.ZodString>;
-        maxVersion: zod.ZodOptional<zod.ZodString>;
-        minDate: zod.ZodOptional<zod.ZodString>;
-        maxDate: zod.ZodOptional<zod.ZodString>;
-        commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
-        surface: zod.ZodOptional<zod.ZodString>;
-    }, "strip", zod.ZodTypeAny, {
-        id: string;
-        type: "info" | "error" | "warning";
-        message: string;
-        frequency: "always" | "once" | "once_a_day" | "once_a_week";
-        ownerChannel: string;
-        title?: string | undefined;
-        cta?: {
-            url: string;
-            label: string;
-        } | undefined;
-        minVersion?: string | undefined;
-        maxVersion?: string | undefined;
-        minDate?: string | undefined;
-        maxDate?: string | undefined;
-        commands?: string[] | undefined;
-        surface?: string | undefined;
-    }, {
-        id: string;
-        type: "info" | "error" | "warning";
-        message: string;
-        frequency: "always" | "once" | "once_a_day" | "once_a_week";
-        ownerChannel: string;
-        title?: string | undefined;
-        cta?: {
-            url: string;
-            label: string;
-        } | undefined;
-        minVersion?: string | undefined;
-        maxVersion?: string | undefined;
-        minDate?: string | undefined;
-        maxDate?: string | undefined;
-        commands?: string[] | undefined;
-        surface?: string | undefined;
-    }>, "many">;
-}, "strip", zod.ZodTypeAny, {
-    notifications: {
-        id: string;
-        type: "info" | "error" | "warning";
-        message: string;
-        frequency: "always" | "once" | "once_a_day" | "once_a_week";
-        ownerChannel: string;
-        title?: string | undefined;
-        cta?: {
-            url: string;
-            label: string;
-        } | undefined;
-        minVersion?: string | undefined;
-        maxVersion?: string | undefined;
-        minDate?: string | undefined;
-        maxDate?: string | undefined;
-        commands?: string[] | undefined;
-        surface?: string | undefined;
-    }[];
-}, {
-    notifications: {
-        id: string;
-        type: "info" | "error" | "warning";
-        message: string;
-        frequency: "always" | "once" | "once_a_day" | "once_a_week";
-        ownerChannel: string;
-        title?: string | undefined;
-        cta?: {
-            url: string;
-            label: string;
-        } | undefined;
-        minVersion?: string | undefined;
-        maxVersion?: string | undefined;
-        minDate?: string | undefined;
-        maxDate?: string | undefined;
-        commands?: string[] | undefined;
-        surface?: string | undefined;
-    }[];
-}>;
-export type Notifications = zod.infer<typeof NotificationsSchema>;
+import type { Notification, Notifications } from '../../private/node/notifications-schema.js';
+export type { Notification, Notifications } from '../../private/node/notifications-schema.js';
 /**
  * Shows notifications to the user if they meet the criteria specified in the notifications.json file.
  *
@@ -202,5 +45,4 @@ export declare function filterNotifications(notifications: Notification[], comma
  * @param notification - The notification to get the filters from.
  * @returns A string with human-readable filters from the notification.
  */
-export declare function stringifyFilters(notification: Notification): string;
-export {};
\ No newline at end of file
+export declare function stringifyFilters(notification: Notification): string;
\ No newline at end of file
packages/cli-kit/dist/public/node/session.d.ts
@@ -1,4 +1,5 @@
 import { AdminAPIScope, AppManagementAPIScope, BusinessPlatformScope, EnsureAuthenticatedAdditionalOptions, PartnersAPIScope, StorefrontRendererScope } from '../../private/node/session.js';
+export { findSessionIdByAlias, setCurrentSessionAlias } from '../../private/node/session-alias.js';
 /**
  * Session Object to access the Admin API, includes the token and the store FQDN.
  */
@@ -22,19 +23,6 @@ export type AccountInfo = UserAccountInfo | ServiceAccountInfo | UnknownAccountI
  * @param userId - User identifier to report on the command analytics event.
  */
 export declare function setLastSeenUserId(userId: string): void;
-/**
- * Finds a stored Shopify account session by alias without changing the current session.
- *
- * @param alias - The account alias to find.
- * @returns The matching session ID, or undefined if no session matches.
- */
-export declare function findSessionIdByAlias(alias: string): Promise<string | undefined>;
-/**
- * Selects a stored Shopify account session by alias for the current command process.
- *
- * @param alias - The account alias to select. Passing undefined clears the command selection.
- */
-export declare function setCurrentSessionAlias(alias?: string): Promise<void>;
 interface UserAccountInfo {
     type: 'UserAccount';
     email: string;
@@ -152,5 +140,4 @@ export declare function logout(): Promise<void>;
  * @param clientSecret - Client secret of the app.
  * @returns The access token for the Admin API.
  */
-export declare function ensureAuthenticatedAdminAsApp(storeFqdn: string, clientId: string, clientSecret: string): Promise<AdminSession>;
-export {};
\ No newline at end of file
+export declare function ensureAuthenticatedAdminAsApp(storeFqdn: string, clientId: string, clientSecret: string): Promise<AdminSession>;
\ No newline at end of file
packages/cli-kit/dist/public/node/system.d.ts
@@ -1,5 +1,6 @@
 import { AbortSignal } from './abort.js';
 import type { Writable, Readable } from 'stream';
+export { terminalSupportsPrompting } from '../../private/node/terminal.js';
 export interface ExecOptions {
     cwd?: string;
     env?: Record<string, string | undefined>;
@@ -103,12 +104,6 @@ export declare function sleep(seconds: number): Promise<void>;
  * @returns True if the terminal supports hyperlinks.
  */
 export declare function terminalSupportsHyperlinks(): boolean;
-/**
- * Check if the standard input and output streams support prompting.
- *
- * @returns True if the standard input and output streams support prompting.
- */
-export declare function terminalSupportsPrompting(): boolean;
 /**
  * Check if the current environment is a CI environment.
  *

@isaacroldan
isaacroldan marked this pull request as draft August 4, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant