Skip to content

Commit eff87af

Browse files
authored
Add globalConfigSourceRegistry to match middleware pattern (#206)
* Add globalConfigSourceRegistry to match middleware registry pattern Add a global ConfigSourceRegistry singleton that resolveConfig() auto-includes, matching the existing globalMiddlewareRegistry and globalAuthMiddlewareRegistry patterns. This eliminates the need to manually pass plugin config sources to every resolveConfig()/loadConfig() call. Changes: - New ConfigSourceRegistry class with register/unregister/getSources/clear - resolveConfig() auto-includes globalConfigSourceRegistry sources - B2CPluginManager.applyMiddleware() registers config sources globally - Simplified BaseCommand: removed getPluginSources() and pluginSources properties - Simplified 6 loadConfig() callsites across CLI commands - Deprecated PluginSources type in cli/config.ts - VS Code extension: removed resolveConfigWithPlugins() wrapper - Fixes MCP server bug where plugin config sources were silently dropped * Isolate tests from developer environment (plugin hooks and global registries) Add B2C_SKIP_PLUGIN_HOOKS env var guard to all three collectPlugin* methods in BaseCommand to prevent real plugins (keychain, password-store, etc.) from running during tests. Add mocha root hooks to CLI, MCP, and SDK packages that set this env var and clear global registries before each test. This fixes a pre-existing test isolation issue where locally installed plugins could pollute test state, made visible by the globalConfigSourceRegistry addition (plugin config sources accumulated O(N^2) across N tests). * Add changeset for globalConfigSourceRegistry and test isolation
1 parent f82eaaf commit eff87af

File tree

23 files changed

+482
-101
lines changed

23 files changed

+482
-101
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@salesforce/b2c-tooling-sdk': patch
3+
'@salesforce/b2c-cli': patch
4+
'@salesforce/b2c-dx-mcp': patch
5+
---
6+
7+
Add `globalConfigSourceRegistry` for automatic plugin config source inclusion in `resolveConfig()`, matching the existing middleware registry pattern. Plugin config sources are now picked up automatically by all SDK consumers without manual plumbing. Also improves test isolation by preventing locally installed plugins from affecting test runs.

packages/b2c-cli/.mocharc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"recursive": true,
66
"reporter": "spec",
77
"timeout": 60000,
8+
"require": ["test/setup.ts"],
89
"node-option": [
910
"import=tsx",
1011
"conditions=development"

packages/b2c-cli/src/utils/cip/command.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ export abstract class CipCommand<T extends typeof Command> extends OAuthCommand<
102102
cipHost: flags['cip-host'] as string | undefined,
103103
},
104104
this.getBaseConfigOptions(),
105-
this.getPluginSources(),
106105
);
107106
}
108107

packages/b2c-cli/test/setup.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
/**
8+
* Global test setup for CLI tests.
9+
*
10+
* - Skips plugin hook collection so real plugins installed on the developer's
11+
* machine (keychain, password-store, etc.) don't pollute the test environment
12+
* - Clears all SDK global registries before each test to prevent state leakage
13+
*/
14+
import {globalMiddlewareRegistry} from '@salesforce/b2c-tooling-sdk/clients';
15+
import {globalAuthMiddlewareRegistry} from '@salesforce/b2c-tooling-sdk/auth';
16+
import {globalConfigSourceRegistry} from '@salesforce/b2c-tooling-sdk/config';
17+
18+
// Prevent BaseCommand from running plugin hooks during tests
19+
process.env.B2C_SKIP_PLUGIN_HOOKS = '1';
20+
21+
export const mochaHooks = {
22+
beforeEach() {
23+
globalMiddlewareRegistry.clear();
24+
globalAuthMiddlewareRegistry.clear();
25+
globalConfigSourceRegistry.clear();
26+
},
27+
};

