Skip to content
Merged
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
25 changes: 25 additions & 0 deletions src/BaseCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@ class BaseCommand extends Command {

async init () {
await super.init()
// Normalize hooks from plugins loaded by oclif v2 into this v4 Config.
Comment thread
shazron marked this conversation as resolved.
// oclif v2 stores hooks as string arrays; v4 expects {identifier, target} objects.
// Only mutate when string hooks are present; guard against frozen plugin references.
const pluginList = typeof this.config.getPluginsList === 'function'
? this.config.getPluginsList()
: [...(this.config.plugins?.values() ?? [])]
for (const plugin of pluginList) {
if (!plugin.hooks) {
continue
}
for (const [event, hooks] of Object.entries(plugin.hooks)) {
const hooksArr = Array.isArray(hooks) ? hooks : [hooks]
if (!hooksArr.some(h => typeof h === 'string')) {
continue
}
try {
plugin.hooks[event] = hooksArr.map(h =>
typeof h === 'string' ? { identifier: 'default', target: h } : h
)
} catch {
// plugin.hooks is frozen or sealed; skip normalization for this event
}
}
Comment thread
shazron marked this conversation as resolved.
}

// setup a prompt that outputs to stderr
this.prompt = inquirer.createPromptModule({ output: process.stderr })

Expand Down
75 changes: 75 additions & 0 deletions test/BaseCommand.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,81 @@ test('init', async () => {
expect(inquirer.createPromptModule).toHaveBeenCalledWith({ output: process.stderr })
})

test('init normalizes oclif v2 string hooks to v4 object format', async () => {
const cmd = new TheCommand([])
const plugin = {
hooks: {
'pre-deploy-event-reg': ['./src/hooks/pre-deploy-event-reg.js'],
'post-deploy-event-reg': './src/hooks/post-deploy-event-reg.js',
'already-v4': [{ identifier: 'default', target: './src/hooks/foo.js' }],
mixed: ['./src/hooks/string.js', { identifier: 'named', target: './src/hooks/obj.js' }]
}
}
cmd.config = global.createOclifMockConfig({
getPluginsList: jest.fn().mockReturnValue([plugin])
})
await cmd.init()
expect(plugin.hooks['pre-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/pre-deploy-event-reg.js' }])
expect(plugin.hooks['post-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/post-deploy-event-reg.js' }])
expect(plugin.hooks['already-v4']).toEqual([{ identifier: 'default', target: './src/hooks/foo.js' }])
expect(plugin.hooks['mixed']).toEqual([
{ identifier: 'default', target: './src/hooks/string.js' },
{ identifier: 'named', target: './src/hooks/obj.js' }
])
})

test('init skips plugins with no hooks', async () => {
const cmd = new TheCommand([])
const plugin = { name: 'no-hooks-plugin' }
cmd.config = global.createOclifMockConfig({
getPluginsList: jest.fn().mockReturnValue([plugin])
})
await expect(cmd.init()).resolves.not.toThrow()
})

test('init does not mutate hooks already in v4 format', async () => {
const cmd = new TheCommand([])
const original = [{ identifier: 'default', target: './src/hooks/foo.js' }]
const plugin = { hooks: { 'some-event': original } }
cmd.config = global.createOclifMockConfig({
getPluginsList: jest.fn().mockReturnValue([plugin])
})
await cmd.init()
expect(plugin.hooks['some-event']).toBe(original) // same reference, not replaced
})

test('init falls back to this.config.plugins Map when getPluginsList is unavailable', async () => {
const cmd = new TheCommand([])
const plugin = { hooks: { 'pre-deploy-event-reg': ['./src/hooks/hook.js'] } }
const mockConfig = global.createOclifMockConfig({
plugins: new Map([['test-plugin', plugin]])
})
delete mockConfig.getPluginsList
cmd.config = mockConfig
await cmd.init()
expect(plugin.hooks['pre-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/hook.js' }])
})

test('init handles config with neither getPluginsList nor plugins without throwing', async () => {
const cmd = new TheCommand([])
const mockConfig = global.createOclifMockConfig()
delete mockConfig.getPluginsList
delete mockConfig.plugins
cmd.config = mockConfig
await expect(cmd.init()).resolves.not.toThrow()
})

test('init skips normalization gracefully when plugin hooks object is frozen', async () => {
const cmd = new TheCommand([])
const plugin = { hooks: Object.freeze({ 'pre-deploy-event-reg': ['./src/hooks/hook.js'] }) }
cmd.config = global.createOclifMockConfig({
getPluginsList: jest.fn().mockReturnValue([plugin])
})
await expect(cmd.init()).resolves.not.toThrow()
// hooks remain as-is since the frozen object blocked the assignment
expect(plugin.hooks['pre-deploy-event-reg']).toEqual(['./src/hooks/hook.js'])
})

test('catch', async () => {
const cmd = new TheCommand([])
cmd.config = global.createOclifMockConfig()
Expand Down
1 change: 1 addition & 0 deletions test/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ global.createOclifMockConfig = (overrides = {}) => ({
runHook: jest.fn().mockResolvedValue({ successes: [] }),
runCommand: jest.fn(),
findCommand: jest.fn(),
getPluginsList: jest.fn().mockReturnValue([]),
...overrides
})

Expand Down
Loading