Lazy command loading: Option B — oclif pattern strategy with thin re-exports#7301
Lazy command loading: Option B — oclif pattern strategy with thin re-exports#7301byrichardpowell wants to merge 4 commits intomainfrom
Conversation
…tion B) Switch from explicit command strategy (single barrel file loading all packages) to oclif's native pattern strategy with per-command re-export files. Key changes: - Add bootstrap.ts as lightweight CLI entry point (no heavy package imports) - Create commands/ directory with 108 thin re-export files (one per command) - Switch oclif config from explicit to pattern strategy - Split hooks from index.ts barrel into individual files with dynamic imports - Defer app/hydrogen init hooks to prerun (only run when relevant command executes) - Add customPluginName assignment in prerun hook for analytics attribution - Enable Node.js compile cache for faster repeat startups Heavy packages (@shopify/app, @shopify/theme, @shopify/cli-hydrogen) are no longer imported at startup for non-app/theme/hydrogen commands. The version command confirms zero heavy package loads. Performance: 1.594s mean (vs 1.778s main baseline = -10.3%) The remaining time is @shopify/cli-kit + oclif framework overhead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a minimal ShopifyConfig subclass to cli-kit that makes init, prerun, and postrun hooks fire-and-forget (non-blocking). This avoids blocking the command on analytics tracking, upgrade checks, and plugin init setup. Combined with the pattern strategy from the previous commit and process.exit(0) in bootstrap.ts, startup drops from 1.594s to 0.412s (-76.8% vs main). The ShopifyConfig only overrides runHook() — no runCommand() override or custom command registry. Commands are still loaded entirely by oclif's native pattern strategy, keeping the architecture idiomatic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simplify code examples to high-level descriptions of the approach rather than full code listings. The branches are the source of truth for implementation details. Update performance numbers, evaluation sections, and comparison table to reflect the optimized results with ShopifyConfig non-blocking hooks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Pull request overview
This PR refactors the Shopify CLI package to use oclif’s native pattern command loading with thin per-command re-export modules, moving the runtime entrypoint to a lightweight bootstrap.ts to reduce eager imports and improve startup time.
Changes:
- Switch oclif command discovery from
explicit(single COMMANDS barrel) topattern(dist/commands) with many thin per-command re-export files. - Introduce
src/bootstrap.tsas the CLI entrypoint used bybin/run.jsandbin/dev.js, keeping startup imports minimal. - Split hooks into individual modules with dynamic imports; add a custom oclif
Configsubclass (ShopifyConfig) to makeinit/prerun/postrunhooks fire-and-forget.
Reviewed changes
Copilot reviewed 122 out of 125 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/src/index.ts | Converts former CLI runner barrel into a small public API barrel export. |
| packages/cli/src/bootstrap.ts | New lightweight CLI entrypoint that sets up process handlers and invokes cli-kit runCLI. |
| packages/cli/src/hooks/prerun.ts | Adds plugin attribution + deferred init logic, then delegates to cli-kit prerun hook. |
| packages/cli/src/hooks/postrun.ts | Continues delegating postrun behavior to cli-kit postrun hook. |
| packages/cli/src/hooks/app-init.ts | No-op init hook to defer heavy app init until prerun. |
| packages/cli/src/hooks/hydrogen-init.ts | No-op init hook to defer heavy hydrogen init until prerun. |
| packages/cli/src/hooks/did-you-mean.ts | Lazily loads did-you-mean hook implementation. |
| packages/cli/src/hooks/tunnel-start.ts | Lazily loads cloudflare tunnel start hook. |
| packages/cli/src/hooks/tunnel-provider.ts | Lazily loads cloudflare tunnel provider hook. |
| packages/cli/src/hooks/plugin-plugins.ts | Lazily loads oclif plugin-plugins update hook. |
| packages/cli/src/hooks/app-sensitive-metadata.ts | Lazily loads app sensitive metadata hook. |
| packages/cli/src/hooks/app-public-metadata.ts | Lazily loads app public metadata hook. |
| packages/cli/src/commands/version.ts | Thin re-export for the version command. |
| packages/cli/src/commands/upgrade.ts | Thin re-export for the upgrade command. |
| packages/cli/src/commands/search.ts | Thin re-export for the search command. |
| packages/cli/src/commands/help.ts | Thin re-export for the help command. |
| packages/cli/src/commands/cache/clear.ts | Thin re-export for the cache:clear command. |
| packages/cli/src/commands/auth/login.ts | Thin re-export for the auth:login command. |
| packages/cli/src/commands/auth/logout.ts | Thin re-export for the auth:logout command. |
| packages/cli/src/commands/notifications/list.ts | Thin re-export for the notifications:list command. |
| packages/cli/src/commands/notifications/generate.ts | Thin re-export for the notifications:generate command. |
| packages/cli/src/commands/debug/command-flags.ts | Thin re-export for the debug:command-flags command. |
| packages/cli/src/commands/docs/generate.ts | Thin re-export for the docs:generate command. |
| packages/cli/src/commands/doctor-release/index.ts | Thin re-export for the doctor-release command. |
| packages/cli/src/commands/doctor-release/theme/index.ts | Thin re-export for the doctor-release:theme command. |
| packages/cli/src/commands/kitchen-sink/index.ts | Thin re-export for the kitchen-sink command. |
| packages/cli/src/commands/kitchen-sink/async.ts | Thin re-export for the kitchen-sink:async command. |
| packages/cli/src/commands/kitchen-sink/prompts.ts | Thin re-export for the kitchen-sink:prompts command. |
| packages/cli/src/commands/kitchen-sink/static.ts | Thin re-export for the kitchen-sink:static command. |
| packages/cli/src/commands/theme/check.ts | Thin re-export for theme:check via @shopify/theme command map. |
| packages/cli/src/commands/theme/console.ts | Thin re-export for theme:console via @shopify/theme command map. |
| packages/cli/src/commands/theme/delete.ts | Thin re-export for theme:delete via @shopify/theme command map. |
| packages/cli/src/commands/theme/dev.ts | Thin re-export for theme:dev via @shopify/theme command map. |
| packages/cli/src/commands/theme/duplicate.ts | Thin re-export for theme:duplicate via @shopify/theme command map. |
| packages/cli/src/commands/theme/info.ts | Thin re-export for theme:info via @shopify/theme command map. |
| packages/cli/src/commands/theme/init.ts | Thin re-export for theme:init via @shopify/theme command map. |
| packages/cli/src/commands/theme/language-server.ts | Thin re-export for theme:language-server via @shopify/theme command map. |
| packages/cli/src/commands/theme/list.ts | Thin re-export for theme:list via @shopify/theme command map. |
| packages/cli/src/commands/theme/metafields/pull.ts | Thin re-export for theme:metafields:pull via @shopify/theme command map. |
| packages/cli/src/commands/theme/open.ts | Thin re-export for theme:open via @shopify/theme command map. |
| packages/cli/src/commands/theme/package.ts | Thin re-export for theme:package via @shopify/theme command map. |
| packages/cli/src/commands/theme/preview.ts | Thin re-export for theme:preview via @shopify/theme command map. |
| packages/cli/src/commands/theme/profile.ts | Thin re-export for theme:profile via @shopify/theme command map. |
| packages/cli/src/commands/theme/publish.ts | Thin re-export for theme:publish via @shopify/theme command map. |
| packages/cli/src/commands/theme/pull.ts | Thin re-export for theme:pull via @shopify/theme command map. |
| packages/cli/src/commands/theme/push.ts | Thin re-export for theme:push via @shopify/theme command map. |
| packages/cli/src/commands/theme/rename.ts | Thin re-export for theme:rename via @shopify/theme command map. |
| packages/cli/src/commands/theme/serve.ts | Thin re-export for theme:serve via @shopify/theme command map. |
| packages/cli/src/commands/theme/share.ts | Thin re-export for theme:share via @shopify/theme command map. |
| packages/cli/src/commands/commands.ts | Thin re-export for the commands command from @oclif/plugin-commands. |
| packages/cli/src/commands/plugins/index.ts | Thin re-export for plugins (also hides it) from @oclif/plugin-plugins. |
| packages/cli/src/commands/plugins/install.ts | Thin re-export for plugins:install (also clears description) from @oclif/plugin-plugins. |
| packages/cli/src/commands/plugins/inspect.ts | Thin re-export for plugins:inspect from @oclif/plugin-plugins. |
| packages/cli/src/commands/plugins/link.ts | Thin re-export for plugins:link from @oclif/plugin-plugins. |
| packages/cli/src/commands/plugins/reset.ts | Thin re-export for plugins:reset from @oclif/plugin-plugins. |
| packages/cli/src/commands/plugins/uninstall.ts | Thin re-export for plugins:uninstall from @oclif/plugin-plugins. |
| packages/cli/src/commands/plugins/update.ts | Thin re-export for plugins:update from @oclif/plugin-plugins. |
| packages/cli/src/commands/config/autocorrect/off.ts | Thin re-export for autocorrect off from plugin-did-you-mean command map. |
| packages/cli/src/commands/config/autocorrect/on.ts | Thin re-export for autocorrect on from plugin-did-you-mean command map. |
| packages/cli/src/commands/config/autocorrect/status.ts | Thin re-export for autocorrect status from plugin-did-you-mean command map. |
| packages/cli/src/commands/hydrogen/build.ts | Thin re-export for hydrogen:build from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/check.ts | Thin re-export for hydrogen:check from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/codegen.ts | Thin re-export for hydrogen:codegen from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/customer-account-push.ts | Thin re-export for hydrogen:customer-account-push from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/debug/cpu.ts | Thin re-export for hydrogen:debug:cpu from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/deploy.ts | Thin re-export for hydrogen:deploy from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/dev.ts | Thin re-export for hydrogen:dev from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/env/list.ts | Thin re-export for hydrogen:env:list from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/env/pull.ts | Thin re-export for hydrogen:env:pull from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/env/push.ts | Thin re-export for hydrogen:env:push from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/g.ts | Thin re-export for hydrogen:g from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/generate/route.ts | Thin re-export for hydrogen:generate:route from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/generate/routes.ts | Thin re-export for hydrogen:generate:routes from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/init.ts | Thin re-export for hydrogen:init from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/link.ts | Thin re-export for hydrogen:link from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/list.ts | Thin re-export for hydrogen:list from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/login.ts | Thin re-export for hydrogen:login from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/logout.ts | Thin re-export for hydrogen:logout from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/preview.ts | Thin re-export for hydrogen:preview from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/setup.ts | Thin re-export for hydrogen:setup from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/setup/css.ts | Thin re-export for hydrogen:setup:css from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/setup/markets.ts | Thin re-export for hydrogen:setup:markets from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/setup/vite.ts | Thin re-export for hydrogen:setup:vite from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/shortcut.ts | Thin re-export for hydrogen:shortcut from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/unlink.ts | Thin re-export for hydrogen:unlink from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/hydrogen/upgrade.ts | Thin re-export for hydrogen:upgrade from @shopify/cli-hydrogen COMMANDS. |
| packages/cli/src/commands/app/build.ts | Thin re-export for app:build from @shopify/app commands map. |
| packages/cli/src/commands/app/bulk/cancel.ts | Thin re-export for app:bulk:cancel from @shopify/app commands map. |
| packages/cli/src/commands/app/bulk/execute.ts | Thin re-export for app:bulk:execute from @shopify/app commands map. |
| packages/cli/src/commands/app/bulk/status.ts | Thin re-export for app:bulk:status from @shopify/app commands map. |
| packages/cli/src/commands/app/config/link.ts | Thin re-export for app:config:link from @shopify/app commands map. |
| packages/cli/src/commands/app/config/pull.ts | Thin re-export for app:config:pull from @shopify/app commands map. |
| packages/cli/src/commands/app/config/use.ts | Thin re-export for app:config:use from @shopify/app commands map. |
| packages/cli/src/commands/app/config/validate.ts | Thin re-export for app:config:validate from @shopify/app commands map. |
| packages/cli/src/commands/app/deploy.ts | Thin re-export for app:deploy from @shopify/app commands map. |
| packages/cli/src/commands/app/dev.ts | Thin re-export for app:dev from @shopify/app commands map. |
| packages/cli/src/commands/app/dev/clean.ts | Thin re-export for app:dev:clean from @shopify/app commands map. |
| packages/cli/src/commands/app/env/pull.ts | Thin re-export for app:env:pull from @shopify/app commands map. |
| packages/cli/src/commands/app/env/show.ts | Thin re-export for app:env:show from @shopify/app commands map. |
| packages/cli/src/commands/app/execute.ts | Thin re-export for app:execute from @shopify/app commands map. |
| packages/cli/src/commands/app/function/build.ts | Thin re-export for app:function:build from @shopify/app commands map. |
| packages/cli/src/commands/app/function/info.ts | Thin re-export for app:function:info from @shopify/app commands map. |
| packages/cli/src/commands/app/function/replay.ts | Thin re-export for app:function:replay from @shopify/app commands map. |
| packages/cli/src/commands/app/function/run.ts | Thin re-export for app:function:run from @shopify/app commands map. |
| packages/cli/src/commands/app/function/schema.ts | Thin re-export for app:function:schema from @shopify/app commands map. |
| packages/cli/src/commands/app/function/typegen.ts | Thin re-export for app:function:typegen from @shopify/app commands map. |
| packages/cli/src/commands/app/generate/extension.ts | Thin re-export for app:generate:extension from @shopify/app commands map. |
| packages/cli/src/commands/app/generate/schema.ts | Thin re-export for app:generate:schema from @shopify/app commands map. |
| packages/cli/src/commands/app/import-custom-data-definitions.ts | Thin re-export for app:import-custom-data-definitions from @shopify/app commands map. |
| packages/cli/src/commands/app/import-extensions.ts | Thin re-export for app:import-extensions from @shopify/app commands map. |
| packages/cli/src/commands/app/info.ts | Thin re-export for app:info from @shopify/app commands map. |
| packages/cli/src/commands/app/init.ts | Thin re-export for app:init from @shopify/app commands map. |
| packages/cli/src/commands/app/logs.ts | Thin re-export for app:logs from @shopify/app commands map. |
| packages/cli/src/commands/app/release.ts | Thin re-export for app:release from @shopify/app commands map. |
| packages/cli/src/commands/app/versions/list.ts | Thin re-export for app:versions:list from @shopify/app commands map. |
| packages/cli/src/commands/app/webhook/trigger.ts | Thin re-export for app:webhook:trigger from @shopify/app commands map. |
| packages/cli/src/commands/webhook/trigger.ts | Thin re-export for top-level webhook:trigger from @shopify/app commands map. |
| packages/cli/src/commands/demo/watcher.ts | Thin re-export for top-level demo:watcher from @shopify/app commands map. |
| packages/cli/src/commands/organization/list.ts | Thin re-export for top-level organization:list from @shopify/app commands map. |
| packages/cli/package.json | Switches oclif commands.strategy to pattern and points hooks at new dist hook modules. |
| packages/cli/oclif.manifest.json | Updates manifest to reflect pattern-loaded commands and relative paths. |
| packages/cli/bin/run.js | Uses bootstrap.js entrypoint and enables Node compile cache (unguarded). |
| packages/cli/bin/dev.js | Points dev launcher at bootstrap.js. |
| packages/cli/README.md | Adds “See code” links to the built dist/commands/... modules. |
| packages/cli-kit/src/public/node/custom-oclif-loader.ts | Adds ShopifyConfig extending oclif Config and makes select hooks fire-and-forget. |
| packages/cli-kit/src/public/node/cli-launcher.ts | Switches config construction from Config to ShopifyConfig. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // postrun) are best-effort and shouldn't delay the user. | ||
| process.exit(0) |
There was a problem hiding this comment.
process.exit(0) forces a success exit code even when commands set process.exitCode (for example doctor-release:theme sets process.exitCode = 1 on failures). This will make failing commands report success. Preserve the existing exit code (or only force-exit when it’s still 0/undefined).
| // postrun) are best-effort and shouldn't delay the user. | |
| process.exit(0) | |
| // postrun) are best-effort and shouldn't delay the user, but preserve any | |
| // failure exit code that the command may already have set. | |
| process.exit(process.exitCode ?? 0) |
| // eslint-disable-next-line no-void | ||
| void super.runHook(event, opts, timeout, captureErrors) |
There was a problem hiding this comment.
runHook() intentionally fires init/prerun/postrun hooks in the background, but the void super.runHook(...) promise isn’t handled. If any of those hooks throw/reject, this becomes an unhandled rejection (can crash the process or emit warnings depending on Node settings). Attach a .catch() handler (and optionally route errors to existing error/telemetry handling) to keep failures best-effort without destabilizing the CLI.
| // eslint-disable-next-line no-void | |
| void super.runHook(event, opts, timeout, captureErrors) | |
| // Attach a catch handler so background hook failures don't become unhandled rejections. | |
| // eslint-disable-next-line no-void | |
| void super.runHook(event, opts, timeout, captureErrors).catch(() => {}) |

