Skip to content

Commit 16804e8

Browse files
Update command-registry.ts
1 parent d9c22b8 commit 16804e8

File tree

1 file changed

+75
-92
lines changed

1 file changed

+75
-92
lines changed

packages/cli/src/command-registry.ts

Lines changed: 75 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,111 @@
11
/**
2-
* Lazy command registry.
2+
* Manifest-based lazy command loader.
33
*
4-
* Maps each command ID to a dynamic import function that loads ONLY that command's module.
5-
* This avoids importing the entire index.ts (which pulls in `\@shopify/app`, `\@shopify/theme`,
6-
* `\@shopify/cli-hydrogen`, etc.) just to run a single command.
4+
* Reads the oclif manifest to discover which package owns each command, then
5+
* derives the entry point from the command ID using a naming convention:
6+
* dist/cli/commands/\{id with : replaced by /\}.js
7+
*
8+
* This lets us dynamically import ONLY the specific command file instead of
9+
* loading every command from the package index.
10+
*
11+
* Commands from external plugins (cli-hydrogen, oclif plugins) fall back to
12+
* importing the full package.
713
*/
14+
import {dirname, joinPath, moduleDirectory} from '@shopify/cli-kit/node/path'
15+
import {readFileSync} from 'fs'
16+
import {createRequire} from 'module'
17+
import {pathToFileURL} from 'url'
818

