diff --git a/package.json b/package.json index fa7754971..2199d6fd0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "js-yaml": "^3.14.1", + "minimatch": "^9.0.3", "natural-orderby": "^2.0.3", "object-treeify": "^1.1.33", "password-prompt": "^1.1.3", diff --git a/src/config/config.ts b/src/config/config.ts index 43988b92d..53943cac3 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -381,6 +381,7 @@ export class Config implements IConfig { dataDir: this.dataDir, devPlugins: this.options.devPlugins, force: opts?.force ?? false, + pluginAdditions: this.options.pluginAdditions, rootPlugin: this.rootPlugin, userPlugins: this.options.userPlugins, }) diff --git a/src/config/plugin-loader.ts b/src/config/plugin-loader.ts index ea466387d..f54fc0468 100644 --- a/src/config/plugin-loader.ts +++ b/src/config/plugin-loader.ts @@ -1,3 +1,4 @@ +import {minimatch} from 'minimatch' import {join} from 'node:path' import {PJSON} from '../interfaces' @@ -22,10 +23,19 @@ type LoadOpts = { force?: boolean rootPlugin: IPlugin userPlugins?: boolean + pluginAdditions?: { + core?: string[] + dev?: string[] + path?: string + } } type PluginsMap = Map +function findMatchingDependencies(dependencies: Record, patterns: string[]): string[] { + return Object.keys(dependencies).filter((p) => patterns.some((w) => minimatch(p, w))) +} + export default class PluginLoader { public errors: (Error | string)[] = [] public plugins: PluginsMap = new Map() @@ -73,8 +83,23 @@ export default class PluginLoader { } private async loadCorePlugins(opts: LoadOpts): Promise { - if (opts.rootPlugin.pjson.oclif.plugins) { - await this.loadPlugins(opts.rootPlugin.root, 'core', opts.rootPlugin.pjson.oclif.plugins) + const {plugins: corePlugins} = opts.rootPlugin.pjson.oclif + if (corePlugins) { + const plugins = findMatchingDependencies(opts.rootPlugin.pjson.dependencies ?? {}, corePlugins) + await this.loadPlugins(opts.rootPlugin.root, 'core', plugins) + } + + const {core: pluginAdditionsCore, path} = opts.pluginAdditions ?? {core: []} + if (pluginAdditionsCore) { + if (path) { + // If path is provided, load plugins from the path + const pjson = await readJson(join(path, 'package.json')) + const plugins = findMatchingDependencies(pjson.dependencies ?? {}, pluginAdditionsCore) + await this.loadPlugins(path, 'core', plugins) + } else { + const plugins = findMatchingDependencies(opts.rootPlugin.pjson.dependencies ?? {}, pluginAdditionsCore) + await this.loadPlugins(opts.rootPlugin.root, 'core', plugins) + } } } @@ -84,7 +109,26 @@ export default class PluginLoader { if (isProd()) return try { const {devPlugins} = opts.rootPlugin.pjson.oclif - if (devPlugins) await this.loadPlugins(opts.rootPlugin.root, 'dev', devPlugins) + if (devPlugins) { + const allDeps = {...opts.rootPlugin.pjson.dependencies, ...opts.rootPlugin.pjson.devDependencies} + const plugins = findMatchingDependencies(allDeps ?? {}, devPlugins) + await this.loadPlugins(opts.rootPlugin.root, 'dev', plugins) + } + + const {dev: pluginAdditionsDev, path} = opts.pluginAdditions ?? {core: []} + if (pluginAdditionsDev) { + if (path) { + // If path is provided, load plugins from the path + const pjson = await readJson(join(path, 'package.json')) + const allDeps = {...pjson.dependencies, ...pjson.devDependencies} + const plugins = findMatchingDependencies(allDeps ?? {}, pluginAdditionsDev) + await this.loadPlugins(path, 'dev', plugins) + } else { + const allDeps = {...opts.rootPlugin.pjson.dependencies, ...opts.rootPlugin.pjson.devDependencies} + const plugins = findMatchingDependencies(allDeps ?? {}, pluginAdditionsDev) + await this.loadPlugins(opts.rootPlugin.root, 'dev', plugins) + } + } } catch (error: any) { process.emitWarning(error) } @@ -140,7 +184,14 @@ export default class PluginLoader { parent.children.push(instance) } - await this.loadPlugins(instance.root, type, instance.pjson.oclif.plugins || [], instance) + if (instance.pjson.oclif.plugins) { + const allDeps = + type === 'dev' + ? {...instance.pjson.dependencies, ...instance.pjson.devDependencies} + : instance.pjson.dependencies + const plugins = findMatchingDependencies(allDeps ?? {}, instance.pjson.oclif.plugins) + await this.loadPlugins(instance.root, type, plugins, instance) + } } catch (error: any) { this.errors.push(error) } diff --git a/src/interfaces/plugin.ts b/src/interfaces/plugin.ts index 28945e1a6..a263b2aa6 100644 --- a/src/interfaces/plugin.ts +++ b/src/interfaces/plugin.ts @@ -22,6 +22,11 @@ export interface Options extends PluginOptions { devPlugins?: boolean enablePerf?: boolean jitPlugins?: boolean + pluginAdditions?: { + core?: string[] + dev?: string[] + path?: string + } plugins?: Map userPlugins?: boolean version?: string diff --git a/test/config/fixtures/wildcard/package.json b/test/config/fixtures/wildcard/package.json new file mode 100644 index 000000000..494cc6f88 --- /dev/null +++ b/test/config/fixtures/wildcard/package.json @@ -0,0 +1,19 @@ +{ + "name": "wildcard-plugins-fixture", + "version": "0.0.0", + "description": "fixture for testing wildcard plugins", + "private": true, + "files": [], + "dependencies": { + "@oclif/core": "^3", + "@oclif/plugin-help": "^6", + "@oclif/plugin-plugins": "^4" + }, + "oclif": { + "commands": "./lib/commands", + "topicSeparator": " ", + "plugins": [ + "@oclif/plugin-*" + ] + } +} diff --git a/test/config/fixtures/wildcard/src/commands/foo.ts b/test/config/fixtures/wildcard/src/commands/foo.ts new file mode 100644 index 000000000..111b558da --- /dev/null +++ b/test/config/fixtures/wildcard/src/commands/foo.ts @@ -0,0 +1,8 @@ +import {Command} from '../../../../../../src/index' + +export default class Foo extends Command { + public static description = 'foo description' + public async run(): Promise { + this.log('hello world!') + } +} diff --git a/test/config/fixtures/wildcard/tsconfig.json b/test/config/fixtures/wildcard/tsconfig.json new file mode 100644 index 000000000..8d0083147 --- /dev/null +++ b/test/config/fixtures/wildcard/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "outDir": "./lib", + "rootDirs": ["./src"] + }, + "include": ["./src/**/*"] +} diff --git a/test/config/wildcard-plugins.test.ts b/test/config/wildcard-plugins.test.ts new file mode 100644 index 000000000..f381d9285 --- /dev/null +++ b/test/config/wildcard-plugins.test.ts @@ -0,0 +1,56 @@ +import {expect} from 'chai' +import {resolve} from 'node:path' +import {SinonSandbox, createSandbox} from 'sinon' + +import {run, ux} from '../../src/index' + +describe('plugins defined as patterns in package.json', () => { + let sandbox: SinonSandbox + + beforeEach(() => { + sandbox = createSandbox() + sandbox.stub(ux.write, 'stdout') + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should load all core plugins in dependencies that match pattern', async () => { + const result = (await run(['plugins', '--core'], { + root: resolve(__dirname, 'fixtures/wildcard/package.json'), + pluginAdditions: { + core: ['@oclif/plugin-*'], + path: resolve(__dirname, '..', '..'), + }, + })) as Array<{name: string; type: string}> + + expect(result.length).to.equal(3) + const rootPlugin = result.find((r) => r.name === 'wildcard-plugins-fixture') + const pluginHelp = result.find((r) => r.name === '@oclif/plugin-help') + const pluginPlugins = result.find((r) => r.name === '@oclif/plugin-plugins') + + expect(rootPlugin).to.exist + expect(pluginHelp).to.exist + expect(pluginPlugins).to.exist + }) + + it('should load all dev plugins in dependencies and devDependencies that match pattern', async () => { + const result = (await run(['plugins', '--core'], { + root: resolve(__dirname, 'fixtures/wildcard/package.json'), + pluginAdditions: { + dev: ['@oclif/plugin-*'], + path: resolve(__dirname, '..', '..'), + }, + })) as Array<{name: string; type: string}> + + expect(result.length).to.equal(3) + const rootPlugin = result.find((r) => r.name === 'wildcard-plugins-fixture') + const pluginHelp = result.find((r) => r.name === '@oclif/plugin-help') + const pluginPlugins = result.find((r) => r.name === '@oclif/plugin-plugins') + + expect(rootPlugin).to.exist + expect(pluginHelp).to.exist + expect(pluginPlugins).to.exist + }) +})