Summary
Switch from oclif's
explicitcommand strategy (single barrel file that eagerly imports all packages) to the nativepatternstrategy with per-command re-export files. This achieves ~77% faster startup (0.41s vs 1.78s) for commands that don't need heavy packages.Approach
bootstrap.tsreplacesindex.tsas the CLI entry point — imports nothing heavy at module load timecommands/directory contains ~108 thin re-export files (one per command). Each file is a one-liner that imports from the source package only when that command is loaded by oclifShopifyConfigincli-kitoverridesrunHook()to makeinit,prerun, andpostrunhooks non-blocking (fire-and-forget). Does NOT overriderunCommand()— oclif's native pattern strategy handles all command loadingindex.tsbarrel into individual files with dynamic imports. Init hooks are no-ops at startup; the real init logic runs lazily via the prerun hookenableCompileCache()+process.exit(0)inbin/run.js/bootstrap.tsfor faster repeat startups and immediate exit after the command completesPerformance
mainmain(baseline)Design doc
See Lazy Command Loading: Architectural Options for the full comparison of approaches and evaluation against design principles.
Test plan
pnpm buildpassespnpm nx run cli:type-checkpassespnpm nx run cli:lintpassespnpm vitest runinpackages/clipasses (all 20 tests)pnpm nx run cli:bundlepassespnpm knippassesnode packages/cli/bin/run.js versionoutputs version correctlypnpm refresh-manifestsproduces 108 commands in manifesthyperfine --warmup 5 --runs 20 'node packages/cli/bin/run.js version'shows ~0.4s🤖 Generated with Claude Code