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
6 changes: 6 additions & 0 deletions .changeset/content-validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@salesforce/b2c-cli': minor
'@salesforce/b2c-tooling-sdk': minor
---

Add `content validate` command to validate Page Designer metadefinition JSON files against bundled schemas. Supports auto-detection of schema types from file paths and content, or explicit `--type` flag. Includes glob pattern support for validating multiple files.
102 changes: 98 additions & 4 deletions docs/cli/content.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
---
description: Commands for exporting and listing Page Designer content from B2C Commerce content libraries.
description: Commands for exporting, listing, and validating Page Designer content from B2C Commerce content libraries.
---

# Content Commands

Commands for working with Page Designer content libraries on B2C Commerce instances.
Commands for working with Page Designer content libraries and metadefinitions.

## Authentication

Content commands require OAuth authentication:
The `content export` and `content list` commands require OAuth authentication:

| Operation | Auth Required |
|-----------|--------------|
| `content export` | OAuth (OCAPI for export job + WebDAV for assets) |
| `content list` | OAuth (OCAPI for export job) |
| `content validate` | None (local file validation) |

```bash
export SFCC_CLIENT_ID=your-client-id
export SFCC_CLIENT_SECRET=your-client-secret
```

Both commands also support a `--library-file` flag for offline use with a local XML file, which skips authentication entirely.
The `content export` and `content list` commands also support a `--library-file` flag for offline use with a local XML file, which skips authentication entirely.

For complete setup instructions, see the [Authentication Guide](/guide/authentication).

Expand Down Expand Up @@ -201,3 +202,96 @@ footer-content (CONTENT ASSET)
Pages show `id (typeId: type)`, components show `typeId (id)`, content assets show `id (CONTENT ASSET)`, and static assets show `path (STATIC ASSET)`. The tree uses color when output to a terminal: page names are bold, component type IDs are cyan, asset paths are green, and tree connectors are dim.

With `--json`, returns `{ data: [...] }` with each item containing `id`, `type`, `typeId`, and `children` count.

---

## b2c content validate

Validate Page Designer metadefinition JSON files against bundled JSON schemas. This is a local-only command — no instance connection or authentication is required.

The command auto-detects the schema type using file path conventions (`experience/pages/` → pagetype, `experience/components/` → componenttype) and falls back to inspecting the JSON properties. You can also specify the type explicitly with `--type`.

### Usage

```bash
b2c content validate <files...>
```

### Arguments

| Argument | Description | Required |
|----------|-------------|----------|
| `files` | One or more file paths, directories, or glob patterns | Yes |

When a directory is passed, all `*.json` files within it are validated recursively.

### Flags

