Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
375 changes: 152 additions & 223 deletions .claude/skills/testing/SKILL.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/b2c-cli/src/commands/ods/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
this.log(t('commands.ods.create.waiting', 'Waiting for sandbox to be ready...'));

// Initial delay before first poll to allow the sandbox to be registered in the API
await this.sleep(2000);
await this.sleep(pollIntervalMs);

while (true) {
// Check for timeout
Expand Down
7 changes: 1 addition & 6 deletions packages/b2c-tooling-sdk/.c8rc.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
{
"all": true,
"src": ["src"],
"exclude": [
"src/clients/*.generated.ts",
"test/**",
"**/*.d.ts",
"src/**/*types.ts"
],
"exclude": ["src/clients/*.generated.ts", "test/**", "**/*.d.ts", "src/**/*types.ts"],
"reporter": ["text", "text-summary", "html", "lcov"],
"report-dir": "coverage",
"check-coverage": true,
Expand Down
4 changes: 4 additions & 0 deletions packages/b2c-tooling-sdk/.prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
*.generated.ts
dist
coverage
data
specs
64 changes: 32 additions & 32 deletions packages/b2c-tooling-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ npm install @salesforce/b2c-tooling-sdk
Use `resolveConfig()` to load configuration from project files (dw.json) and create a B2C instance:

```typescript
import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config';
import {resolveConfig} from '@salesforce/b2c-tooling-sdk/config';

// Load configuration, override secrets from environment
const config = resolveConfig({
Expand All @@ -36,8 +36,8 @@ await instance.webdav.mkcol('Cartridges/v1');
await instance.webdav.put('Cartridges/v1/app.zip', zipBuffer);

// Use typed OCAPI client (openapi-fetch)
const { data, error } = await instance.ocapi.GET('/sites', {
params: { query: { select: '(**)' } },
const {data, error} = await instance.ocapi.GET('/sites', {
params: {query: {select: '(**)'}},
});
```

Expand All @@ -46,16 +46,16 @@ const { data, error } = await instance.ocapi.GET('/sites', {
For advanced use cases, you can construct a B2CInstance directly:

```typescript
import { B2CInstance } from '@salesforce/b2c-tooling-sdk';
import {B2CInstance} from '@salesforce/b2c-tooling-sdk';

const instance = new B2CInstance(
{ hostname: 'your-sandbox.demandware.net', codeVersion: 'v1' },
{hostname: 'your-sandbox.demandware.net', codeVersion: 'v1'},
{
oauth: {
clientId: 'your-client-id',
clientSecret: 'your-client-secret'
}
}
clientSecret: 'your-client-secret',
},
},
);
```

Expand Down Expand Up @@ -89,24 +89,24 @@ The OCAPI client uses [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) wit

```typescript
// List sites
const { data, error } = await instance.ocapi.GET('/sites', {
params: { query: { select: '(**)' } },
const {data, error} = await instance.ocapi.GET('/sites', {
params: {query: {select: '(**)'}},
});

// Activate a code version
const { data, error } = await instance.ocapi.PATCH('/code_versions/{code_version_id}', {
params: { path: { code_version_id: 'v1' } },
body: { active: true },
const {data, error} = await instance.ocapi.PATCH('/code_versions/{code_version_id}', {
params: {path: {code_version_id: 'v1'}},
body: {active: true},
});
```

### Code Deployment

```typescript
import { findAndDeployCartridges, activateCodeVersion } from '@salesforce/b2c-tooling-sdk/operations/code';
import {findAndDeployCartridges, activateCodeVersion} from '@salesforce/b2c-tooling-sdk/operations/code';

// Deploy cartridges
await findAndDeployCartridges(instance, './cartridges', { reload: true });
await findAndDeployCartridges(instance, './cartridges', {reload: true});

// Activate code version
await activateCodeVersion(instance, 'v1');
Expand All @@ -115,7 +115,7 @@ await activateCodeVersion(instance, 'v1');
### Job Execution

```typescript
import { executeJob, waitForJob, siteArchiveImport } from '@salesforce/b2c-tooling-sdk/operations/jobs';
import {executeJob, waitForJob, siteArchiveImport} from '@salesforce/b2c-tooling-sdk/operations/jobs';

// Run a job and wait for completion
const execution = await executeJob(instance, 'my-job-id');
Expand All @@ -129,32 +129,32 @@ await siteArchiveImport(instance, './site-data.zip');

The SDK provides subpath exports for tree-shaking and organization:

| Export | Description |
|--------|-------------|
| `@salesforce/b2c-tooling-sdk` | Main entry point with all exports |
| `@salesforce/b2c-tooling-sdk/config` | Configuration resolution (resolveConfig) |
| `@salesforce/b2c-tooling-sdk/auth` | Authentication strategies (OAuth, Basic, API Key) |
| `@salesforce/b2c-tooling-sdk/instance` | B2CInstance class |
| `@salesforce/b2c-tooling-sdk/clients` | Low-level API clients (WebDAV, OCAPI, SLAS, ODS, MRT) |
| `@salesforce/b2c-tooling-sdk/operations/code` | Code deployment operations |
| `@salesforce/b2c-tooling-sdk/operations/jobs` | Job execution and site import/export |
| `@salesforce/b2c-tooling-sdk/operations/sites` | Site management |
| `@salesforce/b2c-tooling-sdk/discovery` | Workspace type detection (PWA Kit, SFRA, etc.) |
| `@salesforce/b2c-tooling-sdk/cli` | CLI utilities (BaseCommand, table rendering) |
| `@salesforce/b2c-tooling-sdk/logging` | Structured logging utilities |
| Export | Description |
| ---------------------------------------------- | ----------------------------------------------------- |
| `@salesforce/b2c-tooling-sdk` | Main entry point with all exports |
| `@salesforce/b2c-tooling-sdk/config` | Configuration resolution (resolveConfig) |
| `@salesforce/b2c-tooling-sdk/auth` | Authentication strategies (OAuth, Basic, API Key) |
| `@salesforce/b2c-tooling-sdk/instance` | B2CInstance class |
| `@salesforce/b2c-tooling-sdk/clients` | Low-level API clients (WebDAV, OCAPI, SLAS, ODS, MRT) |
| `@salesforce/b2c-tooling-sdk/operations/code` | Code deployment operations |
| `@salesforce/b2c-tooling-sdk/operations/jobs` | Job execution and site import/export |
| `@salesforce/b2c-tooling-sdk/operations/sites` | Site management |
| `@salesforce/b2c-tooling-sdk/discovery` | Workspace type detection (PWA Kit, SFRA, etc.) |
| `@salesforce/b2c-tooling-sdk/cli` | CLI utilities (BaseCommand, table rendering) |
| `@salesforce/b2c-tooling-sdk/logging` | Structured logging utilities |

## Logging

Configure logging for debugging HTTP requests:

```typescript
import { configureLogger } from '@salesforce/b2c-tooling-sdk/logging';
import {configureLogger} from '@salesforce/b2c-tooling-sdk/logging';

// Enable debug logging (shows HTTP request summaries)
configureLogger({ level: 'debug' });
configureLogger({level: 'debug'});

// Enable trace logging (shows full request/response with headers and bodies)
configureLogger({ level: 'trace' });
configureLogger({level: 'trace'});
```

## Documentation
Expand Down
9 changes: 6 additions & 3 deletions packages/b2c-tooling-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@
"build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
"clean": "shx rm -rf dist",
"lint": "eslint",
"format": "prettier --write src scripts",
"format:check": "prettier --check src scripts",
"format": "prettier --write .",
"format:check": "prettier --check .",
"pretest": "tsc --noEmit -p test",
"test": "c8 mocha --forbid-only \"test/**/*.test.ts\"",
"test:ci": "c8 mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
Expand All @@ -210,12 +210,14 @@
"@eslint/compat": "^1",
"@oclif/core": "^4",
"@oclif/prettier-config": "^0.2.1",
"@oclif/test": "^4.1.14",
"@salesforce/dev-config": "^4.3.2",
"@tony.ganchev/eslint-plugin-header": "^3.1.11",
"@types/archiver": "^7.0.0",
"@types/chai": "^4.3.20",
"@types/mocha": "^10.0.10",
"@types/node": "^18.19.130",
"@types/sinon": "^21.0.0",
"@types/xml2js": "^0.4.14",
"c8": "^10.1.3",
"chai": "^4.5.0",
Expand All @@ -227,6 +229,7 @@
"openapi-typescript": "^7.10.1",
"prettier": "^3.6.2",
"shx": "^0.3.3",
"sinon": "^21.0.1",
"tsx": "^4.20.6",
"typescript": "^5",
"typescript-eslint": "^8"
Expand All @@ -244,9 +247,9 @@
},
"dependencies": {
"archiver": "^7.0.1",
"fuse.js": "^7.0.0",
"chokidar": "^5.0.0",
"cliui": "^9.0.1",
"fuse.js": "^7.0.0",
"glob": "^13.0.0",
"i18next": "^25.6.3",
"jszip": "^3.10.1",
Expand Down
3 changes: 3 additions & 0 deletions packages/b2c-tooling-sdk/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface LoadConfigOptions {
configPath?: string;
/** Cloud origin for MRT ~/.mobify lookup (e.g., https://cloud-staging.mobify.com) */
cloudOrigin?: string;
/** Path to custom MRT credentials file (overrides default ~/.mobify) */
credentialsFile?: string;
}

/**
Expand Down Expand Up @@ -110,6 +112,7 @@ export function loadConfig(
configPath: options.configPath,
hostnameProtection: true,
cloudOrigin: options.cloudOrigin,
credentialsFile: options.credentialsFile,
sourcesBefore: pluginSources.before,
sourcesAfter: pluginSources.after,
});
Expand Down
6 changes: 6 additions & 0 deletions packages/b2c-tooling-sdk/src/cli/mrt-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,21 @@ export abstract class MrtCommand<T extends typeof Command> extends BaseCommand<T
description: `MRT cloud origin URL (default: ${DEFAULT_MRT_ORIGIN})`,
env: 'SFCC_MRT_CLOUD_ORIGIN',
}),
'credentials-file': Flags.string({
description: 'Path to MRT credentials file (overrides default ~/.mobify)',
env: 'MRT_CREDENTIALS_FILE',
}),
};

protected override loadConfiguration(): ResolvedConfig {
const cloudOrigin = this.flags['cloud-origin'] as string | undefined;
const credentialsFile = this.flags['credentials-file'] as string | undefined;

const options: LoadConfigOptions = {
instance: this.flags.instance,
configPath: this.flags.config,
cloudOrigin, // MobifySource uses this to load ~/.mobify--[hostname] if set
credentialsFile, // Override path to MRT credentials file
};

const flagConfig: Partial<ResolvedConfig> = {
Expand Down
16 changes: 11 additions & 5 deletions packages/b2c-tooling-sdk/src/config/dw-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,26 +149,32 @@ function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonCon
/**
* Loads configuration from a dw.json file.
*
* Searches upward from the current directory (or specified startDir) for a dw.json file.
* Supports both single-config and multi-config formats.
* If an explicit path is provided, uses that file. Otherwise, looks for dw.json
* in the startDir (or cwd). Does NOT search parent directories.
*
* 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
*
* @example
* // Auto-find dw.json
* // Load from ./dw.json (current directory)
* const config = loadDwJson();
*
* // Load from specific directory
* const config = loadDwJson({ startDir: '/path/to/project' });
*
* // Use named instance
* const config = loadDwJson({ instance: 'staging' });
*
* // Explicit path
* const config = loadDwJson({ path: './config/dw.json' });
*/
export function loadDwJson(options: LoadDwJsonOptions = {}): DwJsonConfig | undefined {
const dwJsonPath = options.path || findDwJson(options.startDir);
// 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');

if (!dwJsonPath || !fs.existsSync(dwJsonPath)) {
if (!fs.existsSync(dwJsonPath)) {
return undefined;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
*
* @internal This module is internal to the SDK. Use ConfigResolver instead.
*/
import {loadDwJson, findDwJson} from '../dw-json.js';
import * as path from 'node:path';
import {loadDwJson} from '../dw-json.js';
import {mapDwJsonToNormalizedConfig} from '../mapping.js';
import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '../types.js';

Expand All @@ -33,8 +34,8 @@ export class DwJsonSource implements ConfigSource {
return undefined;
}

// Track the path for diagnostics
this.lastPath = options.configPath || findDwJson(options.startDir);
// Track the path for diagnostics - use explicit path or default location
this.lastPath = options.configPath ?? path.join(options.startDir || process.cwd(), 'dw.json');

return mapDwJsonToNormalizedConfig(dwConfig);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export class MobifySource implements ConfigSource {
private lastPath?: string;

load(options: ResolveConfigOptions): NormalizedConfig | undefined {
const mobifyPath = this.getMobifyPath(options.cloudOrigin);
// Use explicit credentialsFile if provided, otherwise use default path
const mobifyPath = options.credentialsFile ?? this.getMobifyPath(options.cloudOrigin);
this.lastPath = mobifyPath;

if (!fs.existsSync(mobifyPath)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/b2c-tooling-sdk/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export interface ResolveConfigOptions {
hostnameProtection?: boolean;
/** Cloud origin for ~/.mobify lookup (MRT) */
cloudOrigin?: string;
/** Path to custom MRT credentials file (overrides default ~/.mobify) */
credentialsFile?: string;

/**
* Custom sources to add BEFORE default sources (higher priority).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 {runCommand} from '@oclif/test';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixtureRoot = path.join(__dirname, '../fixtures/test-cli');

describe('BaseCommand integration', () => {
it('runs test-base command without errors', async () => {
const {error} = await runCommand(['test-base'], {root: fixtureRoot});
expect(error).to.be.undefined;
});

it('handles --extra-query flag', async () => {
const {error, result} = await runCommand<{extraParams?: Record<string, unknown>}>(
['test-base', '--extra-query', '{"debug":"true"}', '--json'],
{root: fixtureRoot},
);

expect(error).to.be.undefined;
expect(result?.extraParams?.query).to.deep.equal({debug: 'true'});
});

it('handles --extra-body flag', async () => {
const {error, result} = await runCommand<{extraParams?: Record<string, unknown>}>(
['test-base', '--extra-body', '{"_internal":true}', '--json'],
{root: fixtureRoot},
);

expect(error).to.be.undefined;
expect(result?.extraParams?.body).to.deep.equal({_internal: true});
});

it('handles both --extra-query and --extra-body flags', async () => {
const {error, result} = await runCommand<{extraParams?: Record<string, unknown>}>(
['test-base', '--extra-query', '{"debug":"true"}', '--extra-body', '{"_internal":true}', '--json'],
{root: fixtureRoot},
);

expect(error).to.be.undefined;
expect(result?.extraParams?.query).to.deep.equal({debug: 'true'});
expect(result?.extraParams?.body).to.deep.equal({_internal: true});
});

it('returns undefined extraParams when no extra flags provided', async () => {
const {error, result} = await runCommand<{extraParams?: Record<string, unknown>}>(['test-base', '--json'], {
root: fixtureRoot,
});

expect(error).to.be.undefined;
expect(result?.extraParams).to.be.undefined;
});
});
Loading