diff --git a/packages/cli-helpers/src/lib/__tests__/version.test.ts b/packages/cli-helpers/src/lib/__tests__/version.test.ts index 8e8872708ba2..cbcd8bb43e68 100644 --- a/packages/cli-helpers/src/lib/__tests__/version.test.ts +++ b/packages/cli-helpers/src/lib/__tests__/version.test.ts @@ -261,14 +261,6 @@ describe('version compatibility detection', () => { ) }) - test('throws if no redwoodjs engine is found', async () => { - await expect( - getCompatibilityData('@scope/package-name', '0.0.1') - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The package '@scope/package-name' does not specify a RedwoodJS compatibility version/range"` - ) - }) - test('throws if no latest version could be found', async () => { jest.spyOn(global, 'fetch').mockImplementation(() => { return { diff --git a/packages/cli-helpers/src/lib/version.ts b/packages/cli-helpers/src/lib/version.ts index 1ba1a426a833..a0ec4fec0ed2 100644 --- a/packages/cli-helpers/src/lib/version.ts +++ b/packages/cli-helpers/src/lib/version.ts @@ -67,18 +67,15 @@ export async function getCompatibilityData( ? packument['dist-tags'][preferredVersionOrTag] : preferredVersionOrTag - // Does that version of the package support the current version of RedwoodJS? + // Extract the package's redwoodjs engine specification for the preferred version const packageRedwoodSpecification = packument.versions[preferredVersion].engines?.redwoodjs - if (packageRedwoodSpecification === undefined) { - throw new Error( - `The package '${packageName}' does not specify a RedwoodJS compatibility version/range` - ) - } - // We have to use the semver.intersects function because the package's redwoodjs engine could be a range - if (semver.intersects(projectRedwoodVersion, packageRedwoodSpecification)) { + if ( + packageRedwoodSpecification !== undefined && + semver.intersects(projectRedwoodVersion, packageRedwoodSpecification) + ) { const tag = getCorrespondingTag(preferredVersion, packument['dist-tags']) return { preferred: { diff --git a/packages/cli/src/__tests__/__snapshots__/plugin.test.js.snap b/packages/cli/src/__tests__/__snapshots__/plugin.test.js.snap new file mode 100644 index 000000000000..b72bcb8b9f18 --- /dev/null +++ b/packages/cli/src/__tests__/__snapshots__/plugin.test.js.snap @@ -0,0 +1,602 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`plugin loading correct loading for @redwoodjs namespace help ('') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for @redwoodjs namespace help ('--help') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for @redwoodjs namespace help ('-h') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for known redwood command (with cache) 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for known redwood command (without cache) 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for known third party command (with cache) 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for known third party command (without cache) 1`] = ` +[ + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package-second-example", + { + "third-party-other": { + "aliases": [ + "tpo", + "thirdPartyOther", + ], + "description": "Some other third party command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for root help ('') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for root help ('--help') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for root help ('-h') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for third party namespace help ('') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for third party namespace help ('--help') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for third party namespace help ('-h') 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for unknown namespace (no command) 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for unknown namespace (with command) 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for unknown redwood command 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], + [ + "@redwoodjs/cli-some-package-not-in-cache", + { + "some-other-command": { + "aliases": [ + "soc", + "someOtherCommand", + ], + "description": "Some example other command", + }, + }, + ], +] +`; + +exports[`plugin loading correct loading for unknown third party command 1`] = ` +[ + [ + "@redwoodjs/cli-some-package", + { + "some-command": { + "aliases": [ + "sc", + "someCommand", + ], + "description": "Some example command", + }, + }, + ], + [ + "@bluewoodjs/cli-some-package", + { + "third-party": { + "aliases": [ + "tp", + "thirdParty", + ], + "description": "Some third party command", + }, + }, + ], +] +`; diff --git a/packages/cli/src/__tests__/plugin.test.js b/packages/cli/src/__tests__/plugin.test.js new file mode 100644 index 000000000000..3be4cca8ec4a --- /dev/null +++ b/packages/cli/src/__tests__/plugin.test.js @@ -0,0 +1,1316 @@ +import fs from 'fs' + +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' + +import { getConfig, getPaths } from '@redwoodjs/project-config' + +import * as pluginLib from '../lib/plugin' +import { loadPlugins } from '../plugin' + +jest.mock('fs') +jest.mock('@redwoodjs/project-config', () => { + return { + getPaths: jest.fn(), + getConfig: jest.fn(), + } +}) +jest.mock('../lib/packages', () => { + return { + installModule: jest.fn(), + isModuleInstalled: jest.fn().mockReturnValue(true), + } +}) + +function getMockYargsInstance() { + return yargs(hideBin(process.argv)) + .scriptName('rw') + .command({ + command: 'built-in', + description: 'Some builtin command', + aliases: ['bi', 'builtIn'], + }) + .exitProcess(false) +} + +describe('command information caching', () => { + beforeEach(() => { + getPaths.mockReturnValue({ + generated: { + base: '', + }, + }) + }) + + test('returns the correct cache when no local cache exists', () => { + const cache = pluginLib.loadCommadCache() + expect(cache).toEqual({ + ...pluginLib.PLUGIN_CACHE_DEFAULT, + _builtin: pluginLib.PLUGIN_CACHE_BUILTIN, + }) + }) + + test('returns the correct cache when a local cache exists', () => { + const anExistingDefaultCacheEntryKey = Object.keys( + pluginLib.PLUGIN_CACHE_DEFAULT + )[0] + const anExistingDefaultCacheEntry = { + [anExistingDefaultCacheEntryKey]: { + ...pluginLib.PLUGIN_CACHE_DEFAULT[anExistingDefaultCacheEntryKey], + description: + 'Mutated description which should be reverted back to default', + }, + } + const exampleCacheEntry = { + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + } + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + ...exampleCacheEntry, + ...anExistingDefaultCacheEntry, + }), + }) + + const cache = pluginLib.loadCommadCache() + expect(cache).toEqual({ + ...pluginLib.PLUGIN_CACHE_DEFAULT, + ...exampleCacheEntry, + _builtin: pluginLib.PLUGIN_CACHE_BUILTIN, + }) + }) +}) + +describe('plugin loading', () => { + beforeAll(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}) + }) + + beforeEach(() => { + getPaths.mockReturnValue({ + generated: { + base: '', + }, + }) + + jest.spyOn(pluginLib, 'loadCommadCache') + jest.spyOn(pluginLib, 'loadPluginPackage') + jest.spyOn(pluginLib, 'checkPluginListAndWarn') + jest.spyOn(pluginLib, 'saveCommandCache') + }) + + afterEach(() => { + pluginLib.loadCommadCache.mockRestore() + pluginLib.checkPluginListAndWarn.mockRestore() + pluginLib.loadPluginPackage.mockRestore() + pluginLib.saveCommandCache.mockRestore() + + console.log.mockClear() + }) + + afterAll(() => { + console.log.mockRestore() + }) + + test('no plugins are loaded for --version at the root level', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '--version'] + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(0) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(0) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(0) + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(0) + + process.argv = originalArgv + }) + + test('no plugins are loaded if it is a built-in command', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', pluginLib.PLUGIN_CACHE_BUILTIN[0]] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [], + autoInstall: true, + }, + }, + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(0) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(0) + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(0) + + getConfig.mockRestore() + process.argv = originalArgv + }) + + test.each([['--help'], ['-h'], ['']])( + `correct loading for root help ('%s')`, + async (command) => { + const originalArgv = process.argv + process.argv = ['node', 'rw', command] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package when it was not in the cache + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(1) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package-not-in-cache', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the help output contains the correct commands + const helpOutput = await yargsInstance.getHelp() + expect(helpOutput).toContain('rw built-in') + expect(helpOutput).toContain('Some builtin command') + expect(helpOutput).toContain('[aliases: bi, builtIn]') + expect(helpOutput).toContain('rw some-command') + expect(helpOutput).toContain('Some example command') + expect(helpOutput).toContain('[aliases: sc, someCommand]') + expect(helpOutput).toContain('rw some-other-command') + expect(helpOutput).toContain('Some example other command') + expect(helpOutput).toContain('[aliases: soc, someOtherCommand]') + expect(helpOutput).toContain('rw @bluewoodjs ') + expect(helpOutput).toContain('Commands from @bluewoodjs') + + getConfig.mockRestore() + process.argv = originalArgv + } + ) + + test.each([['--help'], ['-h'], ['']])( + `correct loading for @redwoodjs namespace help ('%s')`, + async (command) => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '@redwoodjs', command] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package when it was not in the cache + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(1) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package-not-in-cache', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the help output contains the correct commands + const helpOutput = await yargsInstance.getHelp() + expect(helpOutput).toContain('rw built-in') + expect(helpOutput).toContain('Some builtin command') + expect(helpOutput).toContain('[aliases: bi, builtIn]') + expect(helpOutput).toContain('rw some-command') + expect(helpOutput).toContain('Some example command') + expect(helpOutput).toContain('[aliases: sc, someCommand]') + expect(helpOutput).toContain('rw some-other-command') + expect(helpOutput).toContain('Some example other command') + expect(helpOutput).toContain('[aliases: soc, someOtherCommand]') + expect(helpOutput).not.toContain('rw @bluewoodjs ') + expect(helpOutput).not.toContain('Commands from @bluewoodjs') + + getConfig.mockRestore() + process.argv = originalArgv + } + ) + + test.each([['--help'], ['-h'], ['']])( + `correct loading for third party namespace help ('%s')`, + async (command) => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '@bluewoodjs', command] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have NOT loaded the package when it was not in the cache + // because it is not in the correct namespace + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(0) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the help output contains the correct commands + const helpOutput = await yargsInstance.getHelp() + expect(helpOutput).toContain('rw @bluewoodjs ') + expect(helpOutput).toContain('Commands from @bluewoodjs') + expect(helpOutput).toContain('rw @bluewoodjs third-party') + expect(helpOutput).toContain('Some third party command') + expect(helpOutput).toContain('[aliases: tp, thirdParty]') + + getConfig.mockRestore() + process.argv = originalArgv + } + ) + + test('correct loading for unknown namespace (no command)', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '@greenwoodjs'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package when it was not in the cache + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(1) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package-not-in-cache', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the help output contains the correct commands + const helpOutput = await yargsInstance.getHelp() + expect(helpOutput).toContain('rw built-in') + expect(helpOutput).toContain('Some builtin command') + expect(helpOutput).toContain('[aliases: bi, builtIn]') + expect(helpOutput).toContain('rw some-command') + expect(helpOutput).toContain('Some example command') + expect(helpOutput).toContain('[aliases: sc, someCommand]') + expect(helpOutput).toContain('rw some-other-command') + expect(helpOutput).toContain('Some example other command') + expect(helpOutput).toContain('[aliases: soc, someOtherCommand]') + expect(helpOutput).toContain('rw @bluewoodjs ') + expect(helpOutput).toContain('Commands from @bluewoodjs') + + getConfig.mockRestore() + process.argv = originalArgv + }) + test('correct loading for unknown namespace (with command)', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '@greenwoodjs', 'anything'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package when it was not in the cache + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(1) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package-not-in-cache', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the help output contains the correct commands + const helpOutput = await yargsInstance.getHelp() + expect(helpOutput).toContain('rw built-in') + expect(helpOutput).toContain('Some builtin command') + expect(helpOutput).toContain('[aliases: bi, builtIn]') + expect(helpOutput).toContain('rw some-command') + expect(helpOutput).toContain('Some example command') + expect(helpOutput).toContain('[aliases: sc, someCommand]') + expect(helpOutput).toContain('rw some-other-command') + expect(helpOutput).toContain('Some example other command') + expect(helpOutput).toContain('[aliases: soc, someOtherCommand]') + expect(helpOutput).toContain('rw @bluewoodjs ') + expect(helpOutput).toContain('Commands from @bluewoodjs') + + getConfig.mockRestore() + process.argv = originalArgv + }) + + test('correct loading for known redwood command (with cache)', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', 'someCommand'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + pluginLib.loadPluginPackage.mockImplementation((packageName) => { + if (packageName === '@redwoodjs/cli-some-package') { + return { + commands: [ + { + command: 'some-command', + description: 'Some example command', + aliases: ['sc', 'someCommand'], + builder: () => {}, + handler: () => { + console.log('MARKER') + }, + }, + ], + } + } + throw new Error(`Unexpected behaviour: loading ${packageName}`) + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package - only the one we need + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(1) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the right handler was invoked + await yargsInstance.parse() + expect(console.log).toHaveBeenCalledWith('MARKER') + + getConfig.mockRestore() + process.argv = originalArgv + }) + test('correct loading for known redwood command (without cache)', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', 'someCommand'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + jest.mock( + '@redwoodjs/cli-some-package', + () => { + return { + commands: [ + { + command: 'some-command', + description: 'Some example command', + aliases: ['sc', 'someCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({}), + }) + + pluginLib.loadPluginPackage.mockImplementation((packageName) => { + if (packageName === '@redwoodjs/cli-some-package') { + return { + commands: [ + { + command: 'some-command', + description: 'Some example command', + aliases: ['sc', 'someCommand'], + builder: () => {}, + handler: () => { + console.log('MARKER SOME') + }, + }, + ], + } + } + if (packageName === '@redwoodjs/cli-some-package-not-in-cache') { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + builder: () => {}, + handler: () => { + console.log('MARKER SOME OTHER') + }, + }, + ], + } + } + throw new Error(`Unexpected behaviour: loading ${packageName}`) + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package - all in the namespace + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(2) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package', + undefined, + true + ) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package-not-in-cache', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the right handler was invoked + await yargsInstance.parse() + expect(console.log).toHaveBeenCalledWith('MARKER SOME') + expect(console.log).not.toHaveBeenCalledWith('MARKER SOME OTHER') + + getConfig.mockRestore() + process.argv = originalArgv + }) + test('correct loading for unknown redwood command', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', 'unknownCommand'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + pluginLib.loadPluginPackage.mockImplementation((packageName) => { + if (packageName === '@redwoodjs/cli-some-package') { + return { + commands: [ + { + command: 'some-command', + description: 'Some example command', + aliases: ['sc', 'someCommand'], + builder: () => {}, + handler: () => { + console.log('MARKER SOME') + }, + }, + ], + } + } + if (packageName === '@redwoodjs/cli-some-package-not-in-cache') { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + builder: () => {}, + handler: () => { + console.log('MARKER SOME OTHER') + }, + }, + ], + } + } + throw new Error(`Unexpected behaviour: loading ${packageName}`) + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package that we couldn't rule out from the cache + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(1) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@redwoodjs/cli-some-package-not-in-cache', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the help output contains the correct commands + const helpOutput = await yargsInstance.getHelp() + expect(helpOutput).toContain('rw built-in') + expect(helpOutput).toContain('Some builtin command') + expect(helpOutput).toContain('[aliases: bi, builtIn]') + expect(helpOutput).toContain('rw some-command') + expect(helpOutput).toContain('Some example command') + expect(helpOutput).toContain('[aliases: sc, someCommand]') + expect(helpOutput).toContain('rw some-other-command') + expect(helpOutput).toContain('Some example other command') + expect(helpOutput).toContain('[aliases: soc, someOtherCommand]') + expect(helpOutput).toContain('rw @bluewoodjs ') + expect(helpOutput).toContain('Commands from @bluewoodjs') + + // Rudimentary check that the right handler was invoked + await yargsInstance.parse() + expect(console.log).not.toHaveBeenCalledWith('MARKER SOME') + expect(console.log).not.toHaveBeenCalledWith('MARKER SOME OTHER') + + getConfig.mockRestore() + process.argv = originalArgv + }) + + test('correct loading for known third party command (with cache)', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '@bluewoodjs', 'tp'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + jest.mock( + '@redwoodjs/cli-some-package-not-in-cache', + () => { + return { + commands: [ + { + command: 'some-other-command', + description: 'Some example other command', + aliases: ['soc', 'someOtherCommand'], + }, + ], + } + }, + { virtual: true } + ) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + pluginLib.loadPluginPackage.mockImplementation((packageName) => { + if (packageName === '@bluewoodjs/cli-some-package') { + return { + commands: [ + { + command: 'third-party', + description: 'Some third party command', + aliases: ['tp', 'thirdParty'], + builder: () => {}, + handler: () => { + console.log('MARKER') + }, + }, + ], + } + } + throw new Error(`Unexpected behaviour: loading ${packageName}`) + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package - only the one we need + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(1) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@bluewoodjs/cli-some-package', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the right handler was invoked + await yargsInstance.parse() + expect(console.log).toHaveBeenCalledWith('MARKER') + + getConfig.mockRestore() + process.argv = originalArgv + }) + test('correct loading for known third party command (without cache)', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '@bluewoodjs', 'tpo'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + { + package: '@bluewoodjs/cli-some-package-second-example', + }, + ], + autoInstall: true, + }, + }, + }) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({}), + }) + + pluginLib.loadPluginPackage.mockImplementation((packageName) => { + if (packageName === '@bluewoodjs/cli-some-package') { + return { + commands: [ + { + command: 'third-party', + description: 'Some third party command', + aliases: ['tp', 'thirdParty'], + builder: () => {}, + handler: () => { + console.log('MARKER TP') + }, + }, + ], + } + } + if (packageName === '@bluewoodjs/cli-some-package-second-example') { + return { + commands: [ + { + command: 'third-party-other', + description: 'Some other third party command', + aliases: ['tpo', 'thirdPartyOther'], + builder: () => {}, + handler: () => { + console.log('MARKER TPO') + }, + }, + ], + } + } + throw new Error(`Unexpected behaviour: loading ${packageName}`) + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package - only the one we need + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(2) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@bluewoodjs/cli-some-package', + undefined, + true + ) + expect(pluginLib.loadPluginPackage).toHaveBeenCalledWith( + '@bluewoodjs/cli-some-package-second-example', + undefined, + true + ) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the right handler was invoked + await yargsInstance.parse() + expect(console.log).not.toHaveBeenCalledWith('MARKER TP') + expect(console.log).toHaveBeenCalledWith('MARKER TPO') + + getConfig.mockRestore() + process.argv = originalArgv + }) + test('correct loading for unknown third party command', async () => { + const originalArgv = process.argv + process.argv = ['node', 'rw', '@bluewoodjs', 'unknownCommand'] + + getConfig.mockReturnValue({ + experimental: { + cli: { + plugins: [ + { + package: '@redwoodjs/cli-some-package', + }, + { + package: '@redwoodjs/cli-some-package-not-in-cache', + }, + { + package: '@bluewoodjs/cli-some-package', + }, + ], + autoInstall: true, + }, + }, + }) + fs.__setMockFiles({ + ['commandCache.json']: JSON.stringify({ + '@redwoodjs/cli-some-package': { + 'some-command': { + aliases: ['sc', 'someCommand'], + description: 'Some example command', + }, + }, + '@bluewoodjs/cli-some-package': { + 'third-party': { + aliases: ['tp', 'thirdParty'], + description: 'Some third party command', + }, + }, + }), + }) + + pluginLib.loadPluginPackage.mockImplementation((packageName) => { + if (packageName === '@bluewoodjs/cli-some-package') { + return { + commands: [ + { + command: 'third-party', + description: 'Some third party command', + aliases: ['tp', 'thirdParty'], + builder: () => {}, + handler: () => { + console.log('MARKER SOME') + }, + }, + ], + } + } + throw new Error(`Unexpected behaviour: loading ${packageName}`) + }) + + const yargsInstance = getMockYargsInstance() + await loadPlugins(yargsInstance) + + expect(pluginLib.loadCommadCache).toHaveBeenCalledTimes(1) + expect(pluginLib.checkPluginListAndWarn).toHaveBeenCalledTimes(1) + + // Should have loaded the package that we couldn't rule out from the cache + expect(pluginLib.loadPluginPackage).toHaveBeenCalledTimes(0) + + // Should have saved the cache with the new package + expect(pluginLib.saveCommandCache).toHaveBeenCalledTimes(1) + const knownPlugins = + getConfig.mock.results[0].value.experimental.cli.plugins.map( + (plugin) => plugin.package + ) + const saveCommandCacheArg = Object.entries( + pluginLib.saveCommandCache.mock.calls[0][0] + ).filter(([key]) => knownPlugins.includes(key)) + expect(saveCommandCacheArg).toMatchSnapshot() + + // Rudimentary check that the help output contains the correct commands + const helpOutput = await yargsInstance.getHelp() + expect(helpOutput).not.toContain('rw built-in') + expect(helpOutput).not.toContain('Some builtin command') + expect(helpOutput).not.toContain('[aliases: bi, builtIn]') + expect(helpOutput).not.toContain('rw some-command') + expect(helpOutput).not.toContain('Some example command') + expect(helpOutput).not.toContain('[aliases: sc, someCommand]') + expect(helpOutput).not.toContain('rw some-other-command') + expect(helpOutput).not.toContain('Some example other command') + expect(helpOutput).not.toContain('[aliases: soc, someOtherCommand]') + expect(helpOutput).toContain('rw @bluewoodjs ') + expect(helpOutput).toContain('Commands from @bluewoodjs') + + // Rudimentary check that the right handler was invoked + await yargsInstance.parse() + expect(console.log).not.toHaveBeenCalledWith('MARKER') + + getConfig.mockRestore() + process.argv = originalArgv + }) +}) diff --git a/packages/cli/src/lib/plugin.js b/packages/cli/src/lib/plugin.js new file mode 100644 index 000000000000..d5fc70d3fb98 --- /dev/null +++ b/packages/cli/src/lib/plugin.js @@ -0,0 +1,279 @@ +import fs from 'fs' +import path from 'path' + +import chalk from 'chalk' + +import { getCompatibilityData } from '@redwoodjs/cli-helpers' + +import { installModule, isModuleInstalled } from './packages' + +import { getPaths } from './index' + +const { Select } = require('enquirer') + +/** + * The file inside .redwood which will contain cached plugin command mappings + */ +const PLUGIN_CACHE_FILENAME = 'commandCache.json' + +/** + * A cache of yargs information for redwood commands that are available from plugins. + * + * This is intended to be used for commands which lazy install their dependencies so that + * this information otherwise would not be available and help output would be unavailable/ + * incorrect. + */ +export const PLUGIN_CACHE_DEFAULT = { + '@redwoodjs/cli-storybook': { + storybook: { + aliases: ['sb'], + description: + 'Launch Storybook: a tool for building UI components and pages in isolation', + }, + }, + '@redwoodjs/cli-data-migrate': { + 'data-migrate ': { + aliases: ['dataMigrate', 'dm'], + description: 'Migrate the data in your database', + }, + }, +} + +/** + * A list of commands that are built into the CLI and require no plugin to be loaded. + */ +export const PLUGIN_CACHE_BUILTIN = [ + 'build', + 'check', + 'diagnostics', + 'console', + 'c', + 'deploy', + 'destroy', + 'd', + 'dev', + 'exec', + 'experimental', + 'exp', + 'generate', + 'g', + 'info', + 'lint', + 'prerender', + 'render', + 'prisma', + 'record', + 'serve', + 'setup', + 'test', + 'ts-to-js', + 'type-check', + 'tsc', + 'tc', + 'upgrade', +] + +export function loadCommadCache() { + // Always default to the default cache + let pluginCommandCache = PLUGIN_CACHE_DEFAULT + const commandCachePath = path.join( + getPaths().generated.base, + PLUGIN_CACHE_FILENAME + ) + try { + const localCommandCache = JSON.parse(fs.readFileSync(commandCachePath)) + // This validity check is rather naive but it exists to invalidate a + // previous format of the cache file + let valid = true + for (const [key, value] of Object.entries(localCommandCache)) { + if (key === '_builtin') { + continue + } + valid &&= !Array.isArray(value) + } + if (valid) { + // Merge the default cache with the local cache but ensure the default + // cache takes precedence - this ensure the cache is consistent with the + // current version of the framework + pluginCommandCache = { + ...localCommandCache, + ...PLUGIN_CACHE_DEFAULT, + } + } + } catch (error) { + // If the cache file doesn't exist we can just ignore it and continue + if (error.code !== 'ENOENT') { + console.error(`Error loading plugin command cache at ${commandCachePath}`) + console.error(error) + } + } + // Built in commands must be in sync with the framework code + pluginCommandCache._builtin = PLUGIN_CACHE_BUILTIN + return pluginCommandCache +} + +export function saveCommandCache(pluginCommandCache) { + const commandCachePath = path.join( + getPaths().generated.base, + PLUGIN_CACHE_FILENAME + ) + try { + fs.writeFileSync( + commandCachePath, + JSON.stringify(pluginCommandCache, undefined, 2) + ) + } catch (error) { + console.error(`Error saving plugin command cache at ${commandCachePath}`) + console.error(error) + } +} + +/** + * Logs warnings for any plugins that have invalid definitions in the redwood.toml file + * + * @param {any[]} plugins An array of plugin objects read from the redwood.toml file + */ +export function checkPluginListAndWarn(plugins) { + // Plugins must define a package + for (const plugin of plugins) { + if (!plugin.package) { + console.warn( + chalk.yellow(`⚠️ A plugin is missing a package, it cannot be loaded.`) + ) + } + } + + // Plugins should only occur once in the list + const pluginPackages = plugins + .map((p) => p.package) + .filter((p) => p !== undefined) + if (pluginPackages.length !== new Set(pluginPackages).size) { + console.warn( + chalk.yellow( + '⚠️ Duplicate plugin packages found in redwood.toml, duplicates will be ignored.' + ) + ) + } + + // Plugins should be published to npm under a scope which is used as the namespace + const namespaces = plugins.map((p) => p.package?.split('/')[0]) + namespaces.forEach((ns) => { + if (ns !== undefined && !ns.startsWith('@')) { + console.warn( + chalk.yellow( + `⚠️ Plugin "${ns}" is missing a scope/namespace, it will not be loaded.` + ) + ) + } + }) +} + +/** + * Attempts to load a plugin package and return it. Returns null if the plugin failed to load. + * + * @param {string} packageName The npm package name of the plugin + * @param {string | undefined} packageVersion The npm package version of the plugin, defaults to loading the plugin at the + * same version as the cli + * @param {boolean} autoInstall Whether to automatically install the plugin package if it is not installed already + * @returns The plugin package or null if it failed to load + */ +export async function loadPluginPackage( + packageName, + packageVersion, + autoInstall +) { + // NOTE: This likely does not handle mismatch versions between what is installed and what is requested + if (isModuleInstalled(packageName)) { + return await import(packageName) + } + + if (!autoInstall) { + console.warn( + chalk.yellow( + `⚠️ Plugin "${packageName}" cannot be loaded because it is not installed and "autoInstall" is disabled.` + ) + ) + return null + } + + // Attempt to install the plugin + console.log(chalk.green(`Installing plugin "${packageName}"...`)) + const installed = await installPluginPackage(packageName, packageVersion) + if (installed) { + return await import(packageName) + } + return null +} + +/** + * Attempts to install a plugin package. Installs the package as a dev dependency. + * + * @param {string} packageName The npm package name of the plugin + * @param {string} packageVersion The npm package version of the plugin to install or undefined + * to install the same version as the cli + * @returns True if the plugin was installed successfully, false otherwise + */ +async function installPluginPackage(packageName, packageVersion) { + // We use a simple heuristic here to try and be a little more convienient for the user + // when no version is specified. + + let versionToInstall = packageVersion + const isRedwoodPackage = packageName.startsWith('@redwoodjs/') + if (!isRedwoodPackage && versionToInstall === undefined) { + versionToInstall = 'latest' + try { + const compatibilityData = await getCompatibilityData( + packageName, + versionToInstall + ) + versionToInstall = compatibilityData.compatible.version + console.log( + chalk.green( + `Installing the latest compatible version: ${versionToInstall}` + ) + ) + } catch (error) { + console.log( + 'The following error occurred while checking plugin compatibility for automatic installation:' + ) + const errorMessage = error.message ?? error + console.log(errorMessage) + + // Exit without a chance to continue if it makes sense to do so + if ( + errorMessage.includes('does not have a tag') || + errorMessage.includes('does not have a version') + ) { + process.exit(1) + } + + const prompt = new Select({ + name: 'versionDecision', + message: 'What would you like to do?', + choices: [ + { + name: 'cancel', + message: 'Cancel', + }, + { + name: 'continue', + message: "Continue and install the 'latest' version", + }, + ], + }) + const decision = await prompt.run() + if (decision === 'cancel') { + process.exit(1) + } + } + } + + try { + // Note that installModule does the cli version matching for us if versionToInstall is undefined + await installModule(packageName, versionToInstall) + return true + } catch (error) { + console.error(error) + return false + } +} diff --git a/packages/cli/src/plugin.js b/packages/cli/src/plugin.js index dccd94be4458..6860507dd7e6 100644 --- a/packages/cli/src/plugin.js +++ b/packages/cli/src/plugin.js @@ -1,62 +1,10 @@ -import fs from 'fs' -import path from 'path' - -import chalk from 'chalk' - -import { getConfig, getPaths } from './lib' -import { installModule, isModuleInstalled } from './lib/packages' - -/** - * The file inside .redwood which will contain cached plugin command mappings - */ -const PLUGIN_CACHE_FILENAME = 'commandCache.json' - -const PLUGIN_CACHE_DEFAULT = { - '@redwoodjs/cli-storybook': { - storybook: { - aliases: ['sb'], - description: - 'Launch Storybook: a tool for building UI components and pages in isolation', - }, - }, - '@redwoodjs/cli-data-migrate': { - 'data-migrate': { - aliases: ['dataMigrate', 'dm'], - description: 'Migrate the data in your database', - }, - }, -} - -const PLUGIN_CACHE_BUILTIN = [ - 'build', - 'check', - 'diagnostics', - 'console', - 'c', - 'deploy', - 'destroy', - 'd', - 'dev', - 'exec', - 'experimental', - 'exp', - 'generate', - 'g', - 'info', - 'lint', - 'prerender', - 'render', - 'prisma', - 'record', - 'serve', - 'setup', - 'test', - 'ts-to-js', - 'type-check', - 'tsc', - 'tc', - 'upgrade', -] +import { getConfig } from './lib' +import { + loadCommadCache, + checkPluginListAndWarn, + saveCommandCache, + loadPluginPackage, +} from './lib/plugin' /** * Attempts to load all CLI plugins as defined in the redwood.toml file @@ -65,63 +13,46 @@ const PLUGIN_CACHE_BUILTIN = [ * @returns The yargs instance with plugins loaded */ export async function loadPlugins(yargs) { - // We filter plugins based on the first word which depends on if a namespace is in use - const firstWord = process.argv[2]?.startsWith('@') - ? process.argv[3] - : process.argv[2] + // Extract some useful information from the command line args + const namespaceIsExplicit = process.argv[2]?.startsWith('@') + const namespaceInUse = + (namespaceIsExplicit ? process.argv[2] : '@redwoodjs') ?? '@redwoodjs' + const commandString = namespaceIsExplicit + ? process.argv.slice(3).join(' ') + : process.argv.slice(2).join(' ') + const commandFirstWord = commandString.split(' ')[0] // Check for possible early exit for `yarn rw --version` - const showRootVersion = firstWord === '--version' - if (showRootVersion) { + if (commandFirstWord === '--version' && namespaceInUse === '@redwoodjs') { // We don't need to load any plugins in this case return yargs } // TODO: We should have some mechanism to fetch the cache from an online or precomputed // source this will allow us to have a cache hit on the first run of a command - let pluginCommandCache = PLUGIN_CACHE_DEFAULT - try { - const localCommandCache = JSON.parse( - fs.readFileSync( - path.join(getPaths().generated.base, PLUGIN_CACHE_FILENAME) - ) - ) - let valid = true - for (const [key, value] of Object.entries(localCommandCache)) { - if (key === '_builtin') { - continue - } - valid &&= !Array.isArray(value) - } - if (valid) { - pluginCommandCache = { - ...localCommandCache, - ...PLUGIN_CACHE_DEFAULT, - } - } - } catch (error) { - // If the cache file doesn't exist we can just ignore it and continue - if (error.code !== 'ENOENT') { - console.error(error) - } - } - pluginCommandCache._builtin = PLUGIN_CACHE_BUILTIN + const pluginCommandCache = loadCommadCache() // Check if the command is built in to the base CLI package - if (pluginCommandCache._builtin.includes(firstWord)) { + if ( + pluginCommandCache._builtin.includes(commandFirstWord) && + namespaceInUse === '@redwoodjs' + ) { // If the command is built in we don't need to load any plugins return yargs } + // The TOML is the source of truth for plugins const { plugins, autoInstall } = getConfig().experimental.cli + // Plugins are enabled unless explicitly disabled const enabledPlugins = plugins.filter( (p) => p.package !== undefined && (p.enabled ?? true) ) - // Print warnings about invalid plugins + // Print warnings about any invalid plugins checkPluginListAndWarn(enabledPlugins) + // Extract some useful information from the enabled plugins const redwoodPackages = new Set() const thirdPartyPackages = new Set() for (const plugin of enabledPlugins) { @@ -140,7 +71,7 @@ export async function loadPlugins(yargs) { } } - // Order alphabetically but with @redwoodjs namespace first + // Order alphabetically but with @redwoodjs namespace first, orders the help output const namespaces = Array.from( thirdPartyPackages.map((p) => p.split('/')[0]) ).sort() @@ -148,234 +79,238 @@ export async function loadPlugins(yargs) { namespaces.unshift('@redwoodjs') } - // If the user is running a help command or no command was given - // we want to load all plugins for observability in the help output - const processArgv = process.argv.slice(2).join(' ') - const showRootHelp = - processArgv === '--help' || processArgv === '-h' || processArgv === '' - - // Filter the namespaces based on the command line args to - // reduce the number of plugins we need to load - const namespacesInUse = namespaces.filter( - (ns) => showRootHelp || processArgv.includes(ns) - ) - if (namespacesInUse.length === 0) { - // If no namespace is in use we're using the default @redwoodjs namespace which - // is just an empty string '' - namespacesInUse.push('@redwoodjs') - } - - for (const namespace of namespacesInUse) { - // Get all the plugins for this namespace - const namespacePlugins = new Set( - enabledPlugins.filter((p) => p.package.startsWith(namespace)) - ) - // Do nothing if there are no enabled plugins for this namespace - if (namespacePlugins.size === 0) { - continue - } - - const namespacePluginsToLoad = [] - - // Attempt to find a plugin that matches the first word - for (const namespacePlugin of namespacePlugins) { - const cacheEntry = pluginCommandCache[namespacePlugin.package] - if (cacheEntry === undefined) { - continue - } - const commands = Object.keys(cacheEntry) - const allTriggers = commands.flatMap((c) => [ - c, - ...(cacheEntry[c].aliases ?? []), - ]) - if (allTriggers.includes(firstWord)) { - namespacePluginsToLoad.push(namespacePlugin) - // Only one plugin can match the first word so we break here - break - } - } - - // For help output we only show the root level commands which for third - // party plugins is just the namespace. No need to load the plugin for this. - if (showRootHelp || namespacePluginsToLoad.length === 0) { - if (namespace !== '@redwoodjs') { + // There are cases where we can avoid loading the plugins if they are in the cache + // this includes when we are showing help output at the root level + const showingHelpAtRootLevel = + !namespaceIsExplicit && + (commandFirstWord === '--help' || + commandFirstWord === '-h' || + commandFirstWord === '') + + // We also need the same logic for when an unknown namespace is used + const namespaceIsUnknown = !namespaces.includes(namespaceInUse) + + if (showingHelpAtRootLevel || namespaceIsUnknown) { + // In this case we wish to show all available redwoodjs commands and all the + // third party namespaces available + for (const namespace of namespaces) { + if (namespace === '@redwoodjs') { + for (const redwoodPluginPackage of redwoodPackages) { + // We'll load the plugin information from the cache if there is a cache entry + const commands = await loadCommandsFromCacheOrPackage( + redwoodPluginPackage, + pluginCommandCache, + autoInstall, + true + ) + yargs.command(commands) + } + } else { + // We only need to show that the namespace exists, users can then run + // `yarn rw @namespace` to see the commands available in that namespace yargs.command({ command: `${namespace} `, - describe: `${namespace} plugin commands`, + describe: `Commands from ${namespace}`, builder: () => {}, handler: () => {}, }) - } else { - // For the @redwoodjs namespace we want to show all the commands for each package - for (const namespacePlugin of namespacePlugins) { - // We get the details from the cache so we don't have to install/load the plugin package - const cacheEntry = pluginCommandCache[namespacePlugin.package] - if (cacheEntry === undefined) { - // if we have the default cache entry set properly we should never end up here - continue - } - const commands = Object.keys(cacheEntry) - for (const command of commands) { - yargs.command({ - command, - describe: cacheEntry[command].description, - aliases: cacheEntry[command].aliases, - builder: () => {}, - handler: () => {}, - }) - } - } } - continue } - // Load plugins for this namespace - const namespaceCommands = [] - for (const namespacePlugin of namespacePluginsToLoad) { - // Attempt to load the plugin - const plugin = await loadPluginPackage( - namespacePlugin.package, - namespacePlugin.version, - autoInstall - ) + // Update the cache with any new information we have + saveCommandCache(pluginCommandCache) - // Show an error if the plugin failed to load - if (!plugin) { - console.error( - chalk.red(`❌ Plugin "${namespacePlugin.package}" failed to load.`) + return yargs + } + + const showingHelpAtNamespaceLevel = + namespaceIsExplicit && + (commandFirstWord === '--help' || + commandFirstWord === '-h' || + commandFirstWord === '') + if (showingHelpAtNamespaceLevel) { + // In this case we wish to show all available commands for the particular namespace + if (namespaceInUse === '@redwoodjs') { + for (const redwoodPluginPackage of redwoodPackages) { + // We'll load the plugin information from the cache if there is a cache entry + const commands = await loadCommandsFromCacheOrPackage( + redwoodPluginPackage, + pluginCommandCache, + autoInstall, + true ) - continue + yargs.command(commands) } - - // Add the plugin to the cache entry - pluginCommandCache[namespacePlugin.package] = {} - for (const command of plugin.commands) { - pluginCommandCache[namespacePlugin.package][command.command] = { - aliases: command.aliases, - description: command.description, - } + } else { + const packagesForNamespace = Array.from(thirdPartyPackages).filter((p) => + p.startsWith(namespaceInUse) + ) + for (const packageForNamespace of packagesForNamespace) { + // We'll load the plugin information from the cache if there is a cache entry + const commands = await loadCommandsFromCacheOrPackage( + packageForNamespace, + pluginCommandCache, + autoInstall, + true + ) + yargs.command({ + command: `${namespaceInUse} `, + describe: `Commands from ${namespaceInUse}`, + builder: (yargs) => { + yargs.command(commands).demandCommand() + }, + handler: () => {}, + }) } - - // Add these commands to the namespace list - namespaceCommands.push(...plugin.commands) } - // Register all commands we loaded for this namespace - // If the namespace is @redwoodjs, we don't need to nest the commands under a namespace - if (namespace === '@redwoodjs') { - yargs.command(namespaceCommands).demandCommand() - } else { - yargs.command({ - command: `${namespace} `, - describe: `${namespace} plugin commands`, - builder: (yargs) => { - yargs.command(namespaceCommands).demandCommand() - }, - handler: () => {}, - }) - } - } + // Update the cache with any new information we have + saveCommandCache(pluginCommandCache) - // Cache the plugin-command mapping to optimise loading on the next invocation - try { - fs.writeFileSync( - path.join(getPaths().generated.base, PLUGIN_CACHE_FILENAME), - JSON.stringify(pluginCommandCache, undefined, 2) - ) - } catch (error) { - console.error(error) + return yargs } - return yargs -} - -/** - * Logs warnings for any plugins that have invalid definitions in the redwood.toml file - * - * @param {any[]} plugins An array of plugin objects read from the redwood.toml file - */ -function checkPluginListAndWarn(plugins) { - // Plugins must define a package - for (const plugin of plugins) { - if (!plugin.package) { - console.warn( - chalk.yellow(`⚠️ A plugin is missing a package, it cannot be loaded.`) + // At this point we know that: + // - The command is not built in + // - The namespace is known + // - We're not asking for help at the root level + // - We're not asking for help at the namespace level + // Now we need to try to cull based on the namespace and the specific command + + // Try to find the package for this command from the cache + const packagesToLoad = new Set() + for (const [packageName, cacheEntry] of Object.entries(pluginCommandCache)) { + if (packageName === '_builtin') { + continue + } + const commandFirstWords = [] + for (const [command, info] of Object.entries(cacheEntry)) { + commandFirstWords.push(command.split(' ')[0]) + commandFirstWords.push( + ...(info.aliases?.map((a) => a.split(' ')[0]) ?? []) ) } + if ( + commandFirstWords.includes(commandFirstWord) && + packageName.startsWith(namespaceInUse) + ) { + packagesToLoad.add(packageName) + break + } } - // Plugins should only occur once in the list - const pluginPackages = plugins - .map((p) => p.package) - .filter((p) => p !== undefined) - if (pluginPackages.length !== new Set(pluginPackages).size) { - console.warn( - chalk.yellow( - '⚠️ Duplicate plugin packages found in redwood.toml, duplicates will be ignored.' - ) - ) + // If we didn't find the package in the cache we'll have to load all + // of them, for help output essentially + const foundMatchingPackage = packagesToLoad.size > 0 + if (!foundMatchingPackage) { + for (const plugin of enabledPlugins) { + if (plugin.package.startsWith(namespaceInUse)) { + packagesToLoad.add(plugin.package) + } + } } - // Plugins should be published to npm under a scope which is used as the namespace - const namespaces = plugins.map((p) => p.package?.split('/')[0]) - namespaces.forEach((ns) => { - if (ns !== undefined && !ns.startsWith('@')) { - console.warn( - chalk.yellow( - `⚠️ Plugin "${ns}" is missing a scope/namespace, it will not be loaded.` - ) + const commandsToRegister = [] + // If we nailed down the package to load we can go ahead and load it now + if (foundMatchingPackage) { + // We'll have to load the plugin package since we may need to actually execute + // the command builder/handler functions + const packageToLoad = packagesToLoad.values().next().value + const commands = await loadCommandsFromCacheOrPackage( + packageToLoad, + pluginCommandCache, + autoInstall, + false + ) + commandsToRegister.push(...commands) + } else { + // It's safe to try and load the plugin information from the cache since any + // that are present in the cache didn't match the command we're trying to run + // so they'll never be executed and will only be used for help output + for (const packageToLoad of packagesToLoad) { + const commands = await loadCommandsFromCacheOrPackage( + packageToLoad, + pluginCommandCache, + autoInstall, + true ) + commandsToRegister.push(...commands) } - }) -} - -/** - * Attempts to load a plugin package and return it. Returns null if the plugin failed to load. - * - * @param {string} packageName The npm package name of the plugin - * @param {string | undefined} packageVersion The npm package version of the plugin, defaults to loading the plugin at the - * same version as the cli - * @param {boolean} autoInstall Whether to automatically install the plugin package if it is not installed already - * @returns The plugin package or null if it failed to load - */ -async function loadPluginPackage(packageName, packageVersion, autoInstall) { - // NOTE: This likely does not handle mismatch versions between what is installed and what is requested - if (isModuleInstalled(packageName)) { - return await import(packageName) } - if (!autoInstall) { - console.warn( - chalk.yellow( - `⚠️ Plugin "${packageName}" cannot be loaded because it is not installed and "autoInstall" is disabled.` - ) - ) - return null + // We need to nest the commands under the namespace rememebering that the + // @redwoodjs namespace is special and doesn't need to be nested + if (namespaceInUse === '@redwoodjs') { + yargs.command(commandsToRegister) + } else { + yargs.command({ + command: `${namespaceInUse} `, + describe: `Commands from ${namespaceInUse}`, + builder: (yargs) => { + yargs.command(commandsToRegister).demandCommand() + }, + handler: () => {}, + }) } - // Attempt to install the plugin - console.log(chalk.green(`Installing plugin "${packageName}"...`)) - const installed = await installPluginPackage(packageName, packageVersion) - if (installed) { - return await import(packageName) + // For consistency in the help output at the root level we'll register + // the namespace stubs if they didn't explicitly use a namespace + if (!namespaceIsExplicit) { + for (const namespace of namespaces) { + if (namespace === '@redwoodjs') { + continue + } + yargs.command({ + command: `${namespace} `, + describe: `Commands from ${namespace}`, + builder: () => {}, + handler: () => {}, + }) + } } - return null + + // Update the cache with any new information we have + saveCommandCache(pluginCommandCache) + + return yargs } -/** - * Attempts to install a plugin package. Installs the package as a dev dependency. - * - * @param {string} packageName The npm package name of the plugin - * @param {string} packageVersion The npm package version of the plugin to install or undefined - * to install the same version as the cli - * @returns True if the plugin was installed successfully, false otherwise - */ -async function installPluginPackage(packageName, packageVersion) { - try { - await installModule(packageName, packageVersion) - return true - } catch (error) { - console.error(error) - return false +async function loadCommandsFromCacheOrPackage( + packageName, + cache, + autoInstall, + readFromCache +) { + let cacheEntry = undefined + if (readFromCache) { + cacheEntry = cache !== undefined ? cache[packageName] : undefined + } + if (cacheEntry !== undefined) { + const commands = Object.entries(cacheEntry).map(([command, info]) => { + return { + command, + describe: info.description, + aliases: info.aliases, + } + }) + return commands + } + + // We'll have to load the plugin package to get the command information + const plugin = await loadPluginPackage(packageName, undefined, autoInstall) + if (plugin) { + const commands = plugin.commands ?? [] + const cacheUpdate = {} + for (const command of commands) { + cacheUpdate[command.command] = { + aliases: command.aliases, + description: command.description, + } + } + cache[packageName] = cacheUpdate + return commands } + + // NOTE: If the plugin failed to load there should have been a warning printed + return [] } diff --git a/packages/project-config/src/config.ts b/packages/project-config/src/config.ts index 27f4bd79b05a..d975215ff972 100644 --- a/packages/project-config/src/config.ts +++ b/packages/project-config/src/config.ts @@ -115,7 +115,6 @@ export interface Config { export interface CLIPlugin { package: string - version?: string enabled?: boolean }