Skip to content

Commit 035cb59

Browse files
committed
change interface of config sources for clarity
1 parent c126990 commit 035cb59

File tree

10 files changed

+154
-114
lines changed

10 files changed

+154
-114
lines changed

packages/b2c-plugin-example-config/src/sources/env-file-source.ts

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
import {existsSync, readFileSync} from 'node:fs';
1313
import {join} from 'node:path';
14-
import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '@salesforce/b2c-tooling-sdk/config';
14+
import type {ConfigSource, ConfigLoadResult, ResolveConfigOptions} from '@salesforce/b2c-tooling-sdk/config';
1515

1616
/**
1717
* ConfigSource implementation that loads from .env.b2c files.
@@ -53,8 +53,6 @@ import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '@salesf
5353
export class EnvFileSource implements ConfigSource {
5454
readonly name = 'env-file (.env.b2c)';
5555

56-
private envFilePath?: string;
57-
5856
/**
5957
* Load configuration from .env.b2c file.
6058
*
@@ -64,48 +62,45 @@ export class EnvFileSource implements ConfigSource {
6462
* 3. .env.b2c in current working directory
6563
*
6664
* @param options - Resolution options (startDir used for file lookup)
67-
* @returns Parsed configuration or undefined if file not found
65+
* @returns Parsed configuration and location, or undefined if file not found
6866
*/
69-
load(options: ResolveConfigOptions): NormalizedConfig | undefined {
67+
load(options: ResolveConfigOptions): ConfigLoadResult | undefined {
7068
// Check for explicit path override via environment variable
7169
const envOverride = process.env.B2C_ENV_FILE_PATH;
70+
let envFilePath: string;
7271
if (envOverride) {
73-
this.envFilePath = envOverride;
72+
envFilePath = envOverride;
7473
} else {
7574
const searchDir = options.startDir ?? process.cwd();
76-
this.envFilePath = join(searchDir, '.env.b2c');
75+
envFilePath = join(searchDir, '.env.b2c');
7776
}
7877

79-
if (!existsSync(this.envFilePath)) {
78+
if (!existsSync(envFilePath)) {
8079
return undefined;
8180
}
8281

83-
const content = readFileSync(this.envFilePath, 'utf-8');
82+
const content = readFileSync(envFilePath, 'utf-8');
8483
const vars = this.parseEnvFile(content);
8584

8685
return {
87-
hostname: vars.HOSTNAME,
88-
webdavHostname: vars.WEBDAV_HOSTNAME,
89-
codeVersion: vars.CODE_VERSION,
90-
username: vars.USERNAME,
91-
password: vars.PASSWORD,
92-
clientId: vars.CLIENT_ID,
93-
clientSecret: vars.CLIENT_SECRET,
94-
scopes: vars.SCOPES ? vars.SCOPES.split(',').map((s) => s.trim()) : undefined,
95-
shortCode: vars.SHORT_CODE,
96-
mrtProject: vars.MRT_PROJECT,
97-
mrtEnvironment: vars.MRT_ENVIRONMENT,
98-
mrtApiKey: vars.MRT_API_KEY,
86+
config: {
87+
hostname: vars.HOSTNAME,
88+
webdavHostname: vars.WEBDAV_HOSTNAME,
89+
codeVersion: vars.CODE_VERSION,
90+
username: vars.USERNAME,
91+
password: vars.PASSWORD,
92+
clientId: vars.CLIENT_ID,
93+
clientSecret: vars.CLIENT_SECRET,
94+
scopes: vars.SCOPES ? vars.SCOPES.split(',').map((s) => s.trim()) : undefined,
95+
shortCode: vars.SHORT_CODE,
96+
mrtProject: vars.MRT_PROJECT,
97+
mrtEnvironment: vars.MRT_ENVIRONMENT,
98+
mrtApiKey: vars.MRT_API_KEY,
99+
},
100+
location: envFilePath,
99101
};
100102
}
101103

102-
/**
103-
* Get the path to the env file (for diagnostics).
104-
*/
105-
getPath(): string | undefined {
106-
return this.envFilePath;
107-
}
108-
109104
/**
110105
* Parse a .env file format into key-value pairs.
111106
*

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ export function loadConfig(
107107
// Log source summary
108108
for (const source of resolved.sources) {
109109
logger.trace(
110-
{source: source.name, path: source.path, fields: source.fieldsContributed},
110+
{
111+
source: source.name,
112+
location: source.location,
113+
fields: source.fields,
114+
fieldsIgnored: source.fieldsIgnored,
115+
},
111116
`[${source.name}] Contributed fields`,
112117
);
113118
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,16 @@
6666
* Implement the {@link ConfigSource} interface to create custom sources:
6767
*
6868
* ```typescript
69-
* import { ConfigResolver, type ConfigSource } from '@salesforce/b2c-tooling-sdk/config';
69+
* import { ConfigResolver, type ConfigSource, type ConfigLoadResult } from '@salesforce/b2c-tooling-sdk/config';
7070
*
7171
* class MySource implements ConfigSource {
7272
* name = 'my-source';
73-
* load(options) { return { hostname: 'custom.example.com' }; }
73+
* load(options): ConfigLoadResult | undefined {
74+
* return {
75+
* config: { hostname: 'custom.example.com' },
76+
* location: '/path/to/source',
77+
* };
78+
* }
7479
* }
7580
*
7681
* const resolver = new ConfigResolver([new MySource()]);
@@ -97,6 +102,7 @@ export {resolveConfig, ConfigResolver, createConfigResolver} from './resolver.js
97102
export type {
98103
NormalizedConfig,
99104
ConfigSource,
105+
ConfigLoadResult,
100106
ConfigSourceInfo,
101107
ConfigResolutionResult,
102108
ConfigWarning,

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,33 +157,46 @@ export class ConfigResolver {
157157
// Load from each source in order, merging results
158158
// Earlier sources have higher priority - later sources only fill in missing values
159159
for (const source of this.sources) {
160-
const sourceConfig = source.load(options);
161-
if (sourceConfig) {
162-
const fieldsContributed = getPopulatedFields(sourceConfig);
163-
if (fieldsContributed.length > 0) {
164-
sourceInfos.push({
165-
name: source.name,
166-
path: source.getPath?.(),
167-
fieldsContributed,
168-
});
169-
160+
const result = source.load(options);
161+
if (result) {
162+
const {config: sourceConfig, location} = result;
163+
const fields = getPopulatedFields(sourceConfig);
164+
if (fields.length > 0) {
170165
// Capture which credential groups are already claimed BEFORE processing this source
171166
// This allows a single source to provide complete credential pairs
172167
const claimedGroups = getClaimedCredentialGroups(baseConfig);
173168

169+
// Track which fields are ignored during merge
170+
const fieldsIgnored: (keyof NormalizedConfig)[] = [];
171+
174172
// Merge: source values fill in gaps (don't override existing values)
175173
for (const [key, value] of Object.entries(sourceConfig)) {
176174
if (value === undefined) continue;
177-
if (baseConfig[key as keyof NormalizedConfig] !== undefined) continue;
175+
176+
const fieldKey = key as keyof NormalizedConfig;
177+
178+
// Skip if already set by higher-priority source
179+
if (baseConfig[fieldKey] !== undefined) {
180+
fieldsIgnored.push(fieldKey);
181+
continue;
182+
}
178183

179184
// Skip if this field's credential group was already claimed by a higher-priority source
180185
// This prevents mixing credentials from different sources
181186
if (isFieldInClaimedGroup(key, claimedGroups)) {
187+
fieldsIgnored.push(fieldKey);
182188
continue;
183189
}
184190

185191
(baseConfig as Record<string, unknown>)[key] = value;
186192
}
193+
194+
sourceInfos.push({
195+
name: source.name,
196+
location,
197+
fields,
198+
fieldsIgnored: fieldsIgnored.length > 0 ? fieldsIgnored : undefined,
199+
});
187200
}
188201
}
189202
}

packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import {loadDwJson} from '../dw-json.js';
1212
import {getPopulatedFields} from '../mapping.js';
1313
import {mapDwJsonToNormalizedConfig} from '../mapping.js';
14-
import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '../types.js';
14+
import type {ConfigSource, ConfigLoadResult, ResolveConfigOptions} from '../types.js';
1515
import {getLogger} from '../../logging/logger.js';
1616

1717
/**
@@ -21,9 +21,8 @@ import {getLogger} from '../../logging/logger.js';
2121
*/
2222
export class DwJsonSource implements ConfigSource {
2323
readonly name = 'DwJsonSource';
24-
private lastPath?: string;
2524

26-
load(options: ResolveConfigOptions): NormalizedConfig | undefined {
25+
load(options: ResolveConfigOptions): ConfigLoadResult | undefined {
2726
const logger = getLogger();
2827

2928
const result = loadDwJson({
@@ -33,22 +32,14 @@ export class DwJsonSource implements ConfigSource {
3332
});
3433

3534
if (!result) {
36-
this.lastPath = undefined;
3735
return undefined;
3836
}
3937

40-
// Track the actual path from the loaded result
41-
this.lastPath = result.path;
38+
const config = mapDwJsonToNormalizedConfig(result.config);
39+
const fields = getPopulatedFields(config);
4240

43-
const normalized = mapDwJsonToNormalizedConfig(result.config);
44-
const fields = getPopulatedFields(normalized);
41+
logger.trace({location: result.path, fields}, '[DwJsonSource] Loaded config');
4542

46-
logger.trace({path: this.lastPath, fields}, '[DwJsonSource] Loaded config');
47-
48-
return normalized;
49-
}
50-
51-
getPath(): string | undefined {
52-
return this.lastPath;
43+
return {config, location: result.path};
5344
}
5445
}

packages/b2c-tooling-sdk/src/config/sources/mobify-source.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import * as fs from 'node:fs';
1212
import * as os from 'node:os';
1313
import * as path from 'node:path';
14-
import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '../types.js';
14+
import type {ConfigSource, ConfigLoadResult, ResolveConfigOptions} from '../types.js';
1515
import {getLogger} from '../../logging/logger.js';
1616

1717
/**
@@ -39,19 +39,17 @@ interface MobifyConfigFile {
3939
*/
4040
export class MobifySource implements ConfigSource {
4141
readonly name = 'MobifySource';
42-
private lastPath?: string;
4342

44-
load(options: ResolveConfigOptions): NormalizedConfig | undefined {
43+
load(options: ResolveConfigOptions): ConfigLoadResult | undefined {
4544
const logger = getLogger();
4645

4746
// Use explicit credentialsFile if provided, otherwise use default path
4847
const mobifyPath = options.credentialsFile ?? this.getMobifyPath(options.cloudOrigin);
49-
this.lastPath = mobifyPath;
5048

51-
logger.trace({path: mobifyPath}, '[MobifySource] Checking for credentials file');
49+
logger.trace({location: mobifyPath}, '[MobifySource] Checking for credentials file');
5250

5351
if (!fs.existsSync(mobifyPath)) {
54-
logger.trace({path: mobifyPath}, '[MobifySource] No credentials file found');
52+
logger.trace({location: mobifyPath}, '[MobifySource] No credentials file found');
5553
return undefined;
5654
}
5755

@@ -60,27 +58,24 @@ export class MobifySource implements ConfigSource {
6058
const config = JSON.parse(content) as MobifyConfigFile;
6159

6260
if (!config.api_key) {
63-
logger.trace({path: mobifyPath}, '[MobifySource] Credentials file found but no api_key present');
61+
logger.trace({location: mobifyPath}, '[MobifySource] Credentials file found but no api_key present');
6462
return undefined;
6563
}
6664

67-
logger.trace({path: mobifyPath, fields: ['mrtApiKey']}, '[MobifySource] Loaded credentials');
65+
logger.trace({location: mobifyPath, fields: ['mrtApiKey']}, '[MobifySource] Loaded credentials');
6866

6967
return {
70-
mrtApiKey: config.api_key,
68+
config: {mrtApiKey: config.api_key},
69+
location: mobifyPath,
7170
};
7271
} catch (error) {
7372
// Invalid JSON or read error
7473
const message = error instanceof Error ? error.message : String(error);
75-
logger.trace({path: mobifyPath, error: message}, '[MobifySource] Failed to parse credentials file');
74+
logger.trace({location: mobifyPath, error: message}, '[MobifySource] Failed to parse credentials file');
7675
return undefined;
7776
}
7877
}
7978

80-
getPath(): string | undefined {
81-
return this.lastPath;
82-
}
83-
8479
/**
8580
* Determines the mobify config file path based on cloud origin.
8681
*/

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

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,12 @@ export interface ConfigWarning {
9191
export interface ConfigSourceInfo {
9292
/** Human-readable name of the source */
9393
name: string;
94-
/** Path to the source file (if applicable) */
95-
path?: string;
96-
/** Fields that this source contributed to the final config */
97-
fieldsContributed: (keyof NormalizedConfig)[];
94+
/** Location of the source (file path, keychain entry, URL, etc.) */
95+
location?: string;
96+
/** All fields that this source provided values for */
97+
fields: (keyof NormalizedConfig)[];
98+
/** Fields that were not used because a higher priority source already provided them */
99+
fieldsIgnored?: (keyof NormalizedConfig)[];
98100
}
99101

100102
/**
@@ -142,6 +144,19 @@ export interface ResolveConfigOptions {
142144
replaceDefaultSources?: boolean;
143145
}
144146

147+
/**
148+
* Result of loading configuration from a source.
149+
*/
150+
export interface ConfigLoadResult {
151+
/** The loaded configuration */
152+
config: NormalizedConfig;
153+
/**
154+
* Location of the source (for diagnostics).
155+
* May be a file path, keychain entry, URL, or other identifier.
156+
*/
157+
location?: string;
158+
}
159+
145160
/**
146161
* A configuration source that can contribute config values.
147162
*
@@ -150,14 +165,14 @@ export interface ResolveConfigOptions {
150165
*
151166
* @example
152167
* ```typescript
153-
* import type { ConfigSource, NormalizedConfig, ResolveConfigOptions } from '@salesforce/b2c-tooling-sdk/config';
168+
* import type { ConfigSource, ConfigLoadResult, ResolveConfigOptions } from '@salesforce/b2c-tooling-sdk/config';
154169
*
155170
* class MyCustomSource implements ConfigSource {
156171
* name = 'my-custom-source';
157172
*
158-
* load(options: ResolveConfigOptions): NormalizedConfig | undefined {
173+
* load(options: ResolveConfigOptions): ConfigLoadResult | undefined {
159174
* // Load config from your custom source
160-
* return { hostname: 'example.com' };
175+
* return { config: { hostname: 'example.com' }, location: '/path/to/config' };
161176
* }
162177
* }
163178
* ```
@@ -170,15 +185,9 @@ export interface ConfigSource {
170185
* Load configuration from this source.
171186
*
172187
* @param options - Resolution options
173-
* @returns Partial config from this source, or undefined if source not available
174-
*/
175-
load(options: ResolveConfigOptions): NormalizedConfig | undefined;
176-
177-
/**
178-
* Get the path to this source's file (if applicable).
179-
* Used for diagnostics and source info.
188+
* @returns Config and location from this source, or undefined if source not available
180189
*/
181-
getPath?(): string | undefined;
190+
load(options: ResolveConfigOptions): ConfigLoadResult | undefined;
182191
}
183192

184193
/**

0 commit comments

Comments
 (0)