9-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10-
type CommandLoader = () => Promise<any>
11-
12-
/* eslint-disable @typescript-eslint/naming-convention */
13-
const cliCommands: Record<string, CommandLoader> = {
14-
version: () => import('./cli/commands/version.js'),
15-
search: () => import('./cli/commands/search.js'),
16-
upgrade: () => import('./cli/commands/upgrade.js'),
17-
'auth:logout': () => import('./cli/commands/auth/logout.js'),
18-
'auth:login': () => import('./cli/commands/auth/login.js'),
19-
'debug:command-flags': () => import('./cli/commands/debug/command-flags.js'),
20-
'kitchen-sink:async': () => import('./cli/commands/kitchen-sink/async.js'),
21-
'kitchen-sink:prompts': () => import('./cli/commands/kitchen-sink/prompts.js'),
22-
'kitchen-sink:static': () => import('./cli/commands/kitchen-sink/static.js'),
23-
'kitchen-sink': () => import('./cli/commands/kitchen-sink/index.js'),
24-
'doctor-release': () => import('./cli/commands/doctor-release/doctor-release.js'),
25-
'doctor-release:theme': () => import('./cli/commands/doctor-release/theme/index.js'),
26-
'docs:generate': () => import('./cli/commands/docs/generate.js'),
27-
help: () => import('./cli/commands/help.js'),
28-
'notifications:list': () => import('./cli/commands/notifications/list.js'),
29-
'notifications:generate': () => import('./cli/commands/notifications/generate.js'),
30-
'cache:clear': () => import('./cli/commands/cache/clear.js'),
19+
const require = createRequire(import.meta.url)
20+
21+
interface ManifestCommand {
22+
customPluginName?: string
23+
pluginName?: string
3124
}
32-
/* eslint-enable @typescript-eslint/naming-convention */
33-
34-
const appCommandIds = [
35-
'app:build',
36-
'app:bulk:cancel',
37-
'app:bulk:status',
38-
'app:deploy',
39-
'app:dev',
40-
'app:dev:clean',
41-
'app:logs',
42-
'app:logs:sources',
43-
'app:import-custom-data-definitions',
44-
'app:import-extensions',
45-
'app:info',
46-
'app:init',
47-
'app:release',
48-
'app:config:link',
49-
'app:config:use',
50-
'app:config:pull',
51-
'app:env:pull',
52-
'app:env:show',
53-
'app:execute',
54-
'app:bulk:execute',
55-
'app:generate:schema',
56-
'app:function:build',
57-
'app:function:replay',
58-
'app:function:run',
59-
'app:function:info',
60-
'app:function:schema',
61-
'app:function:typegen',
62-
'app:generate:extension',
63-
'app:versions:list',
64-
'app:webhook:trigger',
65-
'webhook:trigger',
66-
'demo:watcher',
67-
'organization:list',
68-
]
6925

70-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71-
function searchForDefault(module: any): any {
72-
if (module.default?.run) return module.default
73-
for (const value of Object.values(module)) {
74-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
75-
if (typeof (value as any) === 'function' && typeof (value as any).run === 'function') return value
26+
let cachedCommands: Record<string, ManifestCommand> | undefined
27+
28+
function getManifestCommands(): Record<string, ManifestCommand> {
29+
if (!cachedCommands) {
30+
const manifestPath = joinPath(moduleDirectory(import.meta.url), '..', 'oclif.manifest.json')
31+
cachedCommands = JSON.parse(readFileSync(manifestPath, 'utf8')).commands
7632
}
77-
return undefined
33+
return cachedCommands!
34+
}
35+
36+
const packageDirCache = new Map<string, string>()
37+
38+
function resolvePackageDir(packageName: string): string {
39+
let dir = packageDirCache.get(packageName)
40+
if (!dir) {
41+
dir = dirname(require.resolve(`${packageName}/package.json`))
42+
packageDirCache.set(packageName, dir)
43+
}
44+
return dir
7845
}
7946

47+
const entryPointOverrides: Record<string, string> = {
48+
'app:logs:sources': 'dist/cli/commands/app/app-logs/sources.js',
49+
'demo:watcher': 'dist/cli/commands/app/demo/watcher.js',
50+
'kitchen-sink': 'dist/cli/commands/kitchen-sink/index.js',
51+
'doctor-release': 'dist/cli/commands/doctor-release/doctor-release.js',
52+
'doctor-release:theme': 'dist/cli/commands/doctor-release/theme/index.js',
53+
}
54+
55+
function entryPointForCommand(id: string): string {
56+
return entryPointOverrides[id] ?? `dist/cli/commands/${id.replace(/:/g, '/')}.js`
57+
}
58+
59+
const packagesWithPerFileLoading = new Set(['@shopify/cli', '@shopify/app', '@shopify/theme'])
60+
8061
/**
8162
* Load a command class by its ID.
82-
* Returns the command class, or undefined if not found.
63+
*
64+
* Looks up the command in the oclif manifest to find the owning package,
65+
* derives the file path from the command ID, and imports only that file.
66+
* Falls back to importing the full package for external plugins.
8367
*/
8468
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8569
export async function loadCommand(id: string): Promise<any | undefined> {
86-
// Check CLI-local commands first
87-
const cliLoader = cliCommands[id]
88-
if (cliLoader) {
89-
const module = await cliLoader()
90-
return searchForDefault(module)
91-
}
70+
const commands = getManifestCommands()
71+
const entry = commands[id]
72+
if (!entry) return undefined
9273

93-
// App commands
94-
if (appCommandIds.includes(id)) {
95-
const {commands} = await import('@shopify/app')
96-
return commands[id]
97-
}
74+
const packageName = entry.customPluginName ?? entry.pluginName
75+
if (!packageName) return undefined
9876

99-
// Theme commands
100-
if (id.startsWith('theme:')) {
101-
const themeModule = await import('@shopify/theme')
102-
return (themeModule.default as Record<string, unknown>)?.[id]
77+
if (packagesWithPerFileLoading.has(packageName)) {
78+
const packageDir = resolvePackageDir(packageName)
79+
const entryPoint = entryPointForCommand(id)
80+
const modulePath = pathToFileURL(joinPath(packageDir, entryPoint)).href
81+
const module = await import(modulePath)
82+
return module.default
10383
}
10484

105-
// Hydrogen commands
106-
if (id.startsWith('hydrogen:')) {
85+
return loadCommandFromPackage(id, packageName)
86+
}
87+
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
async function loadCommandFromPackage(id: string, packageName: string): Promise<any | undefined> {
90+
if (packageName === '@shopify/cli-hydrogen') {
10791
const {COMMANDS} = await import('@shopify/cli-hydrogen')
10892
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10993
return (COMMANDS as any)?.[id]
11094
}
11195

112-
// Plugin commands
113-
if (id === 'commands') {
96+
if (packageName === '@oclif/plugin-commands') {
11497
const {commands} = await import('@oclif/plugin-commands')
11598
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11699
return (commands as any)[id]
117100
}
118101

119-
if (id.startsWith('plugins')) {
102+
if (packageName === '@oclif/plugin-plugins') {
120103
const {commands} = await import('@oclif/plugin-plugins')
121104
// eslint-disable-next-line @typescript-eslint/no-explicit-any
122105
return (commands as any)[id]
123106
}
124107

125-
if (id.startsWith('config:autocorrect')) {
108+
if (packageName === '@shopify/plugin-did-you-mean') {
126109
const {DidYouMeanCommands} = await import('@shopify/plugin-did-you-mean')
127110
// eslint-disable-next-line @typescript-eslint/no-explicit-any
128111
return (DidYouMeanCommands as any)[id]

0 commit comments

Comments
 (0)