diff --git a/docs/guide/extending.md b/docs/guide/extending.md index 8d0e0e7..51db678 100644 --- a/docs/guide/extending.md +++ b/docs/guide/extending.md @@ -151,29 +151,28 @@ export default hook; // src/sources/my-custom-source.ts import type { ConfigSource, - NormalizedConfig, + ConfigLoadResult, ResolveConfigOptions } from '@salesforce/b2c-tooling-sdk/config'; export class MyCustomSource implements ConfigSource { readonly name = 'my-custom-source'; - load(options: ResolveConfigOptions): NormalizedConfig | undefined { + load(options: ResolveConfigOptions): ConfigLoadResult | undefined { // Load config from your custom source // Return undefined if source is not available return { - hostname: 'example.sandbox.us03.dx.commercecloud.salesforce.com', - clientId: 'your-client-id', - clientSecret: 'your-client-secret', - codeVersion: 'version1', + config: { + hostname: 'example.sandbox.us03.dx.commercecloud.salesforce.com', + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + codeVersion: 'version1', + }, + // Location is used for diagnostics - can be a file path, keychain entry, URL, etc. + location: '/path/to/config/source', }; } - - // Optional: return path for diagnostics - getPath(): string | undefined { - return '/path/to/config/source'; - } } ``` diff --git a/packages/b2c-cli/src/commands/code/activate.ts b/packages/b2c-cli/src/commands/code/activate.ts index af4197c..5f9fcee 100644 --- a/packages/b2c-cli/src/commands/code/activate.ts +++ b/packages/b2c-cli/src/commands/code/activate.ts @@ -38,10 +38,10 @@ export default class CodeActivate extends InstanceCommand { this.requireOAuthCredentials(); const codeVersionArg = this.args.codeVersion; - const hostname = this.resolvedConfig.hostname!; + const hostname = this.resolvedConfig.values.hostname!; // Get code version from arg, flag, or config - const codeVersion = codeVersionArg ?? this.resolvedConfig.codeVersion; + const codeVersion = codeVersionArg ?? this.resolvedConfig.values.codeVersion; if (this.flags.reload) { // Reload mode - re-activate the code version diff --git a/packages/b2c-cli/src/commands/code/delete.ts b/packages/b2c-cli/src/commands/code/delete.ts index 4bb0888..bc2870a 100644 --- a/packages/b2c-cli/src/commands/code/delete.ts +++ b/packages/b2c-cli/src/commands/code/delete.ts @@ -55,7 +55,7 @@ export default class CodeDelete extends InstanceCommand { this.requireOAuthCredentials(); const codeVersion = this.args.codeVersion; - const hostname = this.resolvedConfig.hostname!; + const hostname = this.resolvedConfig.values.hostname!; // Confirm deletion unless --force is used if (!this.flags.force) { diff --git a/packages/b2c-cli/src/commands/code/deploy.ts b/packages/b2c-cli/src/commands/code/deploy.ts index 5a600a5..65fa2d7 100644 --- a/packages/b2c-cli/src/commands/code/deploy.ts +++ b/packages/b2c-cli/src/commands/code/deploy.ts @@ -51,8 +51,8 @@ export default class CodeDeploy extends CartridgeCommand { this.requireWebDavCredentials(); this.requireOAuthCredentials(); - const hostname = this.resolvedConfig.hostname!; - let version = this.resolvedConfig.codeVersion; + const hostname = this.resolvedConfig.values.hostname!; + let version = this.resolvedConfig.values.codeVersion; // If no code version specified, discover the active one if (!version) { @@ -102,7 +102,8 @@ export default class CodeDeploy extends CartridgeCommand { this.error(t('commands.code.deploy.noCartridges', 'No cartridges found in {{path}}', {path: this.cartridgePath})); } - this.log( + this.logger?.info( + {path: this.cartridgePath, server: hostname, codeVersion: version}, t('commands.code.deploy.deploying', 'Deploying {{path}} to {{hostname}} ({{version}})', { path: this.cartridgePath, hostname, @@ -112,7 +113,7 @@ export default class CodeDeploy extends CartridgeCommand { // Log found cartridges for (const c of cartridges) { - this.logger?.debug(` ${c.name} (${c.src})`); + this.logger?.debug({cartridgeName: c.name, path: c.src}, ` ${c.name}`); } try { @@ -141,7 +142,8 @@ export default class CodeDeploy extends CartridgeCommand { reloaded, }; - this.log( + this.logger?.info( + {codeVersion: result.codeVersion, cartridgeCount: result.cartridges.length}, t('commands.code.deploy.summary', 'Deployed {{count}} cartridge(s) to {{codeVersion}}', { count: result.cartridges.length, codeVersion: result.codeVersion, diff --git a/packages/b2c-cli/src/commands/code/list.ts b/packages/b2c-cli/src/commands/code/list.ts index b0d9a48..86012b0 100644 --- a/packages/b2c-cli/src/commands/code/list.ts +++ b/packages/b2c-cli/src/commands/code/list.ts @@ -47,7 +47,7 @@ export default class CodeList extends InstanceCommand { async run(): Promise { this.requireOAuthCredentials(); - const hostname = this.resolvedConfig.hostname!; + const hostname = this.resolvedConfig.values.hostname!; this.log(t('commands.code.list.fetching', 'Fetching code versions from {{hostname}}...', {hostname})); diff --git a/packages/b2c-cli/src/commands/code/watch.ts b/packages/b2c-cli/src/commands/code/watch.ts index 03f763d..849fe0a 100644 --- a/packages/b2c-cli/src/commands/code/watch.ts +++ b/packages/b2c-cli/src/commands/code/watch.ts @@ -31,8 +31,8 @@ export default class CodeWatch extends CartridgeCommand { this.requireWebDavCredentials(); this.requireOAuthCredentials(); - const hostname = this.resolvedConfig.hostname!; - const version = this.resolvedConfig.codeVersion; + const hostname = this.resolvedConfig.values.hostname!; + const version = this.resolvedConfig.values.codeVersion; this.log(t('commands.code.watch.starting', 'Starting watcher for {{path}}', {path: this.cartridgePath})); this.log(t('commands.code.watch.target', 'Target: {{hostname}}', {hostname})); diff --git a/packages/b2c-cli/src/commands/docs/download.ts b/packages/b2c-cli/src/commands/docs/download.ts index 42d2d6f..20927d6 100644 --- a/packages/b2c-cli/src/commands/docs/download.ts +++ b/packages/b2c-cli/src/commands/docs/download.ts @@ -46,7 +46,7 @@ export default class DocsDownload extends InstanceCommand { this.log( t('commands.docs.download.downloading', 'Downloading documentation from {{hostname}}...', { - hostname: this.resolvedConfig.hostname, + hostname: this.resolvedConfig.values.hostname, }), ); diff --git a/packages/b2c-cli/src/commands/job/export.ts b/packages/b2c-cli/src/commands/job/export.ts index 611c2ea..2188845 100644 --- a/packages/b2c-cli/src/commands/job/export.ts +++ b/packages/b2c-cli/src/commands/job/export.ts @@ -117,7 +117,7 @@ export default class JobExport extends JobCommand { 'show-log': showLog, } = this.flags; - const hostname = this.resolvedConfig.hostname!; + const hostname = this.resolvedConfig.values.hostname!; // Build data units configuration const dataUnits = this.buildDataUnits({ diff --git a/packages/b2c-cli/src/commands/job/import.ts b/packages/b2c-cli/src/commands/job/import.ts index 3b735ad..eaca9f4 100644 --- a/packages/b2c-cli/src/commands/job/import.ts +++ b/packages/b2c-cli/src/commands/job/import.ts @@ -63,7 +63,7 @@ export default class JobImport extends JobCommand { const {target} = this.args; const {'keep-archive': keepArchive, remote, timeout, 'show-log': showLog} = this.flags; - const hostname = this.resolvedConfig.hostname!; + const hostname = this.resolvedConfig.values.hostname!; // Create lifecycle context const context = this.createContext('job:import', { diff --git a/packages/b2c-cli/src/commands/job/run.ts b/packages/b2c-cli/src/commands/job/run.ts index 6eafde8..1284187 100644 --- a/packages/b2c-cli/src/commands/job/run.ts +++ b/packages/b2c-cli/src/commands/job/run.ts @@ -82,7 +82,7 @@ export default class JobRun extends JobCommand { parameters: rawBody ? undefined : parameters, body: rawBody, wait, - hostname: this.resolvedConfig.hostname, + hostname: this.resolvedConfig.values.hostname, }); // Run beforeOperation hooks - check for skip @@ -100,7 +100,7 @@ export default class JobRun extends JobCommand { this.log( t('commands.job.run.executing', 'Executing job {{jobId}} on {{hostname}}...', { jobId, - hostname: this.resolvedConfig.hostname!, + hostname: this.resolvedConfig.values.hostname!, }), ); diff --git a/packages/b2c-cli/src/commands/job/search.ts b/packages/b2c-cli/src/commands/job/search.ts index e371774..021bfe1 100644 --- a/packages/b2c-cli/src/commands/job/search.ts +++ b/packages/b2c-cli/src/commands/job/search.ts @@ -86,7 +86,7 @@ export default class JobSearch extends InstanceCommand { this.log( t('commands.job.search.searching', 'Searching job executions on {{hostname}}...', { - hostname: this.resolvedConfig.hostname!, + hostname: this.resolvedConfig.values.hostname!, }), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/create.ts b/packages/b2c-cli/src/commands/mrt/env/create.ts index 011a6bc..012f23d 100644 --- a/packages/b2c-cli/src/commands/mrt/env/create.ts +++ b/packages/b2c-cli/src/commands/mrt/env/create.ts @@ -197,7 +197,7 @@ export default class MrtEnvCreate extends MrtCommand { this.requireMrtCredentials(); const {slug} = this.args; - const {mrtProject: project} = this.resolvedConfig; + const {mrtProject: project} = this.resolvedConfig.values; if (!project) { this.error( @@ -242,7 +242,7 @@ export default class MrtEnvCreate extends MrtCommand { allowCookies: allowCookies || undefined, enableSourceMaps: enableSourceMaps || undefined, proxyConfigs, - origin: this.resolvedConfig.mrtOrigin, + origin: this.resolvedConfig.values.mrtOrigin, }, this.getMrtAuth(), ); @@ -256,7 +256,7 @@ export default class MrtEnvCreate extends MrtCommand { { projectSlug: project, slug, - origin: this.resolvedConfig.mrtOrigin, + origin: this.resolvedConfig.values.mrtOrigin, onPoll: (env) => { if (!this.jsonEnabled()) { const elapsed = Math.round((Date.now() - waitStartTime) / 1000); diff --git a/packages/b2c-cli/src/commands/mrt/env/delete.ts b/packages/b2c-cli/src/commands/mrt/env/delete.ts index ece18f8..140276a 100644 --- a/packages/b2c-cli/src/commands/mrt/env/delete.ts +++ b/packages/b2c-cli/src/commands/mrt/env/delete.ts @@ -59,7 +59,7 @@ export default class MrtEnvDelete extends MrtCommand { this.requireMrtCredentials(); const {slug} = this.args; - const {mrtProject: project} = this.resolvedConfig; + const {mrtProject: project} = this.resolvedConfig.values; if (!project) { this.error( @@ -99,7 +99,7 @@ export default class MrtEnvDelete extends MrtCommand { { projectSlug: project, slug, - origin: this.resolvedConfig.mrtOrigin, + origin: this.resolvedConfig.values.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/var/delete.ts b/packages/b2c-cli/src/commands/mrt/env/var/delete.ts index 62d6141..6359275 100644 --- a/packages/b2c-cli/src/commands/mrt/env/var/delete.ts +++ b/packages/b2c-cli/src/commands/mrt/env/var/delete.ts @@ -39,7 +39,7 @@ export default class MrtEnvVarDelete extends MrtCommand this.requireMrtCredentials(); const {key} = this.args; - const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig; + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; if (!project) { this.error( @@ -57,7 +57,7 @@ export default class MrtEnvVarDelete extends MrtCommand projectSlug: project, environment, key, - origin: this.resolvedConfig.mrtOrigin, + origin: this.resolvedConfig.values.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/var/list.ts b/packages/b2c-cli/src/commands/mrt/env/var/list.ts index 1b3d2bf..ef90797 100644 --- a/packages/b2c-cli/src/commands/mrt/env/var/list.ts +++ b/packages/b2c-cli/src/commands/mrt/env/var/list.ts @@ -56,7 +56,7 @@ export default class MrtEnvVarList extends MrtCommand { async run(): Promise { this.requireMrtCredentials(); - const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig; + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; if (!project) { this.error( @@ -80,7 +80,7 @@ export default class MrtEnvVarList extends MrtCommand { { projectSlug: project, environment, - origin: this.resolvedConfig.mrtOrigin, + origin: this.resolvedConfig.values.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/var/set.ts b/packages/b2c-cli/src/commands/mrt/env/var/set.ts index 537d4e1..6744ebd 100644 --- a/packages/b2c-cli/src/commands/mrt/env/var/set.ts +++ b/packages/b2c-cli/src/commands/mrt/env/var/set.ts @@ -43,7 +43,7 @@ export default class MrtEnvVarSet extends MrtCommand { this.requireMrtCredentials(); const {argv} = await this.parse(MrtEnvVarSet); - const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig; + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; if (!project) { this.error( @@ -88,7 +88,7 @@ export default class MrtEnvVarSet extends MrtCommand { projectSlug: project, environment, variables, - origin: this.resolvedConfig.mrtOrigin, + origin: this.resolvedConfig.values.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/push.ts b/packages/b2c-cli/src/commands/mrt/push.ts index 5153cd5..a0f22de 100644 --- a/packages/b2c-cli/src/commands/mrt/push.ts +++ b/packages/b2c-cli/src/commands/mrt/push.ts @@ -79,7 +79,7 @@ export default class MrtPush extends MrtCommand { async run(): Promise { this.requireMrtCredentials(); - const {mrtProject: project, mrtEnvironment: target} = this.resolvedConfig; + const {mrtProject: project, mrtEnvironment: target} = this.resolvedConfig.values; const {message} = this.flags; if (!project) { @@ -116,7 +116,7 @@ export default class MrtPush extends MrtCommand { ssrOnly, ssrShared, ssrParameters, - origin: this.resolvedConfig.mrtOrigin, + origin: this.resolvedConfig.values.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/ods/create.ts b/packages/b2c-cli/src/commands/ods/create.ts index f752919..d98cb57 100644 --- a/packages/b2c-cli/src/commands/ods/create.ts +++ b/packages/b2c-cli/src/commands/ods/create.ts @@ -122,7 +122,7 @@ export default class OdsCreate extends OdsCommand { t( 'commands.ods.create.settingPermissions', 'Setting OCAPI and WebDAV permissions for client ID: {{clientId}}', - {clientId: this.resolvedConfig.clientId!}, + {clientId: this.resolvedConfig.values.clientId!}, ), ); } @@ -177,7 +177,7 @@ export default class OdsCreate extends OdsCommand { return undefined; } - const clientId = this.resolvedConfig.clientId; + const clientId = this.resolvedConfig.values.clientId; if (!clientId) { return undefined; } diff --git a/packages/b2c-cli/src/commands/scapi/custom/status.ts b/packages/b2c-cli/src/commands/scapi/custom/status.ts index de5c066..e8ed0df 100644 --- a/packages/b2c-cli/src/commands/scapi/custom/status.ts +++ b/packages/b2c-cli/src/commands/scapi/custom/status.ts @@ -219,7 +219,7 @@ export default class ScapiCustomStatus extends ScapiCustomCommand { async run(): Promise { this.requireOAuthCredentials(); - const hostname = this.resolvedConfig.hostname!; + const hostname = this.resolvedConfig.values.hostname!; this.log(t('commands.sites.list.fetching', 'Fetching sites from {{hostname}}...', {hostname})); diff --git a/packages/b2c-cli/src/commands/slas/client/open.ts b/packages/b2c-cli/src/commands/slas/client/open.ts index a7a6f24..e0e4749 100644 --- a/packages/b2c-cli/src/commands/slas/client/open.ts +++ b/packages/b2c-cli/src/commands/slas/client/open.ts @@ -48,7 +48,7 @@ export default class SlasClientOpen extends BaseCommand { const {'tenant-id': tenantId, 'short-code': shortCodeFlag} = this.flags; const {clientId} = this.args; - const shortCode = shortCodeFlag ?? this.resolvedConfig.shortCode; + const shortCode = shortCodeFlag ?? this.resolvedConfig.values.shortCode; if (!shortCode) { this.error( diff --git a/packages/b2c-cli/src/index.ts b/packages/b2c-cli/src/index.ts index 3d71f7d..8f47cb5 100644 --- a/packages/b2c-cli/src/index.ts +++ b/packages/b2c-cli/src/index.ts @@ -16,4 +16,4 @@ export { loadConfig, findDwJson, } from '@salesforce/b2c-tooling-sdk/cli'; -export type {ResolvedConfig, LoadConfigOptions} from '@salesforce/b2c-tooling-sdk/cli'; +export type {LoadConfigOptions} from '@salesforce/b2c-tooling-sdk/cli'; diff --git a/packages/b2c-cli/src/utils/slas/client.ts b/packages/b2c-cli/src/utils/slas/client.ts index 70e5ed1..0a48388 100644 --- a/packages/b2c-cli/src/utils/slas/client.ts +++ b/packages/b2c-cli/src/utils/slas/client.ts @@ -180,7 +180,7 @@ export abstract class SlasClientCommand extends OAuthC * Get the SLAS client, ensuring short code is configured. */ protected getSlasClient(): SlasClient { - const {shortCode} = this.resolvedConfig; + const {shortCode} = this.resolvedConfig.values; if (!shortCode) { this.error( t( diff --git a/packages/b2c-cli/test/helpers/ods.ts b/packages/b2c-cli/test/helpers/ods.ts index 802dba6..ba84bdc 100644 --- a/packages/b2c-cli/test/helpers/ods.ts +++ b/packages/b2c-cli/test/helpers/ods.ts @@ -42,9 +42,13 @@ export function stubOdsClient(command: any, client: Partial<{GET: any; POST: any }); } -export function stubResolvedConfig(command: any, resolvedConfig: Record): void { +export function stubResolvedConfig(command: any, values: Record): void { Object.defineProperty(command, 'resolvedConfig', { - get: () => resolvedConfig, + get: () => ({ + values, + warnings: [], + sources: [], + }), configurable: true, }); } diff --git a/packages/b2c-plugin-example-config/src/sources/env-file-source.ts b/packages/b2c-plugin-example-config/src/sources/env-file-source.ts index 50c572d..c41c2c9 100644 --- a/packages/b2c-plugin-example-config/src/sources/env-file-source.ts +++ b/packages/b2c-plugin-example-config/src/sources/env-file-source.ts @@ -11,7 +11,7 @@ */ import {existsSync, readFileSync} from 'node:fs'; import {join} from 'node:path'; -import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '@salesforce/b2c-tooling-sdk/config'; +import type {ConfigSource, ConfigLoadResult, ResolveConfigOptions} from '@salesforce/b2c-tooling-sdk/config'; /** * ConfigSource implementation that loads from .env.b2c files. @@ -53,8 +53,6 @@ import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '@salesf export class EnvFileSource implements ConfigSource { readonly name = 'env-file (.env.b2c)'; - private envFilePath?: string; - /** * Load configuration from .env.b2c file. * @@ -64,48 +62,45 @@ export class EnvFileSource implements ConfigSource { * 3. .env.b2c in current working directory * * @param options - Resolution options (startDir used for file lookup) - * @returns Parsed configuration or undefined if file not found + * @returns Parsed configuration and location, or undefined if file not found */ - load(options: ResolveConfigOptions): NormalizedConfig | undefined { + load(options: ResolveConfigOptions): ConfigLoadResult | undefined { // Check for explicit path override via environment variable const envOverride = process.env.B2C_ENV_FILE_PATH; + let envFilePath: string; if (envOverride) { - this.envFilePath = envOverride; + envFilePath = envOverride; } else { const searchDir = options.startDir ?? process.cwd(); - this.envFilePath = join(searchDir, '.env.b2c'); + envFilePath = join(searchDir, '.env.b2c'); } - if (!existsSync(this.envFilePath)) { + if (!existsSync(envFilePath)) { return undefined; } - const content = readFileSync(this.envFilePath, 'utf-8'); + const content = readFileSync(envFilePath, 'utf-8'); const vars = this.parseEnvFile(content); return { - hostname: vars.HOSTNAME, - webdavHostname: vars.WEBDAV_HOSTNAME, - codeVersion: vars.CODE_VERSION, - username: vars.USERNAME, - password: vars.PASSWORD, - clientId: vars.CLIENT_ID, - clientSecret: vars.CLIENT_SECRET, - scopes: vars.SCOPES ? vars.SCOPES.split(',').map((s) => s.trim()) : undefined, - shortCode: vars.SHORT_CODE, - mrtProject: vars.MRT_PROJECT, - mrtEnvironment: vars.MRT_ENVIRONMENT, - mrtApiKey: vars.MRT_API_KEY, + config: { + hostname: vars.HOSTNAME, + webdavHostname: vars.WEBDAV_HOSTNAME, + codeVersion: vars.CODE_VERSION, + username: vars.USERNAME, + password: vars.PASSWORD, + clientId: vars.CLIENT_ID, + clientSecret: vars.CLIENT_SECRET, + scopes: vars.SCOPES ? vars.SCOPES.split(',').map((s) => s.trim()) : undefined, + shortCode: vars.SHORT_CODE, + mrtProject: vars.MRT_PROJECT, + mrtEnvironment: vars.MRT_ENVIRONMENT, + mrtApiKey: vars.MRT_API_KEY, + }, + location: envFilePath, }; } - /** - * Get the path to the env file (for diagnostics). - */ - getPath(): string | undefined { - return this.envFilePath; - } - /** * Parse a .env file format into key-value pairs. * diff --git a/packages/b2c-tooling-sdk/src/auth/api-key.ts b/packages/b2c-tooling-sdk/src/auth/api-key.ts index 97f4f40..4a0643c 100644 --- a/packages/b2c-tooling-sdk/src/auth/api-key.ts +++ b/packages/b2c-tooling-sdk/src/auth/api-key.ts @@ -37,7 +37,7 @@ export class ApiKeyStrategy implements AuthStrategy { // Show partial key for identification (first 8 chars) const keyPreview = key.length > 8 ? `${key.slice(0, 8)}...` : key; - logger.debug({headerName}, `[Auth] Using API Key authentication (${headerName}): ${keyPreview}`); + logger.debug({headerName, keyPreview}, `[Auth] Using API Key authentication (${headerName}): ${keyPreview}`); } async fetch(url: string, init: RequestInit = {}): Promise { diff --git a/packages/b2c-tooling-sdk/src/auth/oauth-implicit.ts b/packages/b2c-tooling-sdk/src/auth/oauth-implicit.ts index 4044446..f6d7da1 100644 --- a/packages/b2c-tooling-sdk/src/auth/oauth-implicit.ts +++ b/packages/b2c-tooling-sdk/src/auth/oauth-implicit.ts @@ -107,20 +107,17 @@ export class ImplicitOAuthStrategy implements AuthStrategy { const logger = getLogger(); logger.debug( - {clientId: this.config.clientId, accountManagerHost: this.accountManagerHost, localPort: this.localPort}, - `[Auth] ImplicitOAuthStrategy initialized for client: ${this.config.clientId}`, - ); - logger.trace( - {scopes: this.config.scopes}, - `[Auth] Configured scopes: ${this.config.scopes?.join(', ') || '(none)'}`, + {clientId: this.config.clientId, accountManagerHost: this.accountManagerHost, port: this.localPort}, + '[Auth] ImplicitOAuthStrategy initialized', ); + logger.trace({scopes: this.config.scopes}, '[Auth] Configured scopes'); } async fetch(url: string, init: RequestInit = {}): Promise { const logger = getLogger(); const method = init.method || 'GET'; - logger.trace({method, url}, `[Auth] Fetching with implicit OAuth: ${method} ${url}`); + logger.trace({method, url}, '[Auth] Fetching with implicit OAuth'); const token = await this.getAccessToken(); @@ -132,10 +129,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { let res = await fetch(url, {...init, headers}); const duration = Date.now() - startTime; - logger.debug( - {method, url, status: res.status, duration}, - `[Auth] Response: ${method} ${url} ${res.status} ${duration}ms`, - ); + logger.debug({method, url, status: res.status, duration}, '[Auth] Response'); // RESILIENCE: If the server says 401, the token might have expired or been revoked. // We retry exactly once after invalidating the cached token. @@ -149,10 +143,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { res = await fetch(url, {...init, headers}); const retryDuration = Date.now() - retryStart; - logger.debug( - {method, url, status: res.status, duration: retryDuration}, - `[Auth] Retry response: ${method} ${url} ${res.status} ${retryDuration}ms`, - ); + logger.debug({method, url, status: res.status, duration: retryDuration}, '[Auth] Retry response'); } return res; @@ -244,10 +235,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { ); ACCESS_TOKEN_CACHE.delete(clientId); } else { - logger.debug( - {timeUntilExpiryMs: timeUntilExpiry}, - `[Auth] Reusing cached access token (expires in ${Math.round(timeUntilExpiry / 1000)}s)`, - ); + logger.debug({timeUntilExpiryMs: timeUntilExpiry}, '[Auth] Reusing cached access token'); return cached.accessToken; } } @@ -314,7 +302,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { logger.trace({authorizeUrl}, '[Auth] Authorization URL'); // Print URL to console (in case machine has no default browser) - logger.info(`Login URL: ${authorizeUrl}`); + logger.info({url: authorizeUrl}, `Login URL: ${authorizeUrl}`); logger.info('If the URL does not open automatically, copy/paste it into a browser on this machine.'); // Attempt to open the browser @@ -337,7 +325,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { hasAccessToken: !!accessToken, hasError: !!error, }, - `[Auth] Received redirect request: ${requestUrl.pathname}`, + '[Auth] Received redirect request', ); if (!accessToken && !error) { @@ -349,7 +337,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { } else if (accessToken) { const authDuration = Date.now() - startTime; // Successfully received access token - logger.debug({authDurationMs: authDuration}, `[Auth] Got access token response (took ${authDuration}ms)`); + logger.debug({duration: authDuration}, `[Auth] Got access token response (${authDuration}ms)`); logger.info('Successfully authenticated'); try { @@ -366,7 +354,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { logger.debug( {expiresIn, expiresAt: expiration.toISOString(), scopes}, - `[Auth] Token expires in ${expiresIn}s, scopes: ${scopes.join(', ') || '(none)'}`, + `[Auth] Token expires in ${expiresIn}s, scopes: ${scopes.join(' ')}`, ); resolve({ @@ -391,7 +379,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { } else if (error) { // OAuth error response const errorMessage = errorDescription || error; - logger.error({error, errorDescription}, `[Auth] OAuth error: ${errorMessage}`); + logger.error({error, errorDescription}, `[Auth] OAuth error: ${error}`); response.writeHead(500, {'Content-Type': 'text/plain'}); response.write(`Authentication failed: ${errorMessage}`); response.end(); @@ -421,7 +409,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy { }); server.on('error', (err) => { - logger.error({error: err.message, port: this.localPort}, `[Auth] Failed to start OAuth redirect server`); + logger.error({error: err.message, port: this.localPort}, '[Auth] Failed to start OAuth redirect server'); reject(new Error(`Failed to start OAuth redirect server: ${err.message}`)); }); }); diff --git a/packages/b2c-tooling-sdk/src/auth/oauth.ts b/packages/b2c-tooling-sdk/src/auth/oauth.ts index b5e1e39..30c7536 100644 --- a/packages/b2c-tooling-sdk/src/auth/oauth.ts +++ b/packages/b2c-tooling-sdk/src/auth/oauth.ts @@ -178,7 +178,7 @@ export class OAuthStrategy implements AuthStrategy { logger.debug({method, url}, `[Auth REQ] ${method} ${url}`); // Trace: Log request details - logger.trace({headers: requestHeaders, body: params.toString()}, `[Auth REQ BODY] ${method} ${url}`); + logger.trace({method, url, headers: requestHeaders, body: params.toString()}, `[Auth REQ BODY] ${method} ${url}`); const startTime = Date.now(); const response = await fetch(url, { @@ -202,7 +202,7 @@ export class OAuthStrategy implements AuthStrategy { if (!response.ok) { const errorText = await response.text(); - logger.trace({headers: responseHeaders, body: errorText}, `[Auth RESP BODY] ${method} ${url}`); + logger.trace({method, url, headers: responseHeaders, body: errorText}, `[Auth RESP BODY] ${method} ${url}`); throw new Error(`Failed to get access token: ${response.status} ${response.statusText} - ${errorText}`); } @@ -213,7 +213,7 @@ export class OAuthStrategy implements AuthStrategy { }; // Trace: Log response details - logger.trace({headers: responseHeaders, body: data}, `[Auth RESP BODY] ${method} ${url}`); + logger.trace({method, url, headers: responseHeaders, body: data}, `[Auth RESP BODY] ${method} ${url}`); const jwt = decodeJWT(data.access_token); logger.trace({jwt: jwt.payload}, '[Auth] JWT payload'); diff --git a/packages/b2c-tooling-sdk/src/cli/base-command.ts b/packages/b2c-tooling-sdk/src/cli/base-command.ts index 7af8546..e5462b2 100644 --- a/packages/b2c-tooling-sdk/src/cli/base-command.ts +++ b/packages/b2c-tooling-sdk/src/cli/base-command.ts @@ -5,7 +5,8 @@ */ import {Command, Flags, type Interfaces} from '@oclif/core'; import {loadConfig} from './config.js'; -import type {ResolvedConfig, LoadConfigOptions, PluginSources} from './config.js'; +import type {LoadConfigOptions, PluginSources} from './config.js'; +import type {ResolvedB2CConfig} from '../config/index.js'; import type { ConfigSourcesHookOptions, ConfigSourcesHookResult, @@ -95,7 +96,7 @@ export abstract class BaseCommand extends Command { protected flags!: Flags; protected args!: Args; - protected resolvedConfig!: ResolvedConfig; + protected resolvedConfig!: ResolvedB2CConfig; protected logger!: Logger; /** High-priority config sources from plugins (inserted before defaults) */ @@ -193,7 +194,7 @@ export abstract class BaseCommand extends Command { return input; } - protected loadConfiguration(): ResolvedConfig { + protected loadConfiguration(): ResolvedB2CConfig { const options: LoadConfigOptions = { instance: this.flags.instance, configPath: this.flags.config, @@ -219,13 +220,17 @@ export abstract class BaseCommand extends Command { * - `pluginSourcesAfter`: Low priority sources (fill gaps) */ protected async collectPluginConfigSources(): Promise { + // Access flags that may be defined in subclasses (OAuthCommand, InstanceCommand) + const flags = this.flags as Record; + const hookOptions: ConfigSourcesHookOptions = { instance: this.flags.instance, configPath: this.flags.config, - flags: this.flags as Record, + flags, resolveOptions: { instance: this.flags.instance, configPath: this.flags.config, + accountManagerHost: flags['account-manager-host'] as string | undefined, }, }; diff --git a/packages/b2c-tooling-sdk/src/cli/cartridge-command.ts b/packages/b2c-tooling-sdk/src/cli/cartridge-command.ts index ec903fb..a690772 100644 --- a/packages/b2c-tooling-sdk/src/cli/cartridge-command.ts +++ b/packages/b2c-tooling-sdk/src/cli/cartridge-command.ts @@ -157,7 +157,7 @@ export abstract class CartridgeCommand extends Instanc directory: absoluteDir, include: filterOptions.include, exclude: filterOptions.exclude, - codeVersion: this.resolvedConfig?.codeVersion, + codeVersion: this.resolvedConfig?.values.codeVersion, instance: this.instance, }; diff --git a/packages/b2c-tooling-sdk/src/cli/config.ts b/packages/b2c-tooling-sdk/src/cli/config.ts index 6b521c0..da258af 100644 --- a/packages/b2c-tooling-sdk/src/cli/config.ts +++ b/packages/b2c-tooling-sdk/src/cli/config.ts @@ -9,14 +9,11 @@ * This module provides configuration loading for CLI commands. * It uses {@link resolveConfig} internally for consistent behavior. * - * For most use cases, prefer using {@link resolveConfig} directly from the - * `config` module, which provides a richer API with factory methods. - * * @module cli/config */ import type {AuthMethod} from '../auth/types.js'; import {ALL_AUTH_METHODS} from '../auth/types.js'; -import {resolveConfig, type NormalizedConfig, type ConfigSource} from '../config/index.js'; +import {resolveConfig, type NormalizedConfig, type ConfigSource, type ResolvedB2CConfig} from '../config/index.js'; import {findDwJson} from '../config/dw-json.js'; import {getLogger} from '../logging/logger.js'; @@ -25,15 +22,6 @@ export type {AuthMethod}; export {ALL_AUTH_METHODS}; export {findDwJson}; -/** - * Resolved configuration for CLI commands. - * - * This type is an alias for NormalizedConfig to maintain backward compatibility - * with existing CLI code. For new code, prefer using {@link resolveConfig} - * which returns a {@link ResolvedB2CConfig} with factory methods. - */ -export type ResolvedConfig = NormalizedConfig; - /** * Options for loading configuration. */ @@ -46,6 +34,8 @@ export interface LoadConfigOptions { cloudOrigin?: string; /** Path to custom MRT credentials file (overrides default ~/.mobify) */ credentialsFile?: string; + /** Account Manager hostname for OAuth (passed to plugins for host-specific config) */ + accountManagerHost?: string; } /** @@ -78,7 +68,7 @@ export interface PluginSources { * @param flags - Configuration values from CLI flags/env vars * @param options - Loading options * @param pluginSources - Optional sources from CLI plugins (via b2c:config-sources hook) - * @returns Resolved configuration values + * @returns Resolved configuration with factory methods * * @example * ```typescript @@ -87,48 +77,53 @@ export interface PluginSources { * { hostname: this.flags.server, clientId: this.flags['client-id'] }, * { instance: this.flags.instance } * ); - * ``` * - * @example - * ```typescript - * // For richer API with factory methods, use resolveConfig directly: - * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; - * - * const config = resolveConfig(flags, options); * if (config.hasB2CInstanceConfig()) { * const instance = config.createB2CInstance(); * } * ``` */ export function loadConfig( - flags: Partial = {}, + flags: Partial = {}, options: LoadConfigOptions = {}, pluginSources: PluginSources = {}, -): ResolvedConfig { +): ResolvedB2CConfig { const logger = getLogger(); - const resolved = resolveConfig(flags, { + // Preserve instanceName from options.instance if not already in flags + const effectiveFlags = { + ...flags, + instanceName: flags.instanceName ?? options.instance, + }; + + const resolved = resolveConfig(effectiveFlags, { instance: options.instance, configPath: options.configPath, hostnameProtection: true, cloudOrigin: options.cloudOrigin, credentialsFile: options.credentialsFile, + accountManagerHost: options.accountManagerHost, sourcesBefore: pluginSources.before, sourcesAfter: pluginSources.after, }); + // Log source summary + for (const source of resolved.sources) { + logger.trace( + { + source: source.name, + location: source.location, + fields: source.fields, + fieldsIgnored: source.fieldsIgnored, + }, + `[${source.name}] Contributed fields`, + ); + } + // Log warnings for (const warning of resolved.warnings) { logger.trace({warning}, `[Config] ${warning.message}`); } - const config = resolved.values; - - // Handle instanceName from options if not in resolved config - // This preserves backward compatibility with the old behavior - if (!config.instanceName && options.instance) { - config.instanceName = options.instance; - } - - return config as ResolvedConfig; + return resolved; } diff --git a/packages/b2c-tooling-sdk/src/cli/index.ts b/packages/b2c-tooling-sdk/src/cli/index.ts index 234ce26..81f9195 100644 --- a/packages/b2c-tooling-sdk/src/cli/index.ts +++ b/packages/b2c-tooling-sdk/src/cli/index.ts @@ -104,7 +104,7 @@ export type {WebDavRootKey} from './webdav-command.js'; // Config utilities export {loadConfig, findDwJson} from './config.js'; -export type {ResolvedConfig, LoadConfigOptions, PluginSources} from './config.js'; +export type {LoadConfigOptions, PluginSources} from './config.js'; // Hook types for plugin extensibility export type { diff --git a/packages/b2c-tooling-sdk/src/cli/instance-command.ts b/packages/b2c-tooling-sdk/src/cli/instance-command.ts index 255332d..c50b716 100644 --- a/packages/b2c-tooling-sdk/src/cli/instance-command.ts +++ b/packages/b2c-tooling-sdk/src/cli/instance-command.ts @@ -6,8 +6,8 @@ import {Command, Flags} from '@oclif/core'; import {OAuthCommand} from './oauth-command.js'; import {loadConfig} from './config.js'; -import type {ResolvedConfig, LoadConfigOptions, PluginSources} from './config.js'; -import {createInstanceFromConfig} from '../config/index.js'; +import type {LoadConfigOptions, PluginSources} from './config.js'; +import type {NormalizedConfig, ResolvedB2CConfig} from '../config/index.js'; import type {B2CInstance} from '../instance/index.js'; import {t} from '../i18n/index.js'; import { @@ -159,13 +159,13 @@ export abstract class InstanceCommand extends OAuthCom await this.lifecycleRunner.runAfter(context, result); } - protected override loadConfiguration(): ResolvedConfig { + protected override loadConfiguration(): ResolvedB2CConfig { const options: LoadConfigOptions = { instance: this.flags.instance, configPath: this.flags.config, }; - const flagConfig: Partial = { + const flagConfig: Partial = { hostname: this.flags.server, webdavHostname: this.flags['webdav-server'], codeVersion: this.flags['code-version'], @@ -175,6 +175,8 @@ export abstract class InstanceCommand extends OAuthCom clientSecret: this.flags['client-secret'], authMethods: this.parseAuthMethods(), accountManagerHost: this.flags['account-manager-host'], + // Merge scopes from flags (if provided) + scopes: this.flags.scope && this.flags.scope.length > 0 ? this.flags.scope : undefined, }; const pluginSources: PluginSources = { @@ -182,14 +184,7 @@ export abstract class InstanceCommand extends OAuthCom after: this.pluginSourcesAfter, }; - const config = loadConfig(flagConfig, options, pluginSources); - - // Merge scopes from flags with config file scopes (flags take precedence if provided) - if (this.flags.scope && this.flags.scope.length > 0) { - config.scopes = this.flags.scope; - } - - return config; + return loadConfig(flagConfig, options, pluginSources); } /** @@ -208,7 +203,7 @@ export abstract class InstanceCommand extends OAuthCom protected get instance(): B2CInstance { if (!this._instance) { this.requireServer(); - this._instance = createInstanceFromConfig(this.resolvedConfig); + this._instance = this.resolvedConfig.createB2CInstance(); } return this._instance; } @@ -217,16 +212,15 @@ export abstract class InstanceCommand extends OAuthCom * Check if WebDAV credentials are available (Basic or OAuth including implicit). */ protected hasWebDavCredentials(): boolean { - const config = this.resolvedConfig; // Basic auth, or OAuth (client-credentials needs secret, implicit only needs clientId) - return Boolean((config.username && config.password) || config.clientId); + return this.resolvedConfig.hasBasicAuthConfig() || this.resolvedConfig.hasOAuthConfig(); } /** * Validates that server is configured, errors if not. */ protected requireServer(): void { - if (!this.resolvedConfig.hostname) { + if (!this.resolvedConfig.hasB2CInstanceConfig()) { this.error(t('error.serverRequired', 'Server is required. Set via --server, SFCC_SERVER env var, or dw.json.')); } } @@ -235,7 +229,7 @@ export abstract class InstanceCommand extends OAuthCom * Validates that code version is configured, errors if not. */ protected requireCodeVersion(): void { - if (!this.resolvedConfig.codeVersion) { + if (!this.resolvedConfig.values.codeVersion) { this.error( t( 'error.codeVersionRequired', diff --git a/packages/b2c-tooling-sdk/src/cli/mrt-command.ts b/packages/b2c-tooling-sdk/src/cli/mrt-command.ts index 79441f9..0c2e671 100644 --- a/packages/b2c-tooling-sdk/src/cli/mrt-command.ts +++ b/packages/b2c-tooling-sdk/src/cli/mrt-command.ts @@ -6,9 +6,9 @@ import {Command, Flags} from '@oclif/core'; import {BaseCommand} from './base-command.js'; import {loadConfig} from './config.js'; -import type {ResolvedConfig, LoadConfigOptions, PluginSources} from './config.js'; +import type {LoadConfigOptions, PluginSources} from './config.js'; +import type {NormalizedConfig, ResolvedB2CConfig} from '../config/index.js'; import type {AuthStrategy} from '../auth/types.js'; -import {ApiKeyStrategy} from '../auth/api-key.js'; import {MrtClient} from '../platform/mrt.js'; import type {MrtProject} from '../platform/mrt.js'; import {t} from '../i18n/index.js'; @@ -61,7 +61,7 @@ export abstract class MrtCommand extends BaseCommand extends BaseCommand = { + const flagConfig: Partial = { // Flag/env takes precedence, ConfigResolver handles ~/.mobify fallback mrtApiKey: this.flags['api-key'], // Project/environment from flags @@ -94,10 +94,8 @@ export abstract class MrtCommand extends BaseCommand extends BaseCommand extends BaseCommand helpGroup: 'AUTH', }), 'account-manager-host': Flags.string({ - description: 'Account Manager hostname for OAuth', + description: `Account Manager hostname for OAuth (default: ${DEFAULT_ACCOUNT_MANAGER_HOST})`, env: 'SFCC_ACCOUNT_MANAGER_HOST', - default: DEFAULT_ACCOUNT_MANAGER_HOST, helpGroup: 'AUTH', }), }; @@ -83,18 +83,20 @@ export abstract class OAuthCommand extends BaseCommand return methods.length > 0 ? methods : undefined; } - protected override loadConfiguration(): ResolvedConfig { + protected override loadConfiguration(): ResolvedB2CConfig { const options: LoadConfigOptions = { instance: this.flags.instance, configPath: this.flags.config, }; - const flagConfig: Partial = { + const flagConfig: Partial = { clientId: this.flags['client-id'], clientSecret: this.flags['client-secret'], shortCode: this.flags['short-code'], authMethods: this.parseAuthMethods(), accountManagerHost: this.flags['account-manager-host'], + // Merge scopes from flags (if provided) + scopes: this.flags.scope && this.flags.scope.length > 0 ? this.flags.scope : undefined, }; const pluginSources: PluginSources = { @@ -102,21 +104,14 @@ export abstract class OAuthCommand extends BaseCommand after: this.pluginSourcesAfter, }; - const config = loadConfig(flagConfig, options, pluginSources); - - // Merge scopes from flags with config file scopes (flags take precedence if provided) - if (this.flags.scope && this.flags.scope.length > 0) { - config.scopes = this.flags.scope; - } - - return config; + return loadConfig(flagConfig, options, pluginSources); } /** * Gets the configured Account Manager host. */ protected get accountManagerHost(): string { - return this.resolvedConfig.accountManagerHost ?? DEFAULT_ACCOUNT_MANAGER_HOST; + return this.resolvedConfig.values.accountManagerHost ?? DEFAULT_ACCOUNT_MANAGER_HOST; } /** @@ -128,7 +123,7 @@ export abstract class OAuthCommand extends BaseCommand * @throws Error if no allowed method has the required credentials configured */ protected getOAuthStrategy(): OAuthStrategy | ImplicitOAuthStrategy { - const config = this.resolvedConfig; + const config = this.resolvedConfig.values; const accountManagerHost = this.accountManagerHost; // Default to client-credentials and implicit if no methods specified const allowedMethods = config.authMethods || (['client-credentials', 'implicit'] as AuthMethod[]); @@ -177,8 +172,7 @@ export abstract class OAuthCommand extends BaseCommand * Returns true if clientId is configured (with or without clientSecret). */ protected hasOAuthCredentials(): boolean { - const config = this.resolvedConfig; - return Boolean(config.clientId); + return this.resolvedConfig.hasOAuthConfig(); } /** @@ -186,7 +180,7 @@ export abstract class OAuthCommand extends BaseCommand * Returns true only if both clientId and clientSecret are configured. */ protected hasFullOAuthCredentials(): boolean { - const config = this.resolvedConfig; + const config = this.resolvedConfig.values; return Boolean(config.clientId && config.clientSecret); } diff --git a/packages/b2c-tooling-sdk/src/clients/middleware.ts b/packages/b2c-tooling-sdk/src/clients/middleware.ts index 29bf37d..df3a818 100644 --- a/packages/b2c-tooling-sdk/src/clients/middleware.ts +++ b/packages/b2c-tooling-sdk/src/clients/middleware.ts @@ -143,7 +143,7 @@ export function createLoggingMiddleware(config?: string | LoggingMiddlewareConfi // Mask sensitive/large body keys before logging const maskedBody = maskBody(body, maskBodyKeys); logger.trace( - {headers: headersToObject(request.headers), body: maskedBody}, + {method: request.method, url, headers: headersToObject(request.headers), body: maskedBody}, `${reqTag} ${request.method} ${url} body`, ); @@ -178,7 +178,7 @@ export function createLoggingMiddleware(config?: string | LoggingMiddlewareConfi // Mask sensitive/large body keys before logging const maskedResponseBody = maskBody(responseBody, maskBodyKeys); logger.trace( - {headers: headersToObject(response.headers), body: maskedResponseBody}, + {method: request.method, url, headers: headersToObject(response.headers), body: maskedResponseBody}, `${respTag} ${request.method} ${url} body`, ); diff --git a/packages/b2c-tooling-sdk/src/clients/webdav.ts b/packages/b2c-tooling-sdk/src/clients/webdav.ts index dac2d4e..06103e4 100644 --- a/packages/b2c-tooling-sdk/src/clients/webdav.ts +++ b/packages/b2c-tooling-sdk/src/clients/webdav.ts @@ -143,7 +143,12 @@ export class WebDavClient { // Trace: Log request details logger.trace( - {headers: this.headersToObject(request.headers), body: this.formatBody(init?.body)}, + { + method: request.method, + url: request.url, + headers: this.headersToObject(request.headers), + body: this.formatBody(init?.body), + }, `[WebDAV REQ BODY] ${request.method} ${request.url}`, ); @@ -188,7 +193,10 @@ export class WebDavClient { const clonedResponse = response.clone(); responseBody = await clonedResponse.text(); } - logger.trace({headers: responseHeaders, body: responseBody}, `[WebDAV RESP BODY] ${request.method} ${request.url}`); + logger.trace( + {method: request.method, url: request.url, headers: responseHeaders, body: responseBody}, + `[WebDAV RESP BODY] ${request.method} ${request.url}`, + ); return response; } diff --git a/packages/b2c-tooling-sdk/src/config/dw-json.ts b/packages/b2c-tooling-sdk/src/config/dw-json.ts index ef70a1a..f190fdb 100644 --- a/packages/b2c-tooling-sdk/src/config/dw-json.ts +++ b/packages/b2c-tooling-sdk/src/config/dw-json.ts @@ -14,6 +14,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type {AuthMethod} from '../auth/types.js'; +import {getLogger} from '../logging/logger.js'; /** * Configuration structure matching dw.json file format. @@ -52,6 +53,8 @@ export interface DwJsonConfig { 'secure-server'?: string; /** Allowed authentication methods in priority order */ 'auth-methods'?: AuthMethod[]; + /** Account Manager hostname for OAuth */ + 'account-manager-host'?: string; /** MRT project slug */ mrtProject?: string; /** MRT environment name (e.g., staging, production) */ @@ -78,6 +81,16 @@ export interface LoadDwJsonOptions { startDir?: string; } +/** + * Result of loading dw.json configuration. + */ +export interface LoadDwJsonResult { + /** The parsed configuration */ + config: DwJsonConfig; + /** The path to the dw.json file that was loaded */ + path: string; +} + /** * Finds dw.json by searching upward from the starting directory. * @@ -113,9 +126,15 @@ export function findDwJson(startDir: string = process.cwd()): string | undefined * 2. Config marked as `active: true` * 3. Root-level config */ -function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonConfig { +function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonConfig | undefined { + const logger = getLogger(); + // Single config or no configs array if (!Array.isArray(json.configs) || json.configs.length === 0) { + logger.trace( + {selection: 'root', instanceName: json.name}, + `[DwJsonSource] Selected config "${json.name ?? 'root'}" (single config)`, + ); return json; } @@ -123,14 +142,21 @@ function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonCon if (instanceName) { // Check root first if (json.name === instanceName) { + logger.trace( + {selection: 'named', instanceName}, + `[DwJsonSource] Selected config "${instanceName}" by name (root)`, + ); return json; } // Then check configs array const found = json.configs.find((c) => c.name === instanceName); if (found) { + logger.trace({selection: 'named', instanceName}, `[DwJsonSource] Selected config "${instanceName}" by name`); return found; } - // Instance not found, fall through to other selection methods + // Instance explicitly requested but not found - return undefined + logger.trace({requestedInstance: instanceName}, `[DwJsonSource] Named instance "${instanceName}" not found`); + return undefined; } // Find active config @@ -138,11 +164,19 @@ function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonCon // Root is inactive, look for active in configs const activeConfig = json.configs.find((c) => c.active === true); if (activeConfig) { + logger.trace( + {selection: 'active', instanceName: activeConfig.name}, + `[DwJsonSource] Selected config "${activeConfig.name}" by active flag`, + ); return activeConfig; } } // Default to root config + logger.trace( + {selection: 'root', instanceName: json.name}, + `[DwJsonSource] Selected config "${json.name ?? 'root'}" (default to root)`, + ); return json; } @@ -155,35 +189,53 @@ function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonCon * Use `findDwJson()` if you need to search upward through parent directories. * * @param options - Loading options - * @returns The parsed config, or undefined if no dw.json found + * @returns The parsed config with its path, or undefined if no dw.json found * * @example * // Load from ./dw.json (current directory) - * const config = loadDwJson(); + * const result = loadDwJson(); + * if (result) { + * console.log(`Loaded from ${result.path}`); + * console.log(result.config.hostname); + * } * * // Load from specific directory - * const config = loadDwJson({ startDir: '/path/to/project' }); + * const result = loadDwJson({ startDir: '/path/to/project' }); * * // Use named instance - * const config = loadDwJson({ instance: 'staging' }); + * const result = loadDwJson({ instance: 'staging' }); * * // Explicit path - * const config = loadDwJson({ path: './config/dw.json' }); + * const result = loadDwJson({ path: './config/dw.json' }); */ -export function loadDwJson(options: LoadDwJsonOptions = {}): DwJsonConfig | undefined { +export function loadDwJson(options: LoadDwJsonOptions = {}): LoadDwJsonResult | undefined { + const logger = getLogger(); + // If explicit path provided, use it. Otherwise default to ./dw.json (no upward search) const dwJsonPath = options.path ?? path.join(options.startDir || process.cwd(), 'dw.json'); + logger.trace({path: dwJsonPath}, '[DwJsonSource] Checking for config file'); + if (!fs.existsSync(dwJsonPath)) { + logger.trace({path: dwJsonPath}, '[DwJsonSource] No config file found'); return undefined; } try { const content = fs.readFileSync(dwJsonPath, 'utf8'); const json = JSON.parse(content) as DwJsonMultiConfig; - return selectConfig(json, options.instance); - } catch { + const config = selectConfig(json, options.instance); + if (!config) { + return undefined; + } + return { + config, + path: dwJsonPath, + }; + } catch (error) { // Invalid JSON or read error + const message = error instanceof Error ? error.message : String(error); + logger.trace({path: dwJsonPath, error: message}, '[DwJsonSource] Failed to parse config file'); return undefined; } } diff --git a/packages/b2c-tooling-sdk/src/config/index.ts b/packages/b2c-tooling-sdk/src/config/index.ts index 4c3549d..369486e 100644 --- a/packages/b2c-tooling-sdk/src/config/index.ts +++ b/packages/b2c-tooling-sdk/src/config/index.ts @@ -66,11 +66,16 @@ * Implement the {@link ConfigSource} interface to create custom sources: * * ```typescript - * import { ConfigResolver, type ConfigSource } from '@salesforce/b2c-tooling-sdk/config'; + * import { ConfigResolver, type ConfigSource, type ConfigLoadResult } from '@salesforce/b2c-tooling-sdk/config'; * * class MySource implements ConfigSource { * name = 'my-source'; - * load(options) { return { hostname: 'custom.example.com' }; } + * load(options): ConfigLoadResult | undefined { + * return { + * config: { hostname: 'custom.example.com' }, + * location: '/path/to/source', + * }; + * } * } * * const resolver = new ConfigResolver([new MySource()]); @@ -97,6 +102,7 @@ export {resolveConfig, ConfigResolver, createConfigResolver} from './resolver.js export type { NormalizedConfig, ConfigSource, + ConfigLoadResult, ConfigSourceInfo, ConfigResolutionResult, ConfigWarning, @@ -107,16 +113,9 @@ export type { CreateMrtClientOptions, } from './types.js'; -// Mapping utilities -export { - mapDwJsonToNormalizedConfig, - mergeConfigsWithProtection, - getPopulatedFields, - buildAuthConfigFromNormalized, - createInstanceFromConfig, -} from './mapping.js'; -export type {MergeConfigOptions, MergeConfigResult} from './mapping.js'; +// Instance creation utility (public API for CLI commands) +export {createInstanceFromConfig} from './mapping.js'; // Low-level dw.json API (still available for advanced use) export {loadDwJson, findDwJson} from './dw-json.js'; -export type {DwJsonConfig, DwJsonMultiConfig, LoadDwJsonOptions} from './dw-json.js'; +export type {DwJsonConfig, DwJsonMultiConfig, LoadDwJsonOptions, LoadDwJsonResult} from './dw-json.js'; diff --git a/packages/b2c-tooling-sdk/src/config/mapping.ts b/packages/b2c-tooling-sdk/src/config/mapping.ts index abc1208..8fb47ff 100644 --- a/packages/b2c-tooling-sdk/src/config/mapping.ts +++ b/packages/b2c-tooling-sdk/src/config/mapping.ts @@ -51,6 +51,7 @@ export function mapDwJsonToNormalizedConfig(json: DwJsonConfig): NormalizedConfi shortCode: json.shortCode || json['short-code'] || json['scapi-shortcode'], instanceName: json.name, authMethods: json['auth-methods'], + accountManagerHost: json['account-manager-host'], mrtProject: json.mrtProject, mrtEnvironment: json.mrtEnvironment, }; diff --git a/packages/b2c-tooling-sdk/src/config/resolver.ts b/packages/b2c-tooling-sdk/src/config/resolver.ts index fd8fed0..fbbc88c 100644 --- a/packages/b2c-tooling-sdk/src/config/resolver.ts +++ b/packages/b2c-tooling-sdk/src/config/resolver.ts @@ -154,36 +154,60 @@ export class ConfigResolver { const sourceInfos: ConfigSourceInfo[] = []; const baseConfig: NormalizedConfig = {}; + // Create enriched options that will be updated with accumulated config values. + // This allows later sources (like plugins) to use values discovered by earlier sources (like dw.json). + // CLI-provided options always take precedence over accumulated values. + const enrichedOptions: ResolveConfigOptions = {...options}; + // Load from each source in order, merging results // Earlier sources have higher priority - later sources only fill in missing values for (const source of this.sources) { - const sourceConfig = source.load(options); - if (sourceConfig) { - const fieldsContributed = getPopulatedFields(sourceConfig); - if (fieldsContributed.length > 0) { - sourceInfos.push({ - name: source.name, - path: source.getPath?.(), - fieldsContributed, - }); - + const result = source.load(enrichedOptions); + if (result) { + const {config: sourceConfig, location} = result; + const fields = getPopulatedFields(sourceConfig); + if (fields.length > 0) { // Capture which credential groups are already claimed BEFORE processing this source // This allows a single source to provide complete credential pairs const claimedGroups = getClaimedCredentialGroups(baseConfig); + // Track which fields are ignored during merge + const fieldsIgnored: (keyof NormalizedConfig)[] = []; + // Merge: source values fill in gaps (don't override existing values) for (const [key, value] of Object.entries(sourceConfig)) { if (value === undefined) continue; - if (baseConfig[key as keyof NormalizedConfig] !== undefined) continue; + + const fieldKey = key as keyof NormalizedConfig; + + // Skip if already set by higher-priority source + if (baseConfig[fieldKey] !== undefined) { + fieldsIgnored.push(fieldKey); + continue; + } // Skip if this field's credential group was already claimed by a higher-priority source // This prevents mixing credentials from different sources if (isFieldInClaimedGroup(key, claimedGroups)) { + fieldsIgnored.push(fieldKey); continue; } (baseConfig as Record)[key] = value; } + + sourceInfos.push({ + name: source.name, + location, + fields, + fieldsIgnored: fieldsIgnored.length > 0 ? fieldsIgnored : undefined, + }); + + // Enrich options with accumulated config values for subsequent sources. + // Only set if not already provided via CLI options. + if (!enrichedOptions.accountManagerHost && baseConfig.accountManagerHost) { + enrichedOptions.accountManagerHost = baseConfig.accountManagerHost; + } } } } @@ -332,24 +356,18 @@ export function resolveConfig( // Build sources list with priority ordering: // 1. sourcesBefore (high priority - override defaults) // 2. default sources (dw.json, ~/.mobify) - // 3. sourcesAfter / sources (low priority - fill gaps) + // 3. sourcesAfter (low priority - fill gaps) let sources: ConfigSource[]; - if (options.replaceDefaultSources && (options.sources || options.sourcesAfter)) { - // Replace mode: only use provided sources - sources = [...(options.sourcesBefore ?? []), ...(options.sourcesAfter ?? options.sources ?? [])]; + if (options.replaceDefaultSources) { + // Replace mode: only use provided sources (no default dw.json/~/.mobify) + sources = [...(options.sourcesBefore ?? []), ...(options.sourcesAfter ?? [])]; } else { // Normal mode: before + defaults + after const defaultSources: ConfigSource[] = [new DwJsonSource(), new MobifySource()]; - // Combine: sourcesBefore > defaults > sourcesAfter/sources - sources = [ - ...(options.sourcesBefore ?? []), - ...defaultSources, - ...(options.sourcesAfter ?? []), - // Backward compat: 'sources' is treated as 'after' priority - ...(options.sources ?? []), - ]; + // Combine: sourcesBefore > defaults > sourcesAfter + sources = [...(options.sourcesBefore ?? []), ...defaultSources, ...(options.sourcesAfter ?? [])]; } const resolver = new ConfigResolver(sources); diff --git a/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts b/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts index 2fa07ea..7dca084 100644 --- a/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts +++ b/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts @@ -8,10 +8,11 @@ * * @internal This module is internal to the SDK. Use ConfigResolver instead. */ -import * as path from 'node:path'; import {loadDwJson} from '../dw-json.js'; +import {getPopulatedFields} from '../mapping.js'; import {mapDwJsonToNormalizedConfig} from '../mapping.js'; -import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '../types.js'; +import type {ConfigSource, ConfigLoadResult, ResolveConfigOptions} from '../types.js'; +import {getLogger} from '../../logging/logger.js'; /** * Configuration source that loads from dw.json files. @@ -19,28 +20,26 @@ import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '../type * @internal */ export class DwJsonSource implements ConfigSource { - readonly name = 'dw.json'; - private lastPath?: string; + readonly name = 'DwJsonSource'; - load(options: ResolveConfigOptions): NormalizedConfig | undefined { - const dwConfig = loadDwJson({ + load(options: ResolveConfigOptions): ConfigLoadResult | undefined { + const logger = getLogger(); + + const result = loadDwJson({ instance: options.instance, path: options.configPath, startDir: options.startDir, }); - if (!dwConfig) { - this.lastPath = undefined; + if (!result) { return undefined; } - // Track the path for diagnostics - use explicit path or default location - this.lastPath = options.configPath ?? path.join(options.startDir || process.cwd(), 'dw.json'); + const config = mapDwJsonToNormalizedConfig(result.config); + const fields = getPopulatedFields(config); - return mapDwJsonToNormalizedConfig(dwConfig); - } + logger.trace({location: result.path, fields}, '[DwJsonSource] Loaded config'); - getPath(): string | undefined { - return this.lastPath; + return {config, location: result.path}; } } diff --git a/packages/b2c-tooling-sdk/src/config/sources/mobify-source.ts b/packages/b2c-tooling-sdk/src/config/sources/mobify-source.ts index 8ddb255..d148523 100644 --- a/packages/b2c-tooling-sdk/src/config/sources/mobify-source.ts +++ b/packages/b2c-tooling-sdk/src/config/sources/mobify-source.ts @@ -11,7 +11,8 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '../types.js'; +import type {ConfigSource, ConfigLoadResult, ResolveConfigOptions} from '../types.js'; +import {getLogger} from '../../logging/logger.js'; /** * Mobify config file structure (~/.mobify) @@ -37,15 +38,18 @@ interface MobifyConfigFile { * @internal */ export class MobifySource implements ConfigSource { - readonly name = 'mobify'; - private lastPath?: string; + readonly name = 'MobifySource'; + + load(options: ResolveConfigOptions): ConfigLoadResult | undefined { + const logger = getLogger(); - load(options: ResolveConfigOptions): NormalizedConfig | undefined { // Use explicit credentialsFile if provided, otherwise use default path const mobifyPath = options.credentialsFile ?? this.getMobifyPath(options.cloudOrigin); - this.lastPath = mobifyPath; + + logger.trace({location: mobifyPath}, '[MobifySource] Checking for credentials file'); if (!fs.existsSync(mobifyPath)) { + logger.trace({location: mobifyPath}, '[MobifySource] No credentials file found'); return undefined; } @@ -54,22 +58,24 @@ export class MobifySource implements ConfigSource { const config = JSON.parse(content) as MobifyConfigFile; if (!config.api_key) { + logger.trace({location: mobifyPath}, '[MobifySource] Credentials file found but no api_key present'); return undefined; } + logger.trace({location: mobifyPath, fields: ['mrtApiKey']}, '[MobifySource] Loaded credentials'); + return { - mrtApiKey: config.api_key, + config: {mrtApiKey: config.api_key}, + location: mobifyPath, }; - } catch { + } catch (error) { // Invalid JSON or read error + const message = error instanceof Error ? error.message : String(error); + logger.trace({location: mobifyPath, error: message}, '[MobifySource] Failed to parse credentials file'); return undefined; } } - getPath(): string | undefined { - return this.lastPath; - } - /** * Determines the mobify config file path based on cloud origin. */ diff --git a/packages/b2c-tooling-sdk/src/config/types.ts b/packages/b2c-tooling-sdk/src/config/types.ts index 82ea45a..b154723 100644 --- a/packages/b2c-tooling-sdk/src/config/types.ts +++ b/packages/b2c-tooling-sdk/src/config/types.ts @@ -64,7 +64,7 @@ export interface NormalizedConfig { mrtOrigin?: string; // Metadata - /** Instance name (from multi-config dw.json) */ + /** Instance name (from multi-config supporting sources) */ instanceName?: string; } @@ -91,10 +91,12 @@ export interface ConfigWarning { export interface ConfigSourceInfo { /** Human-readable name of the source */ name: string; - /** Path to the source file (if applicable) */ - path?: string; - /** Fields that this source contributed to the final config */ - fieldsContributed: (keyof NormalizedConfig)[]; + /** Location of the source (file path, keychain entry, URL, etc.) */ + location?: string; + /** All fields that this source provided values for */ + fields: (keyof NormalizedConfig)[]; + /** Fields that were not used because a higher priority source already provided them */ + fieldsIgnored?: (keyof NormalizedConfig)[]; } /** @@ -113,7 +115,7 @@ export interface ConfigResolutionResult { * Options for configuration resolution. */ export interface ResolveConfigOptions { - /** Named instance from dw.json "configs" array */ + /** Named instance for supporting ConfigSources */ instance?: string; /** Explicit path to config file (defaults to auto-discover) */ configPath?: string; @@ -125,6 +127,8 @@ export interface ResolveConfigOptions { cloudOrigin?: string; /** Path to custom MRT credentials file (overrides default ~/.mobify) */ credentialsFile?: string; + /** Account Manager hostname for OAuth (passed to plugins for host-specific config) */ + accountManagerHost?: string; /** * Custom sources to add BEFORE default sources (higher priority). @@ -138,16 +142,23 @@ export interface ResolveConfigOptions { */ sourcesAfter?: ConfigSource[]; - /** - * Custom configuration sources (added after default sources). - * @deprecated Use `sourcesAfter` for clarity. This is kept for backward compatibility. - */ - sources?: ConfigSource[]; - /** Replace default sources entirely (instead of appending) */ replaceDefaultSources?: boolean; } +/** + * Result of loading configuration from a source. + */ +export interface ConfigLoadResult { + /** The loaded configuration */ + config: NormalizedConfig; + /** + * Location of the source (for diagnostics). + * May be a file path, keychain entry, URL, or other identifier. + */ + location?: string; +} + /** * A configuration source that can contribute config values. * @@ -156,14 +167,14 @@ export interface ResolveConfigOptions { * * @example * ```typescript - * import type { ConfigSource, NormalizedConfig, ResolveConfigOptions } from '@salesforce/b2c-tooling-sdk/config'; + * import type { ConfigSource, ConfigLoadResult, ResolveConfigOptions } from '@salesforce/b2c-tooling-sdk/config'; * * class MyCustomSource implements ConfigSource { * name = 'my-custom-source'; * - * load(options: ResolveConfigOptions): NormalizedConfig | undefined { + * load(options: ResolveConfigOptions): ConfigLoadResult | undefined { * // Load config from your custom source - * return { hostname: 'example.com' }; + * return { config: { hostname: 'example.com' }, location: '/path/to/config' }; * } * } * ``` @@ -176,15 +187,9 @@ export interface ConfigSource { * Load configuration from this source. * * @param options - Resolution options - * @returns Partial config from this source, or undefined if source not available - */ - load(options: ResolveConfigOptions): NormalizedConfig | undefined; - - /** - * Get the path to this source's file (if applicable). - * Used for diagnostics and source info. + * @returns Config and location from this source, or undefined if source not available */ - getPath?(): string | undefined; + load(options: ResolveConfigOptions): ConfigLoadResult | undefined; } /** diff --git a/packages/b2c-tooling-sdk/src/operations/code/deploy.ts b/packages/b2c-tooling-sdk/src/operations/code/deploy.ts index 45af88c..d936670 100644 --- a/packages/b2c-tooling-sdk/src/operations/code/deploy.ts +++ b/packages/b2c-tooling-sdk/src/operations/code/deploy.ts @@ -92,10 +92,10 @@ export async function deleteCartridges(instance: B2CInstance, cartridges: Cartri const cartridgePath = `Cartridges/${codeVersion}/${c.dest}`; try { await webdav.delete(cartridgePath); - logger.debug({cartridge: c.dest}, `Deleted ${cartridgePath}`); + logger.debug({cartridgeName: c.dest, path: cartridgePath}, `Deleted ${cartridgePath}`); } catch { // Ignore errors - cartridge may not exist - logger.debug({cartridge: c.dest}, `Could not delete ${cartridgePath} (may not exist)`); + logger.debug({cartridgeName: c.dest, path: cartridgePath}, `Could not delete ${cartridgePath} (may not exist)`); } } } @@ -178,7 +178,7 @@ export async function uploadCartridges(instance: B2CInstance, cartridges: Cartri logger.debug('Temporary archive deleted'); logger.debug( - {hostname: instance.config.hostname, codeVersion, cartridgeCount: cartridges.length}, + {server: instance.config.hostname, codeVersion, cartridgeCount: cartridges.length}, `Uploaded ${cartridges.length} cartridges to ${instance.config.hostname}`, ); } @@ -243,7 +243,7 @@ export async function findAndDeployCartridges( logger.debug({count: cartridges.length}, `Found ${cartridges.length} cartridge(s)`); for (const c of cartridges) { - logger.debug({cartridge: c.name, path: c.src}, ` ${c.name}`); + logger.debug({cartridgeName: c.name, path: c.src}, ` ${c.name}`); } // Optionally delete existing cartridges first diff --git a/packages/b2c-tooling-sdk/src/operations/code/versions.ts b/packages/b2c-tooling-sdk/src/operations/code/versions.ts index d3f5f67..052be7f 100644 --- a/packages/b2c-tooling-sdk/src/operations/code/versions.ts +++ b/packages/b2c-tooling-sdk/src/operations/code/versions.ts @@ -116,7 +116,7 @@ export async function reloadCodeVersion(instance: B2CInstance, codeVersionId?: s throw new Error('No code version specified and no active version found'); } - logger.debug({targetVersion}, `Reloading code version ${targetVersion}`); + logger.debug({codeVersionId: targetVersion}, `Reloading code version ${targetVersion}`); // If the target is already active, we need to toggle to another version first if (activeVersion?.id === targetVersion) { @@ -125,13 +125,13 @@ export async function reloadCodeVersion(instance: B2CInstance, codeVersionId?: s throw new Error('Cannot reload: no alternate code version available for toggle'); } - logger.debug({alternateVersion: alternateVersion.id}, `Temporarily activating ${alternateVersion.id}`); + logger.debug({codeVersionId: alternateVersion.id}, `Temporarily activating ${alternateVersion.id}`); await activateCodeVersion(instance, alternateVersion.id!); } // Now activate the target version await activateCodeVersion(instance, targetVersion); - logger.debug({targetVersion}, `Code version ${targetVersion} reloaded`); + logger.debug({codeVersionId: targetVersion}, `Code version ${targetVersion} reloaded`); } /** diff --git a/packages/b2c-tooling-sdk/src/operations/code/watch.ts b/packages/b2c-tooling-sdk/src/operations/code/watch.ts index b0939b4..b2d39bf 100644 --- a/packages/b2c-tooling-sdk/src/operations/code/watch.ts +++ b/packages/b2c-tooling-sdk/src/operations/code/watch.ts @@ -145,7 +145,7 @@ export async function watchCartridges( logger.debug({count: cartridges.length}, `Watching ${cartridges.length} cartridge(s)`); for (const c of cartridges) { - logger.info({cartridge: c.name, path: c.src}, ` ${c.name}`); + logger.info({cartridgeName: c.name, path: c.src}, ` ${c.name}`); } const webdav = instance.webdav; @@ -229,7 +229,7 @@ export async function watchCartridges( await webdav.delete(uploadPath); logger.debug( - {fileCount: validUploadFiles.length, hostname: instance.config.hostname}, + {fileCount: validUploadFiles.length, server: instance.config.hostname}, `Uploaded ${validUploadFiles.length} file(s)`, ); @@ -253,9 +253,9 @@ export async function watchCartridges( const deletePath = `${webdavLocation}/${f.dest}`; try { await webdav.delete(deletePath); - logger.info({file: deletePath}, `Deleted: ${deletePath}`); + logger.info({path: deletePath}, `Deleted: ${deletePath}`); } catch (error) { - logger.debug({file: deletePath, error}, `Failed to delete ${deletePath}`); + logger.debug({path: deletePath, error}, `Failed to delete ${deletePath}`); } } @@ -291,7 +291,7 @@ export async function watchCartridges( options.onError?.(error); }); - logger.debug({hostname: instance.config.hostname, codeVersion}, 'Watching for changes...'); + logger.debug({server: instance.config.hostname, codeVersion}, 'Watching for changes...'); return { watcher, diff --git a/packages/b2c-tooling-sdk/src/operations/jobs/run.ts b/packages/b2c-tooling-sdk/src/operations/jobs/run.ts index bb7d5b0..d6a1cba 100644 --- a/packages/b2c-tooling-sdk/src/operations/jobs/run.ts +++ b/packages/b2c-tooling-sdk/src/operations/jobs/run.ts @@ -117,12 +117,12 @@ export async function executeJob( const errorBody = await response.text().catch(() => ''); if (errorBody.includes('JobAlreadyRunningException')) { if (waitForRunning) { - logger.warn(`Job ${jobId} already running, waiting for it to finish...`); + logger.warn({jobId}, `Job ${jobId} already running, waiting for it to finish...`); // Search for the running execution const runningExecution = await findRunningJobExecution(instance, jobId); if (runningExecution) { - logger.debug({executionId: runningExecution.id}, `Found running execution ${runningExecution.id}`); + logger.debug({jobId, executionId: runningExecution.id}, `Found running execution ${runningExecution.id}`); await waitForJob(instance, jobId, runningExecution.id!); // Retry execution after the running job finishes return executeJob(instance, jobId, {...options, waitForRunning: false}); @@ -139,7 +139,7 @@ export async function executeJob( throw new Error(message); } - logger.debug({executionId: data.id, status: data.execution_status}, `Job ${jobId} started: ${data.id}`); + logger.debug({jobId, executionId: data.id, status: data.execution_status}, `Job ${jobId} started: ${data.id}`); return data; } @@ -231,14 +231,14 @@ export async function waitForJob( // Check for terminal states if (execution.execution_status === 'aborted' || execution.exit_status?.code === 'ERROR') { - logger.debug({execution}, `Job ${jobId} failed`); + logger.debug({jobId, executionId, execution}, `Job ${jobId} failed`); throw new JobExecutionError(`Job ${jobId} failed`, execution); } if (execution.execution_status === 'finished') { const durationSec = (execution.duration ?? 0) / 1000; logger.debug( - {executionId, status: execution.exit_status?.code, duration: durationSec}, + {jobId, executionId, status: execution.exit_status?.code, duration: durationSec}, `Job ${jobId} finished. Status: ${execution.exit_status?.code} (duration: ${durationSec}s)`, ); return execution; @@ -247,7 +247,7 @@ export async function waitForJob( // Log periodic updates if (ticks % 5 === 0) { logger.debug( - {executionId, status: execution.execution_status, elapsed: elapsed / 1000}, + {jobId, executionId, status: execution.execution_status, elapsed: elapsed / 1000}, `Waiting for job ${jobId} to finish (${(elapsed / 1000).toFixed(0)}s elapsed)...`, ); } diff --git a/packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts b/packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts index 7cb6f94..e4254d1 100644 --- a/packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts +++ b/packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts @@ -120,7 +120,7 @@ export async function siteArchiveImport( const archiveDirName = archiveName || `import-${timestamp}`; zipFilename = `${archiveDirName}.zip`; - logger.debug({directory: targetPath}, `Creating archive from directory: ${targetPath}`); + logger.debug({path: targetPath}, `Creating archive from directory: ${targetPath}`); archiveContent = await createArchiveFromDirectory(targetPath, archiveDirName); } else { throw new Error(`Target must be a file or directory: ${targetPath}`); @@ -133,11 +133,14 @@ export async function siteArchiveImport( if (needsUpload && archiveContent) { logger.debug({path: uploadPath}, `Uploading archive to ${uploadPath}`); await instance.webdav.put(uploadPath, archiveContent as Buffer, 'application/zip'); - logger.debug(`Archive uploaded: ${uploadPath}`); + logger.debug({path: uploadPath}, `Archive uploaded: ${uploadPath}`); } // Execute the import job with file_name parameter - logger.debug(`Executing ${IMPORT_JOB_ID} job with file_name: ${zipFilename}`); + logger.debug( + {jobId: IMPORT_JOB_ID, file: zipFilename}, + `Executing ${IMPORT_JOB_ID} job with file_name: ${zipFilename}`, + ); let execution: JobExecution; @@ -167,7 +170,7 @@ export async function siteArchiveImport( execution = data; } - logger.debug({executionId: execution.id}, `Import job started: ${execution.id}`); + logger.debug({jobId: IMPORT_JOB_ID, executionId: execution.id}, `Import job started: ${execution.id}`); // Wait for completion try { @@ -177,9 +180,9 @@ export async function siteArchiveImport( // Try to get log file try { const log = await getJobLog(instance, error.execution); - logger.error({logFile: error.execution.log_file_path}, `Job log:\n${log}`); + logger.error({jobId: IMPORT_JOB_ID, logFile: error.execution.log_file_path, log}, `Job log:\n${log}`); } catch { - logger.error('Could not retrieve job log'); + logger.error({jobId: IMPORT_JOB_ID}, 'Could not retrieve job log'); } } throw error; @@ -188,7 +191,7 @@ export async function siteArchiveImport( // Clean up archive if not keeping if (!keepArchive && needsUpload) { await instance.webdav.delete(uploadPath); - logger.debug(`Archive deleted: ${uploadPath}`); + logger.debug({path: uploadPath}, `Archive deleted: ${uploadPath}`); } return { @@ -389,8 +392,7 @@ export async function siteArchiveExport( const zipFilename = `${archiveDirName}.zip`; const webdavPath = `Impex/src/instance/${zipFilename}`; - logger.debug(`Executing ${EXPORT_JOB_ID} job`); - logger.debug({dataUnits}, 'Export data units'); + logger.debug({jobId: EXPORT_JOB_ID, dataUnits}, `Executing ${EXPORT_JOB_ID} job`); let execution: JobExecution; @@ -430,7 +432,7 @@ export async function siteArchiveExport( execution = data; } - logger.debug({executionId: execution.id}, `Export job started: ${execution.id}`); + logger.debug({jobId: EXPORT_JOB_ID, executionId: execution.id}, `Export job started: ${execution.id}`); // Wait for completion try { @@ -440,22 +442,22 @@ export async function siteArchiveExport( // Try to get log file try { const log = await getJobLog(instance, error.execution); - logger.error({logFile: error.execution.log_file_path}, `Job log:\n${log}`); + logger.error({jobId: EXPORT_JOB_ID, logFile: error.execution.log_file_path, log}, `Job log:\n${log}`); } catch { - logger.error('Could not retrieve job log'); + logger.error({jobId: EXPORT_JOB_ID}, 'Could not retrieve job log'); } } throw error; } // Download archive - logger.debug(`Downloading archive: ${webdavPath}`); + logger.debug({path: webdavPath}, `Downloading archive: ${webdavPath}`); const archiveData = await instance.webdav.get(webdavPath); // Clean up if not keeping if (!keepArchive) { await instance.webdav.delete(webdavPath); - logger.debug(`Archive deleted: ${webdavPath}`); + logger.debug({path: webdavPath}, `Archive deleted: ${webdavPath}`); } return { @@ -510,7 +512,7 @@ export async function siteArchiveExportToPath( await fs.promises.mkdir(path.dirname(zipPath), {recursive: true}); await fs.promises.writeFile(zipPath, result.data); - logger.debug(`Archive saved to: ${zipPath}`); + logger.debug({path: zipPath}, `Archive saved to: ${zipPath}`); return { ...result, @@ -535,7 +537,7 @@ export async function siteArchiveExportToPath( } } - logger.debug(`Archive extracted to: ${outputPath}`); + logger.debug({path: outputPath}, `Archive extracted to: ${outputPath}`); return { ...result, diff --git a/packages/b2c-tooling-sdk/test/cli/config.test.ts b/packages/b2c-tooling-sdk/test/cli/config.test.ts index edcb923..d71a9ed 100644 --- a/packages/b2c-tooling-sdk/test/cli/config.test.ts +++ b/packages/b2c-tooling-sdk/test/cli/config.test.ts @@ -4,13 +4,8 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ import {expect} from 'chai'; -import { - loadConfig, - type ResolvedConfig, - type LoadConfigOptions, - type PluginSources, -} from '@salesforce/b2c-tooling-sdk/cli'; -import type {ConfigSource} from '@salesforce/b2c-tooling-sdk/config'; +import {loadConfig, type LoadConfigOptions, type PluginSources} from '@salesforce/b2c-tooling-sdk/cli'; +import type {ConfigSource, ConfigLoadResult, NormalizedConfig} from '@salesforce/b2c-tooling-sdk/config'; /** * Mock config source for testing. @@ -18,45 +13,44 @@ import type {ConfigSource} from '@salesforce/b2c-tooling-sdk/config'; class MockConfigSource implements ConfigSource { constructor( public name: string, - private config: Partial | undefined, - private path?: string, + private config: Partial | undefined, + private location?: string, ) {} - load() { - return this.config as ResolvedConfig | undefined; - } - - getPath(): string | undefined { - return this.path; + load(): ConfigLoadResult | undefined { + if (this.config === undefined) { + return undefined; + } + return {config: this.config as NormalizedConfig, location: this.location}; } } describe('cli/config', () => { describe('loadConfig', () => { it('loads config from flags only', () => { - const flags: Partial = { + const flags: Partial = { hostname: 'test.demandware.net', codeVersion: 'v1', }; const config = loadConfig(flags); - expect(config.hostname).to.equal('test.demandware.net'); - expect(config.codeVersion).to.equal('v1'); + expect(config.values.hostname).to.equal('test.demandware.net'); + expect(config.values.codeVersion).to.equal('v1'); }); it('merges flags with config file sources', () => { - const flags: Partial = { + const flags: Partial = { hostname: 'flag-hostname.demandware.net', }; // loadConfig uses resolveConfig internally which will try to load from dw.json // In test environment, this may not exist, so we test with flags only const config = loadConfig(flags); - expect(config.hostname).to.equal('flag-hostname.demandware.net'); + expect(config.values.hostname).to.equal('flag-hostname.demandware.net'); }); it('handles instance option', () => { - const flags: Partial = {}; + const flags: Partial = {}; const options: LoadConfigOptions = { instance: 'test-instance', }; @@ -67,7 +61,7 @@ describe('cli/config', () => { }); it('handles configPath option', () => { - const flags: Partial = {}; + const flags: Partial = {}; const options: LoadConfigOptions = { configPath: '/custom/path/dw.json', }; @@ -77,7 +71,7 @@ describe('cli/config', () => { }); it('handles cloudOrigin option', () => { - const flags: Partial = {}; + const flags: Partial = {}; const options: LoadConfigOptions = { cloudOrigin: 'https://cloud-staging.mobify.com', }; @@ -87,7 +81,7 @@ describe('cli/config', () => { }); it('merges plugin sources before defaults', () => { - const flags: Partial = { + const flags: Partial = { hostname: 'flag-hostname.demandware.net', }; const beforeSource = new MockConfigSource('before', { @@ -104,7 +98,7 @@ describe('cli/config', () => { }); it('merges plugin sources after defaults', () => { - const flags: Partial = { + const flags: Partial = { hostname: 'flag-hostname.demandware.net', }; const afterSource = new MockConfigSource('after', { @@ -125,33 +119,33 @@ describe('cli/config', () => { }); it('handles empty options', () => { - const flags: Partial = { + const flags: Partial = { hostname: 'test.demandware.net', }; const config = loadConfig(flags, {}); - expect(config.hostname).to.equal('test.demandware.net'); + expect(config.values.hostname).to.equal('test.demandware.net'); }); it('handles empty plugin sources', () => { - const flags: Partial = { + const flags: Partial = { hostname: 'test.demandware.net', }; const config = loadConfig(flags, {}, {}); - expect(config.hostname).to.equal('test.demandware.net'); + expect(config.values.hostname).to.equal('test.demandware.net'); }); it('preserves instanceName from options when not in resolved config', () => { - const flags: Partial = {}; + const flags: Partial = {}; const options: LoadConfigOptions = { instance: 'custom-instance', }; const config = loadConfig(flags, options); - expect(config.instanceName).to.equal('custom-instance'); + expect(config.values.instanceName).to.equal('custom-instance'); }); it('does not override instanceName if already in resolved config', () => { - const flags: Partial = { + const flags: Partial = { instanceName: 'resolved-instance', }; const options: LoadConfigOptions = { @@ -160,11 +154,11 @@ describe('cli/config', () => { const config = loadConfig(flags, options); // Flags take precedence - expect(config.instanceName).to.equal('resolved-instance'); + expect(config.values.instanceName).to.equal('resolved-instance'); }); it('handles multiple plugin sources with priority', () => { - const flags: Partial = { + const flags: Partial = { hostname: 'flag-hostname.demandware.net', }; const beforeSource1 = new MockConfigSource('before1', {codeVersion: 'v1'}); diff --git a/packages/b2c-tooling-sdk/test/config/dw-json.test.ts b/packages/b2c-tooling-sdk/test/config/dw-json.test.ts index 0c5e815..e73fc5e 100644 --- a/packages/b2c-tooling-sdk/test/config/dw-json.test.ts +++ b/packages/b2c-tooling-sdk/test/config/dw-json.test.ts @@ -76,7 +76,7 @@ describe('config/dw-json', () => { fs.writeFileSync(dwJsonPath, JSON.stringify(config)); const result = loadDwJson(); - expect(result).to.deep.equal(config); + expect(result?.config).to.deep.equal(config); }); it('loads config from explicit path', () => { @@ -87,7 +87,7 @@ describe('config/dw-json', () => { fs.writeFileSync(customPath, JSON.stringify(config)); const result = loadDwJson({path: customPath}); - expect(result).to.deep.equal(config); + expect(result?.config).to.deep.equal(config); }); it('selects named instance from multi-config', () => { @@ -102,8 +102,23 @@ describe('config/dw-json', () => { fs.writeFileSync(dwJsonPath, JSON.stringify(multiConfig)); const result = loadDwJson({instance: 'staging'}); - expect(result?.hostname).to.equal('staging.demandware.net'); - expect(result?.name).to.equal('staging'); + expect(result?.config.hostname).to.equal('staging.demandware.net'); + expect(result?.config.name).to.equal('staging'); + }); + + it('returns undefined when requested instance does not exist', () => { + const dwJsonPath = path.join(tempDir, 'dw.json'); + const multiConfig = { + hostname: 'root.demandware.net', + configs: [ + {name: 'staging', hostname: 'staging.demandware.net'}, + {name: 'production', hostname: 'prod.demandware.net'}, + ], + }; + fs.writeFileSync(dwJsonPath, JSON.stringify(multiConfig)); + + const result = loadDwJson({instance: 'nonexistent'}); + expect(result).to.be.undefined; }); it('selects active config when no instance specified', () => { @@ -119,8 +134,8 @@ describe('config/dw-json', () => { fs.writeFileSync(dwJsonPath, JSON.stringify(multiConfig)); const result = loadDwJson(); - expect(result?.hostname).to.equal('prod.demandware.net'); - expect(result?.name).to.equal('production'); + expect(result?.config.hostname).to.equal('prod.demandware.net'); + expect(result?.config.name).to.equal('production'); }); it('returns root config when no active config found', () => { @@ -133,7 +148,7 @@ describe('config/dw-json', () => { fs.writeFileSync(dwJsonPath, JSON.stringify(multiConfig)); const result = loadDwJson(); - expect(result?.hostname).to.equal('root.demandware.net'); + expect(result?.config.hostname).to.equal('root.demandware.net'); }); it('returns undefined for invalid JSON', () => { @@ -160,9 +175,9 @@ describe('config/dw-json', () => { fs.writeFileSync(dwJsonPath, JSON.stringify(config)); const result = loadDwJson(); - expect(result?.['client-id']).to.equal('test-client'); - expect(result?.['client-secret']).to.equal('test-secret'); - expect(result?.['oauth-scopes']).to.deep.equal(['mail', 'roles']); + expect(result?.config['client-id']).to.equal('test-client'); + expect(result?.config['client-secret']).to.equal('test-secret'); + expect(result?.config['oauth-scopes']).to.deep.equal(['mail', 'roles']); }); it('handles webdav-hostname', () => { @@ -174,7 +189,7 @@ describe('config/dw-json', () => { fs.writeFileSync(dwJsonPath, JSON.stringify(config)); const result = loadDwJson(); - expect(result?.['webdav-hostname']).to.equal('webdav.test.com'); + expect(result?.config['webdav-hostname']).to.equal('webdav.test.com'); }); }); }); diff --git a/packages/b2c-tooling-sdk/test/config/mapping.test.ts b/packages/b2c-tooling-sdk/test/config/mapping.test.ts deleted file mode 100644 index 1b92b1a..0000000 --- a/packages/b2c-tooling-sdk/test/config/mapping.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ -import {expect} from 'chai'; -import { - mapDwJsonToNormalizedConfig, - mergeConfigsWithProtection, - getPopulatedFields, -} from '@salesforce/b2c-tooling-sdk/config'; - -describe('config/mapping', () => { - describe('mapDwJsonToNormalizedConfig', () => { - it('maps basic dw.json fields to normalized config', () => { - const dwJson = { - hostname: 'example.demandware.net', - 'code-version': 'v1', - username: 'test-user', - password: 'test-pass', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.hostname).to.equal('example.demandware.net'); - expect(config.codeVersion).to.equal('v1'); - expect(config.username).to.equal('test-user'); - expect(config.password).to.equal('test-pass'); - }); - - it('maps OAuth credentials', () => { - const dwJson = { - hostname: 'example.demandware.net', - 'client-id': 'my-client-id', - 'client-secret': 'my-client-secret', - 'oauth-scopes': ['mail', 'roles'], - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.clientId).to.equal('my-client-id'); - expect(config.clientSecret).to.equal('my-client-secret'); - expect(config.scopes).to.deep.equal(['mail', 'roles']); - }); - - it('maps webdav-hostname as first priority', () => { - const dwJson = { - hostname: 'example.demandware.net', - 'webdav-hostname': 'webdav.example.com', - secureHostname: 'secure.example.com', - 'secure-server': 'secure-server.example.com', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.webdavHostname).to.equal('webdav.example.com'); - }); - - it('maps secureHostname when webdav-hostname is not present', () => { - const dwJson = { - hostname: 'example.demandware.net', - secureHostname: 'secure.example.com', - 'secure-server': 'secure-server.example.com', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.webdavHostname).to.equal('secure.example.com'); - }); - - it('maps secure-server when other webdav options are not present', () => { - const dwJson = { - hostname: 'example.demandware.net', - 'secure-server': 'secure-server.example.com', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.webdavHostname).to.equal('secure-server.example.com'); - }); - - it('maps shortCode as first priority', () => { - const dwJson = { - hostname: 'example.demandware.net', - shortCode: 'abc123', - 'short-code': 'def456', - 'scapi-shortcode': 'ghi789', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.shortCode).to.equal('abc123'); - }); - - it('maps short-code when shortCode is not present', () => { - const dwJson = { - hostname: 'example.demandware.net', - 'short-code': 'def456', - 'scapi-shortcode': 'ghi789', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.shortCode).to.equal('def456'); - }); - - it('maps scapi-shortcode when other short code options are not present', () => { - const dwJson = { - hostname: 'example.demandware.net', - 'scapi-shortcode': 'ghi789', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.shortCode).to.equal('ghi789'); - }); - - it('maps MRT fields', () => { - const dwJson = { - hostname: 'example.demandware.net', - mrtProject: 'my-project', - mrtEnvironment: 'staging', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.mrtProject).to.equal('my-project'); - expect(config.mrtEnvironment).to.equal('staging'); - }); - - it('maps instance name from dw.json name field', () => { - const dwJson = { - hostname: 'example.demandware.net', - name: 'production', - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.instanceName).to.equal('production'); - }); - - it('maps auth-methods', () => { - const dwJson = { - hostname: 'example.demandware.net', - 'auth-methods': ['client-credentials', 'basic'] as ('client-credentials' | 'basic')[], - }; - - const config = mapDwJsonToNormalizedConfig(dwJson); - - expect(config.authMethods).to.deep.equal(['client-credentials', 'basic']); - }); - }); - - describe('mergeConfigsWithProtection', () => { - it('merges overrides with base config (overrides win)', () => { - const overrides = { - codeVersion: 'v2', - clientId: 'override-client', - }; - const base = { - hostname: 'example.demandware.net', - codeVersion: 'v1', - clientId: 'base-client', - clientSecret: 'base-secret', - }; - - const {config, warnings, hostnameMismatch} = mergeConfigsWithProtection(overrides, base); - - expect(config.hostname).to.equal('example.demandware.net'); - expect(config.codeVersion).to.equal('v2'); - expect(config.clientId).to.equal('override-client'); - expect(config.clientSecret).to.equal('base-secret'); - expect(warnings).to.have.length(0); - expect(hostnameMismatch).to.equal(false); - }); - - it('detects hostname mismatch and ignores base config', () => { - const overrides = { - hostname: 'staging.demandware.net', - clientId: 'staging-client', - }; - const base = { - hostname: 'prod.demandware.net', - clientId: 'prod-client', - clientSecret: 'prod-secret', - }; - - const {config, warnings, hostnameMismatch} = mergeConfigsWithProtection(overrides, base); - - expect(config.hostname).to.equal('staging.demandware.net'); - expect(config.clientId).to.equal('staging-client'); - expect(config.clientSecret).to.be.undefined; - expect(hostnameMismatch).to.equal(true); - expect(warnings).to.have.length(1); - expect(warnings[0].code).to.equal('HOSTNAME_MISMATCH'); - expect(warnings[0].message).to.include('staging.demandware.net'); - expect(warnings[0].message).to.include('prod.demandware.net'); - }); - - it('does not trigger mismatch when hostnames match', () => { - const overrides = { - hostname: 'example.demandware.net', - codeVersion: 'v2', - }; - const base = { - hostname: 'example.demandware.net', - codeVersion: 'v1', - clientSecret: 'secret', - }; - - const {config, warnings, hostnameMismatch} = mergeConfigsWithProtection(overrides, base); - - expect(config.hostname).to.equal('example.demandware.net'); - expect(config.codeVersion).to.equal('v2'); - expect(config.clientSecret).to.equal('secret'); - expect(hostnameMismatch).to.equal(false); - expect(warnings).to.have.length(0); - }); - - it('does not trigger mismatch when no override hostname is provided', () => { - const overrides = { - codeVersion: 'v2', - }; - const base = { - hostname: 'example.demandware.net', - codeVersion: 'v1', - }; - - const {config, warnings, hostnameMismatch} = mergeConfigsWithProtection(overrides, base); - - expect(config.hostname).to.equal('example.demandware.net'); - expect(config.codeVersion).to.equal('v2'); - expect(hostnameMismatch).to.equal(false); - expect(warnings).to.have.length(0); - }); - - it('can disable hostname protection', () => { - const overrides = { - hostname: 'staging.demandware.net', - }; - const base = { - hostname: 'prod.demandware.net', - clientSecret: 'prod-secret', - }; - - const {config, warnings, hostnameMismatch} = mergeConfigsWithProtection(overrides, base, { - hostnameProtection: false, - }); - - expect(config.hostname).to.equal('staging.demandware.net'); - expect(config.clientSecret).to.equal('prod-secret'); - expect(hostnameMismatch).to.equal(false); - expect(warnings).to.have.length(0); - }); - - it('handles empty base config', () => { - const overrides = { - hostname: 'example.demandware.net', - clientId: 'client', - }; - const base = {}; - - const {config, warnings} = mergeConfigsWithProtection(overrides, base); - - expect(config.hostname).to.equal('example.demandware.net'); - expect(config.clientId).to.equal('client'); - expect(warnings).to.have.length(0); - }); - - it('handles empty overrides', () => { - const overrides = {}; - const base = { - hostname: 'example.demandware.net', - codeVersion: 'v1', - }; - - const {config, warnings} = mergeConfigsWithProtection(overrides, base); - - expect(config.hostname).to.equal('example.demandware.net'); - expect(config.codeVersion).to.equal('v1'); - expect(warnings).to.have.length(0); - }); - }); - - describe('getPopulatedFields', () => { - it('returns list of fields with values', () => { - const config = { - hostname: 'example.demandware.net', - codeVersion: 'v1', - username: 'user', - }; - - const fields = getPopulatedFields(config); - - expect(fields).to.have.members(['hostname', 'codeVersion', 'username']); - }); - - it('excludes undefined fields', () => { - const config = { - hostname: 'example.demandware.net', - codeVersion: undefined, - username: undefined, - }; - - const fields = getPopulatedFields(config); - - expect(fields).to.deep.equal(['hostname']); - }); - - it('excludes null fields', () => { - const config = { - hostname: 'example.demandware.net', - codeVersion: null as unknown as string, - }; - - const fields = getPopulatedFields(config); - - expect(fields).to.deep.equal(['hostname']); - }); - - it('excludes empty string fields', () => { - const config = { - hostname: 'example.demandware.net', - codeVersion: '', - }; - - const fields = getPopulatedFields(config); - - expect(fields).to.deep.equal(['hostname']); - }); - - it('returns empty array for empty config', () => { - const config = {}; - - const fields = getPopulatedFields(config); - - expect(fields).to.have.length(0); - }); - }); -}); diff --git a/packages/b2c-tooling-sdk/test/config/resolved-config.test.ts b/packages/b2c-tooling-sdk/test/config/resolved-config.test.ts index 93fdcf7..ea36531 100644 --- a/packages/b2c-tooling-sdk/test/config/resolved-config.test.ts +++ b/packages/b2c-tooling-sdk/test/config/resolved-config.test.ts @@ -16,7 +16,7 @@ describe('config/resolved-config', () => { }); it('hasB2CInstanceConfig returns false when hostname is missing', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(config.hasB2CInstanceConfig()).to.be.false; }); @@ -26,7 +26,7 @@ describe('config/resolved-config', () => { }); it('hasMrtConfig returns false when mrtApiKey is missing', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(config.hasMrtConfig()).to.be.false; }); @@ -36,7 +36,7 @@ describe('config/resolved-config', () => { }); it('hasOAuthConfig returns false when clientId is missing', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(config.hasOAuthConfig()).to.be.false; }); @@ -46,12 +46,12 @@ describe('config/resolved-config', () => { }); it('hasBasicAuthConfig returns false when username is missing', () => { - const config = resolveConfig({password: 'pass'}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({password: 'pass'}, {replaceDefaultSources: true}); expect(config.hasBasicAuthConfig()).to.be.false; }); it('hasBasicAuthConfig returns false when password is missing', () => { - const config = resolveConfig({username: 'user'}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({username: 'user'}, {replaceDefaultSources: true}); expect(config.hasBasicAuthConfig()).to.be.false; }); }); @@ -67,7 +67,7 @@ describe('config/resolved-config', () => { }); it('throws error when hostname is missing', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(() => config.createB2CInstance()).to.throw('B2C instance requires hostname'); }); }); @@ -82,12 +82,12 @@ describe('config/resolved-config', () => { }); it('throws error when username is missing', () => { - const config = resolveConfig({password: 'pass'}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({password: 'pass'}, {replaceDefaultSources: true}); expect(() => config.createBasicAuth()).to.throw('Basic auth requires username and password'); }); it('throws error when password is missing', () => { - const config = resolveConfig({username: 'user'}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({username: 'user'}, {replaceDefaultSources: true}); expect(() => config.createBasicAuth()).to.throw('Basic auth requires username and password'); }); }); @@ -102,7 +102,7 @@ describe('config/resolved-config', () => { }); it('throws error when clientId is missing', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(() => config.createOAuth()).to.throw('OAuth requires clientId'); }); @@ -123,7 +123,7 @@ describe('config/resolved-config', () => { }); it('throws error when mrtApiKey is missing', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(() => config.createMrtAuth()).to.throw('MRT auth requires mrtApiKey'); }); }); @@ -146,7 +146,7 @@ describe('config/resolved-config', () => { }); it('throws error when no auth is available', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(() => config.createWebDavAuth()).to.throw( 'WebDAV auth requires basic auth (username/password) or OAuth (clientId)', ); @@ -171,7 +171,7 @@ describe('config/resolved-config', () => { }); it('throws error when mrtApiKey is missing', () => { - const config = resolveConfig({}, {replaceDefaultSources: true, sources: []}); + const config = resolveConfig({}, {replaceDefaultSources: true}); expect(() => config.createMrtClient({org: 'test-org', project: 'test-project'})).to.throw( 'MRT auth requires mrtApiKey', ); diff --git a/packages/b2c-tooling-sdk/test/config/resolver.test.ts b/packages/b2c-tooling-sdk/test/config/resolver.test.ts index 361a703..ea026d6 100644 --- a/packages/b2c-tooling-sdk/test/config/resolver.test.ts +++ b/packages/b2c-tooling-sdk/test/config/resolver.test.ts @@ -8,6 +8,7 @@ import { ConfigResolver, createConfigResolver, type ConfigSource, + type ConfigLoadResult, type NormalizedConfig, type ResolveConfigOptions, } from '@salesforce/b2c-tooling-sdk/config'; @@ -19,15 +20,14 @@ class MockSource implements ConfigSource { constructor( public name: string, private config: NormalizedConfig | undefined, - private path?: string, + private location?: string, ) {} - load(_options: ResolveConfigOptions): NormalizedConfig | undefined { - return this.config; - } - - getPath(): string | undefined { - return this.path; + load(_options: ResolveConfigOptions): ConfigLoadResult | undefined { + if (this.config === undefined) { + return undefined; + } + return {config: this.config, location: this.location}; } } @@ -90,16 +90,16 @@ describe('config/resolver', () => { expect(sources).to.have.length(2); }); - it('tracks source paths when available', () => { + it('tracks source locations when available', () => { const source = new MockSource('test', {hostname: 'example.demandware.net'}, '/path/to/dw.json'); const resolver = new ConfigResolver([source]); const {sources} = resolver.resolve(); - expect(sources[0].path).to.equal('/path/to/dw.json'); + expect(sources[0].location).to.equal('/path/to/dw.json'); }); - it('tracks which fields each source contributed', () => { + it('tracks which fields each source provided', () => { const source1 = new MockSource('first', { hostname: 'example.demandware.net', }); @@ -111,8 +111,37 @@ describe('config/resolver', () => { const {sources} = resolver.resolve(); - expect(sources[0].fieldsContributed).to.deep.equal(['hostname']); - expect(sources[1].fieldsContributed).to.have.members(['clientId', 'clientSecret']); + expect(sources[0].fields).to.deep.equal(['hostname']); + expect(sources[0].fieldsIgnored).to.be.undefined; + expect(sources[1].fields).to.have.members(['clientId', 'clientSecret']); + expect(sources[1].fieldsIgnored).to.be.undefined; + }); + + it('tracks fieldsIgnored when higher priority source provides same fields', () => { + const source1 = new MockSource('higher-priority', { + hostname: 'example.demandware.net', + clientId: 'higher-client', + clientSecret: 'higher-secret', + }); + const source2 = new MockSource('lower-priority', { + clientId: 'lower-client', + clientSecret: 'lower-secret', + }); + const resolver = new ConfigResolver([source1, source2]); + + const {sources, config} = resolver.resolve(); + + // Higher priority source provides and uses all its fields + expect(sources[0].fields).to.have.members(['hostname', 'clientId', 'clientSecret']); + expect(sources[0].fieldsIgnored).to.be.undefined; + + // Lower priority source provides fields but they are ignored + expect(sources[1].fields).to.have.members(['clientId', 'clientSecret']); + expect(sources[1].fieldsIgnored).to.have.members(['clientId', 'clientSecret']); + + // Final config uses higher priority values + expect(config.clientId).to.equal('higher-client'); + expect(config.clientSecret).to.equal('higher-secret'); }); it('skips sources that return undefined', () => { diff --git a/packages/b2c-tooling-sdk/test/config/sources.test.ts b/packages/b2c-tooling-sdk/test/config/sources.test.ts index 501df53..9494191 100644 --- a/packages/b2c-tooling-sdk/test/config/sources.test.ts +++ b/packages/b2c-tooling-sdk/test/config/sources.test.ts @@ -113,7 +113,7 @@ describe('config/sources', () => { expect(config.hostname).to.equal('staging.demandware.net'); }); - it('provides path via getPath', () => { + it('provides location from load result', () => { const dwJsonPath = path.join(tempDir, 'dw.json'); fs.writeFileSync( dwJsonPath, @@ -123,14 +123,13 @@ describe('config/sources', () => { ); const resolver = new ConfigResolver(); - resolver.resolve(); const {sources} = resolver.resolve(); - const dwJsonSource = sources.find((s) => s.name === 'dw.json'); + const dwJsonSource = sources.find((s) => s.name === 'DwJsonSource'); // Normalize paths to handle macOS symlinks (/var -> /private/var) const expectedPath = fs.realpathSync(dwJsonPath); - const actualPath = dwJsonSource?.path ? fs.realpathSync(dwJsonSource.path) : undefined; - expect(actualPath).to.equal(expectedPath); + const actualLocation = dwJsonSource?.location ? fs.realpathSync(dwJsonSource.location) : undefined; + expect(actualLocation).to.equal(expectedPath); }); }); @@ -318,7 +317,7 @@ describe('config/sources', () => { } }); - it('provides path via getPath', function () { + it('provides location from load result', function () { const originalHomedir = os.homedir; let canMock = false; try { @@ -343,14 +342,13 @@ describe('config/sources', () => { ); const resolver = new ConfigResolver(); - resolver.resolve(); const {sources} = resolver.resolve(); - const mobifySource = sources.find((s) => s.name === 'mobify'); + const mobifySource = sources.find((s) => s.name === 'MobifySource'); // Normalize paths to handle macOS symlinks const expectedPath = fs.realpathSync(mobifyPath); - const actualPath = mobifySource?.path ? fs.realpathSync(mobifySource.path) : undefined; - expect(actualPath).to.equal(expectedPath); + const actualLocation = mobifySource?.location ? fs.realpathSync(mobifySource.location) : undefined; + expect(actualLocation).to.equal(expectedPath); // Restore Object.defineProperty(os, 'homedir', { diff --git a/packages/b2c-tooling-sdk/test/fixtures/test-cli/src/commands/test-instance.js b/packages/b2c-tooling-sdk/test/fixtures/test-cli/src/commands/test-instance.js index 9225ae4..f1bc079 100644 --- a/packages/b2c-tooling-sdk/test/fixtures/test-cli/src/commands/test-instance.js +++ b/packages/b2c-tooling-sdk/test/fixtures/test-cli/src/commands/test-instance.js @@ -17,11 +17,11 @@ export default class TestInstance extends InstanceCommand { async run() { // Check server via resolvedConfig (no hasServer method on InstanceCommand) - const hasServer = Boolean(this.resolvedConfig.hostname); + const hasServer = Boolean(this.resolvedConfig.values.hostname); // Return server/instance info without requiring server (for testing flags work) const result = { - server: this.resolvedConfig.hostname, + server: this.resolvedConfig.values.hostname, hasServer, };