packages/b2c-dx-mcp/.mocharc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"recursive": true,
66
"reporter": "spec",
77
"timeout": 60000,
8+
"require": ["test/setup.ts"],
89
"node-option": [
910
"import=tsx",
1011
"conditions=development"

packages/b2c-dx-mcp/test/setup.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
/**
8+
* Global test setup for MCP tests.
9+
*
10+
* - Skips plugin hook collection so real plugins installed on the developer's
11+
* machine don't pollute the test environment
12+
* - Clears all SDK global registries before each test to prevent state leakage
13+
*/
14+
import {globalMiddlewareRegistry} from '@salesforce/b2c-tooling-sdk/clients';
15+
import {globalAuthMiddlewareRegistry} from '@salesforce/b2c-tooling-sdk/auth';
16+
import {globalConfigSourceRegistry} from '@salesforce/b2c-tooling-sdk/config';
17+
18+
// Prevent BaseCommand from running plugin hooks during tests
19+
process.env.B2C_SKIP_PLUGIN_HOOKS = '1';
20+
21+
export const mochaHooks = {
22+
beforeEach() {
23+
globalMiddlewareRegistry.clear();
24+
globalAuthMiddlewareRegistry.clear();
25+
globalConfigSourceRegistry.clear();
26+
},
27+
};

packages/b2c-tooling-sdk/src/cli/base-command.ts

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import {Command, Flags, type Interfaces} from '@oclif/core';
77
import {loadConfig} from './config.js';
8-
import type {LoadConfigOptions, PluginSources} from './config.js';
8+
import type {LoadConfigOptions} from './config.js';
99
import type {ResolvedB2CConfig} from '../config/index.js';
1010
import {parseFriendlySandboxId} from '../operations/ods/sandbox-lookup.js';
1111
import type {
@@ -19,7 +19,7 @@ import type {
1919
import {setLanguage} from '../i18n/index.js';
2020
import {configureLogger, getLogger, type LogLevel, type Logger} from '../logging/index.js';
2121
import {createExtraParamsMiddleware, type ExtraParamsConfig} from '../clients/middleware.js';
22-
import type {ConfigSource} from '../config/types.js';
22+
import {globalConfigSourceRegistry} from '../config/config-source-registry.js';
2323
import {globalMiddlewareRegistry} from '../clients/middleware-registry.js';
2424
import {globalAuthMiddlewareRegistry} from '../auth/middleware.js';
2525
import {setUserAgent} from '../clients/user-agent.js';
@@ -130,11 +130,6 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
130130
/** Telemetry instance for tracking command events */
131131
protected telemetry?: Telemetry;
132132

133-
/** High-priority config sources from plugins (inserted before defaults) */
134-
protected pluginSourcesBefore: ConfigSource[] = [];
135-
/** Low-priority config sources from plugins (inserted after defaults) */
136-
protected pluginSourcesAfter: ConfigSource[] = [];
137-
138133
/** Start time for command duration tracking */
139134
private commandStartTime?: number;
140135

@@ -312,7 +307,7 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
312307
* ...this.getBaseConfigOptions(),
313308
* // Add subclass-specific options here
314309
* };
315-
* return loadConfig(extractMyFlags(this.flags), options, this.getPluginSources());
310+
* return loadConfig(extractMyFlags(this.flags), options);
316311
* }
317312
* ```
318313
*/
@@ -325,18 +320,8 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
325320
};
326321
}
327322

328-
/**
329-
* Gets plugin sources for configuration resolution.
330-
*/
331-
protected getPluginSources(): PluginSources {
332-
return {
333-
before: this.pluginSourcesBefore,
334-
after: this.pluginSourcesAfter,
335-
};
336-
}
337-
338323
protected loadConfiguration(): ResolvedB2CConfig {
339-
return loadConfig({}, this.getBaseConfigOptions(), this.getPluginSources());
324+
return loadConfig({}, this.getBaseConfigOptions());
340325
}
341326

342327
/**
@@ -403,16 +388,17 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
403388
* but before configuration is resolved. It allows CLI plugins to provide
404389
* custom ConfigSource implementations.
405390
*
406-
* Plugin sources are collected into two arrays based on their priority:
407-
* - `pluginSourcesBefore`: High priority sources (override defaults)
408-
* - `pluginSourcesAfter`: Low priority sources (fill gaps)
391+
* Plugin sources are registered with the global config source registry
392+
* and automatically included in all subsequent `resolveConfig()` calls.
409393
*
410394
* Priority mapping:
411395
* - 'before' → -1 (higher priority than defaults)
412396
* - 'after' → 10 (lower priority than defaults)
413397
* - number → used directly
414398
*/
415399
protected async collectPluginConfigSources(): Promise<void> {
400+
if (process.env.B2C_SKIP_PLUGIN_HOOKS) return;
401+
416402
// Access flags that may be defined in subclasses (OAuthCommand, InstanceCommand)
417403
const flags = this.flags as Record<string, unknown>;
418404

@@ -429,7 +415,7 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
429415

430416
const hookResult = await this.config.runHook('b2c:config-sources', hookOptions);
431417

432-
// Collect sources from all plugins that responded, respecting priority
418+
// Collect sources from all plugins and register with global registry
433419
for (const success of hookResult.successes) {
434420
const result = success.result as ConfigSourcesHookResult | undefined;
435421
if (!result?.sources?.length) continue;
@@ -444,19 +430,12 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
444430
? result.priority
445431
: 10; // default 'after'
446432

447-
// Apply priority to sources that don't already have one set
433+
// Apply priority to sources that don't already have one set, then register globally
448434
for (const source of result.sources) {
449435
if (source.priority === undefined) {
450436
(source as {priority?: number}).priority = numericPriority;
451437
}
452-
}
453-
454-
// Still use before/after arrays for backwards compatibility
455-
// The resolver will sort all sources by priority anyway
456-
if (numericPriority < 0) {
457-
this.pluginSourcesBefore.push(...result.sources);
458-
} else {
459-
this.pluginSourcesAfter.push(...result.sources);
438+
globalConfigSourceRegistry.register(source);
460439
}
461440
}
462441

@@ -476,6 +455,8 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
476455
* Plugin middleware is registered with the global middleware registry.
477456
*/
478457
protected async collectPluginHttpMiddleware(): Promise<void> {
458+
if (process.env.B2C_SKIP_PLUGIN_HOOKS) return;
459+
479460
const hookOptions: HttpMiddlewareHookOptions = {
480461
flags: this.flags as Record<string, unknown>,
481462
};
@@ -509,6 +490,8 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
509490
* Plugin middleware is registered with the global auth middleware registry.
510491
*/
511492
protected async collectPluginAuthMiddleware(): Promise<void> {
493+
if (process.env.B2C_SKIP_PLUGIN_HOOKS) return;
494+
512495
const hookOptions: AuthMiddlewareHookOptions = {
513496
flags: this.flags as Record<string, unknown>,
514497
};

packages/b2c-tooling-sdk/src/cli/config.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export interface ExtractedMrtFlags {
146146
* ...this.getBaseConfigOptions(),
147147
* ...mrt.options,
148148
* };
149-
* return loadConfig(mrt.config, options, this.getPluginSources());
149+
* return loadConfig(mrt.config, options);
150150
* ```
151151
*/
152152
export function extractMrtFlags(flags: ParsedFlags): ExtractedMrtFlags {
@@ -189,8 +189,9 @@ export interface LoadConfigOptions {
189189
/**
190190
* Plugin-provided configuration sources with priority ordering.
191191
*
192-
* Used by BaseCommand to pass sources collected from the `b2c:config-sources` hook
193-
* to the configuration resolver.
192+
* @deprecated Plugin config sources are now registered with the global
193+
* {@link globalConfigSourceRegistry} and automatically included in
194+
* {@link resolveConfig}. This type is retained for backwards compatibility.
194195
*/
195196
export interface PluginSources {
196197
/**
@@ -215,7 +216,8 @@ export interface PluginSources {
215216
*
216217
* @param flags - Configuration values from CLI flags/env vars
217218
* @param options - Loading options
218-
* @param pluginSources - Optional sources from CLI plugins (via b2c:config-sources hook)
219+
* @param pluginSources - @deprecated Plugin sources are now registered globally via
220+
* {@link globalConfigSourceRegistry}. This parameter is retained for backwards compatibility.
219221
* @returns Resolved configuration with factory methods
220222
*
221223
* @example

packages/b2c-tooling-sdk/src/cli/instance-command.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,7 @@ export abstract class InstanceCommand<T extends typeof Command> extends OAuthCom
181181
}
182182

183183
protected override loadConfiguration(): ResolvedB2CConfig {
184-
return loadConfig(
185-
extractInstanceFlags(this.flags as Record<string, unknown>),
186-
this.getBaseConfigOptions(),
187-
this.getPluginSources(),
188-
);
184+
return loadConfig(extractInstanceFlags(this.flags as Record<string, unknown>), this.getBaseConfigOptions());
189185
}
190186

191187
/**

packages/b2c-tooling-sdk/src/cli/mrt-command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export abstract class MrtCommand<T extends typeof Command> extends BaseCommand<T
6969
...mrt.options,
7070
};
7171

72-
return loadConfig(mrt.config, options, this.getPluginSources());
72+
return loadConfig(mrt.config, options);
7373
}
7474

7575
/**

0 commit comments

Comments
 (0)