Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions configurations/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const aliases = (packagePath: string) => {
},
{find: '@shopify/theme', replacement: path.join(packagePath, '../theme/src/index')},
{find: '@shopify/organizations', replacement: path.join(packagePath, '../organizations/src/index')},
{find: '@shopify/plugin-did-you-mean', replacement: path.join(packagePath, '../plugin-did-you-mean/src/index')},
{find: '@shopify/store', replacement: path.join(packagePath, '../store/src/index')},
]
}
22 changes: 22 additions & 0 deletions packages/cli/src/command-registry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {loadCommand} from './command-registry.js'
import {describe, expect, test} from 'vitest'

describe('loadCommand', () => {
test('returns undefined for a command id that is not in the manifest', async () => {
await expect(loadCommand('definitely:not:a:command')).resolves.toBeUndefined()
})

test('loads an external oclif plugin command from its package command table', async () => {
// `commands` is owned by @oclif/plugin-commands, which is loaded through
// the full-package fallback rather than per-file loading.
const command = await loadCommand('commands')

expect(command).toBeTypeOf('function')
})

test('loads the plugins command from @oclif/plugin-plugins', async () => {
const command = await loadCommand('plugins')

expect(command).toBeTypeOf('function')
})
})
36 changes: 21 additions & 15 deletions packages/cli/src/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@
import {dirname, joinPath, moduleDirectory} from '@shopify/cli-kit/node/path'
import {existsSync, readFileSync} from 'fs'
import {fileURLToPath, pathToFileURL} from 'url'
import type {Command} from '@oclif/core'

type CommandClass = typeof Command

/**
* Look up a command class in the command table exported by an external plugin
* package. Each package compiles against its own `@oclif/core`, so its command
* classes are not statically assignable to this workspace's `typeof Command`;
* this helper is the single place where that boundary is bridged.
*/
function commandFromTable(table: unknown, id: string): CommandClass | undefined {
return (table as Record<string, CommandClass | undefined>)[id]
}

interface ManifestCommand {
customPluginName?: string
Expand Down Expand Up @@ -81,8 +94,7 @@ const packagesWithPerFileLoading = new Set(['@shopify/cli', '@shopify/app', '@sh
* derives the file path from the command ID, and imports only that file.
* Falls back to importing the full package for external plugins.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function loadCommand(id: string): Promise<any | undefined> {
export async function loadCommand(id: string): Promise<CommandClass | undefined> {
const commands = getManifestCommands()
const entry = commands[id]
if (!entry) return undefined
Expand Down Expand Up @@ -112,39 +124,33 @@ function resolveCommandRoot(id: string, packageName: string): string {
return resolvePackageDir(packageName)
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function loadCommandPerFile(id: string, packageName: string): Promise<any | undefined> {
async function loadCommandPerFile(id: string, packageName: string): Promise<CommandClass | undefined> {
const entryPoint = entryPointForCommand(id)
const root = resolveCommandRoot(id, packageName)
const modulePath = pathToFileURL(joinPath(root, entryPoint)).href
const module = await import(modulePath)
const module: {default?: CommandClass} = await import(modulePath)
return module.default
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function loadCommandFromPackage(id: string, packageName: string): Promise<any | undefined> {
async function loadCommandFromPackage(id: string, packageName: string): Promise<CommandClass | undefined> {
if (packageName === '@shopify/cli-hydrogen') {
const {COMMANDS} = await import('@shopify/cli-hydrogen')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (COMMANDS as any)?.[id]
return commandFromTable(COMMANDS, id)
}

if (packageName === '@oclif/plugin-commands') {
const {commands} = await import('@oclif/plugin-commands')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (commands as any)[id]
return commandFromTable(commands, id)
}

if (packageName === '@oclif/plugin-plugins') {
const {commands} = await import('@oclif/plugin-plugins')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (commands as any)[id]
return commandFromTable(commands, id)
}

if (packageName === '@shopify/plugin-did-you-mean') {
const {DidYouMeanCommands} = await import('@shopify/plugin-did-you-mean')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (DidYouMeanCommands as any)[id]
return commandFromTable(DidYouMeanCommands, id)
}

return undefined
Expand Down
Loading