In addition to [global flags](./index#global-flags):

| Flag | Description | Default |
|------|-------------|---------|
| `--type`, `-t` | Schema type to validate against (skips auto-detection) | Auto-detected |

Available schema types: `pagetype`, `componenttype`, `aspecttype`, `cmsrecord`, `customeditortype`, `contentassetpageconfig`, `contentassetcomponentconfig`, `contentassetstructuredcontentdata`, `image`.

### Examples

```bash
# Validate a single page type definition
b2c content validate cartridge/experience/pages/storePage.json

# Validate all metadefinitions in a directory recursively
b2c content validate cartridge/experience/

# Validate with a glob pattern
b2c content validate 'cartridge/experience/**/*.json'

# Explicitly specify the schema type
b2c content validate --type componenttype mycomponent.json

# Validate multiple files
b2c content validate pages/home.json pages/about.json components/hero.json

# JSON output for CI/scripting
b2c content validate cartridge/experience/ --json
```

### Output

The command prints a color-coded result for each file:

```
PASS: experience/pages/storePage.json (pagetype)
PASS: experience/components/hero.json (componenttype)
FAIL: experience/components/broken.json (componenttype)
ERROR at .attribute_definition_groups[0]: is required

2/3 file(s) valid, 1 error(s)
```

The command exits with a non-zero status when any file fails validation.

With `--json`, returns a structured result:

```json
{
"results": [
{
"valid": true,
"schemaType": "pagetype",
"errors": [],
"filePath": "/path/to/storePage.json"
}
],
"totalFiles": 1,
"validFiles": 1,
"totalErrors": 0
}
```

### Notes

- Auto-detection uses file path conventions first: files under `experience/pages/` are validated as `pagetype`, files under `experience/components/` as `componenttype`. For other schema types, use `--type`.
- Schemas are bundled with the SDK and follow the [Page Designer metadefinition specification](https://developer.salesforce.com/docs/commerce/b2c-commerce/guide/b2c-page-designer.html).
- Use `b2c content validate` in CI pipelines to catch metadefinition errors before deployment.
1 change: 1 addition & 0 deletions docs/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"../packages/b2c-tooling-sdk/src/instance/index.ts",
"../packages/b2c-tooling-sdk/src/logging/index.ts",
"../packages/b2c-tooling-sdk/src/operations/code/index.ts",
"../packages/b2c-tooling-sdk/src/operations/content/index.ts",
"../packages/b2c-tooling-sdk/src/operations/cip/index.ts",
"../packages/b2c-tooling-sdk/src/operations/jobs/index.ts",
"../packages/b2c-tooling-sdk/src/operations/logs/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/b2c-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"description": "Deploy and manage code versions on instances\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/code.html"
},
"content": {
"description": "Export and manage Page Designer content libraries"
"description": "Export, list, and validate Page Designer content"
},
"cip": {
"description": "Run CIP analytics SQL and curated reports\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/cip.html"
Expand Down
148 changes: 148 additions & 0 deletions packages/b2c-cli/src/commands/content/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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 fs from 'node:fs';
import path from 'node:path';
import {Args, Flags, ux} from '@oclif/core';
import {glob} from 'glob';
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {
CONTENT_SCHEMA_TYPES,
MetaDefinitionDetectionError,
validateMetaDefinitionFile,
type ContentSchemaType,
type MetaDefinitionValidationResult,
} from '@salesforce/b2c-tooling-sdk/operations/content';

interface ContentValidateResponse {
results: MetaDefinitionValidationResult[];
totalErrors: number;
totalFiles: number;
validFiles: number;
}

export default class ContentValidate extends BaseCommand<typeof ContentValidate> {
static args = {
files: Args.string({
description: 'File(s), directory, or glob pattern(s) for JSON metadefinition files to validate',
required: true,
}),
};

static description = 'Validate Page Designer metadefinition JSON files against schemas';

static enableJsonFlag = true;

static examples = [
'<%= config.bin %> <%= command.id %> cartridge/experience/pages/storePage.json',
'<%= config.bin %> <%= command.id %> --type componenttype mycomponent.json',
"<%= config.bin %> <%= command.id %> 'cartridge/experience/**/*.json'",
'<%= config.bin %> <%= command.id %> cartridge/experience/',
'<%= config.bin %> <%= command.id %> storePage.json --json',
];

static flags = {
...BaseCommand.baseFlags,
type: Flags.string({
char: 't',
description: 'Schema type (auto-detected if not specified)',
options: [...CONTENT_SCHEMA_TYPES],
}),
};

// Allow multiple file arguments
static strict = false;

protected operations: {
glob: (pattern: string, options?: {nodir?: boolean}) => Promise<string[]>;
validateMetaDefinitionFile: typeof validateMetaDefinitionFile;
} = {
glob,
validateMetaDefinitionFile,
};

async run(): Promise<ContentValidateResponse> {
const {argv, flags} = await this.parse(ContentValidate);
const patterns = argv as string[];

if (patterns.length === 0) {
this.error('At least one file path, directory, or glob pattern is required.');
}

// Expand directories to recursive JSON globs, then resolve all patterns in parallel
const resolvedPatterns = patterns.map((pattern) =>
fs.existsSync(pattern) && fs.statSync(pattern).isDirectory() ? path.join(pattern, '**/*.json') : pattern,
);
const allMatches = await Promise.all(
resolvedPatterns.map((resolved) => this.operations.glob(resolved, {nodir: true})),
);
const filePaths: string[] = [];
for (const [i, matches] of allMatches.entries()) {
if (matches.length === 0) {
this.warn(`No files matched: ${patterns[i]}`);
}
filePaths.push(...matches);
}

if (filePaths.length === 0) {
this.error('No files found matching the provided patterns.');
}

const results: MetaDefinitionValidationResult[] = [];

for (const filePath of filePaths) {
try {
const result = this.operations.validateMetaDefinitionFile(filePath, {
type: flags.type as ContentSchemaType | undefined,
});
results.push(result);
} catch (error) {
if (error instanceof MetaDefinitionDetectionError) {
const relativePath = path.relative(process.cwd(), path.resolve(filePath));
this.error(`${relativePath}: ${error.message}`);
}
throw error;
}
}

const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
const validFiles = results.filter((r) => r.valid).length;

const response: ContentValidateResponse = {
results,
totalErrors,
totalFiles: results.length,
validFiles,
};

if (this.jsonEnabled()) {
return response;
}

for (const result of results) {
const relativePath = path.relative(process.cwd(), result.filePath ?? '');
const typeInfo = result.schemaType ? ` (${result.schemaType})` : '';

if (result.valid) {
ux.stdout(`${ux.colorize('green', 'PASS')}: ${relativePath}${typeInfo}`);
} else {
ux.stdout(`${ux.colorize('red', 'FAIL')}: ${relativePath}${typeInfo}`);
for (const error of result.errors) {
const location = error.path && error.path !== '/' ? ` at ${error.path}` : '';
ux.stdout(` ${ux.colorize('red', 'ERROR')}${location}: ${error.message}`);
}
}
}

ux.stdout('');
ux.stdout(`${validFiles}/${results.length} file(s) valid, ${totalErrors} error(s)`);

if (totalErrors > 0) {
this.error('Validation failed', {exit: 1});
}

return response;
}
}
Loading
Loading