From e8d7fb9351a67f74a00504163e84164d5362dab3 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Tue, 12 May 2026 14:22:57 -0400 Subject: [PATCH 1/4] site library support in libraries config Extends libraries to accept {id, siteLibrary?} entries (mixed with strings). b2c content list/export default --site-library from a matching entry; the flag is now allowNo with no default so explicit values still win. The VS Code Content Libraries tree auto-seeds every entry on activation. Adds libraries, assetQuery, and realm rows to the documented package.json / dw.json field tables. --- .changeset/site-library-config-support.md | 7 ++ docs/cli/content.md | 6 +- docs/guide/configuration.md | 48 ++++++++++--- .../b2c-cli/src/commands/content/export.ts | 19 ++++-- packages/b2c-cli/src/commands/content/list.ts | 17 +++-- .../test/commands/content/list.test.ts | 68 +++++++++++++++++++ .../b2c-tooling-sdk/src/config/dw-json.ts | 11 ++- packages/b2c-tooling-sdk/src/config/index.ts | 3 +- .../b2c-tooling-sdk/src/config/mapping.ts | 14 +++- .../src/config/sources/package-json-source.ts | 5 +- packages/b2c-tooling-sdk/src/config/types.ts | 26 ++++++- .../test/config/mapping.test.ts | 28 ++++++++ .../test/config/sources.test.ts | 32 +++++++++ .../src/content-tree/content-config.ts | 15 +++- .../src/content-tree/content-tree-provider.ts | 18 +++-- .../src/webdav-tree/webdav-mappings.ts | 3 +- 16 files changed, 283 insertions(+), 37 deletions(-) create mode 100644 .changeset/site-library-config-support.md diff --git a/.changeset/site-library-config-support.md b/.changeset/site-library-config-support.md new file mode 100644 index 000000000..55ae056d3 --- /dev/null +++ b/.changeset/site-library-config-support.md @@ -0,0 +1,7 @@ +--- +'@salesforce/b2c-cli': minor +'@salesforce/b2c-tooling-sdk': minor +'b2c-vs-extension': minor +--- + +The `libraries` config field now accepts `{id, siteLibrary?}` objects in addition to bare strings (mixed forms allowed in the same array). This lets you mark site-private libraries in `dw.json` or `package.json` so `b2c content list` / `content export` can default `--site-library` based on which library you target, and the VS Code Content Libraries tree auto-loads every configured library on activation. To upgrade, optionally replace `"libraries": ["RefArch"]` with `"libraries": ["RefArch", {"id": "homepage", "siteLibrary": true}]`. The existing string-only form continues to work unchanged. Also adds `libraries`, `assetQuery`, and `realm` to the documented `package.json` allowed fields list (already supported in code). diff --git a/docs/cli/content.md b/docs/cli/content.md index e43de4210..badf1d11b 100644 --- a/docs/cli/content.md +++ b/docs/cli/content.md @@ -53,7 +53,7 @@ In addition to [global flags](./index#global-flags): |------|-------------|---------| | `--library` | Library ID or site ID. Also configurable via `content-library` in dw.json. | | | `--output`, `-o` | Output directory | `content-` | -| `--site-library` | Treat the library as a site-private library | `false` | +| `--site-library` / `--no-site-library` | Treat the library as a site-private library. Defaults from a matching `libraries` config entry, otherwise `false` | from config | | `--asset-query`, `-q` | JSON dot-paths for static asset extraction (can be repeated) | `image.path` | | `--regex`, `-r` | Treat page IDs as regular expressions | `false` | | `--folder` | Filter by folder classification (can be repeated) | | @@ -116,7 +116,7 @@ With `--json`, returns a structured result including the library tree, output pa ### Notes -- The `--library` flag can be set in `dw.json` as `content-library` or in `package.json` under `b2c.contentLibrary` to avoid passing it every time +- The `--library` flag can be set in `dw.json` as `content-library` or in `package.json` under `b2c.contentLibrary` to avoid passing it every time. You can also list libraries under `b2c.libraries` (mixed strings or `{id, siteLibrary?}` objects); when the resolved library matches an entry marked `siteLibrary: true`, `--site-library` defaults to true automatically. The CLI flag still wins when passed explicitly - Use `b2c content list` to discover available page IDs before exporting - You can export pages, content assets, or individual components by their content ID. When a component ID is specified, it is promoted to the root of the export with its full child tree - The `--asset-query` flag specifies JSON dot-notation paths within component data to extract static asset references. The default `image.path` covers the common Page Designer image component pattern @@ -141,7 +141,7 @@ In addition to [global flags](./index#global-flags): | Flag | Description | Default | |------|-------------|---------| | `--library` | Library ID or site ID. Also configurable via `content-library` in dw.json. | | -| `--site-library` | Treat the library as a site-private library | `false` | +| `--site-library` / `--no-site-library` | Treat the library as a site-private library. Defaults from a matching `libraries` config entry, otherwise `false` | from config | | `--library-file` | Use a local library XML file instead of fetching from instance | | | `--type` | Filter by node type: `page`, `content`, or `component` | | | `--components` | Include components in table output | `false` | diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index f9d4c6c2c..db0ce57a2 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -258,8 +258,11 @@ For the full command reference with all flags, see [Setup Commands](/cli/setup). | `account-manager-host` | Account Manager hostname for OAuth | | `shortCode` | SCAPI short code. Also accepts `short-code` or `scapi-shortcode`. | | `content-library` | Default content library ID for `content export` and `content list` commands | +| `libraries` | Library IDs for the WebDAV browser and Content Libraries tree. Accepts `string[]` or `[{id, siteLibrary?}]`; elements may be mixed | +| `asset-query` | JSON dot-paths used to extract static asset URLs during content library parsing (default `["image.path"]`). Also accepts `assetQuery` | | `tenant-id` | Organization/tenant ID for SCAPI | | `sandbox-api-host` | ODS (sandbox) API hostname | +| `realm` | Default ODS realm for sandbox operations | | `cip-host` | CIP analytics host override | | `mrtApiKey` | MRT API key | | `mrtProject` | MRT project slug | @@ -319,15 +322,18 @@ You can store project-level defaults in your `package.json` file under the `b2c` Only non-sensitive, project-level fields can be configured in `package.json`. Both camelCase and kebab-case are accepted (e.g., `shortCode` or `short-code`): -| Field | Description | -| -------------------- | --------------------------------------------------------------------------- | -| `shortCode` | SCAPI short code | -| `clientId` | OAuth client ID (for implicit login discovery) | -| `contentLibrary` | Default content library ID for `content export` and `content list` commands | -| `mrtProject` | MRT project slug | -| `mrtOrigin` | MRT API origin URL override | -| `accountManagerHost` | Account Manager hostname for OAuth | -| `sandboxApiHost` | ODS (sandbox) API hostname | +| Field | Description | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `shortCode` | SCAPI short code | +| `clientId` | OAuth client ID (for implicit login discovery) | +| `contentLibrary` | Default content library ID for `content export` and `content list` commands | +| `libraries` | Library IDs for the WebDAV browser and Content Libraries tree. Accepts `string[]` or `[{id, siteLibrary?}]`; elements may be mixed | +| `assetQuery` | JSON dot-paths used to extract static asset URLs during content library parsing (default `["image.path"]`) | +| `mrtProject` | MRT project slug | +| `mrtOrigin` | MRT API origin URL override | +| `accountManagerHost` | Account Manager hostname for OAuth | +| `sandboxApiHost` | ODS (sandbox) API hostname | +| `realm` | Default ODS realm for sandbox operations | ::: warning Security Note Sensitive fields like `hostname`, `password`, `clientSecret`, `username`, and `mrtApiKey` are intentionally **not** supported in `package.json`. These should be configured via `dw.json` (which should be in `.gitignore`), environment variables, or secure credential stores. @@ -337,6 +343,30 @@ Sensitive fields like `hostname`, `password`, `clientSecret`, `username`, and `m `package.json` has the lowest priority of all configuration sources. Values from `dw.json`, environment variables, or CLI flags will always override `package.json` settings. This makes it ideal for project defaults that can be overridden per-environment. ::: +### Content Libraries Example + +The `libraries` field can list the content libraries your project works with so that the VS Code Content Libraries tree auto-loads them and `b2c content list/export` can default `--site-library` based on the entry. + +A bare string is treated as a shared library; an object can mark a library as site-private. Both forms can appear in the same array: + +```json +{ + "b2c": { + "libraries": [ + "RefArch", + { "id": "homepage", "siteLibrary": true } + ] + } +} +``` + +With this config: + +- `b2c content list --library homepage` calls the site-library API automatically (no need to pass `--site-library`). +- `b2c content list --library RefArch` treats `RefArch` as a shared library. +- `--site-library` / `--no-site-library` on the command line still wins over the config default. +- The VS Code Content Libraries tree shows both entries on activation, with `homepage` marked `[site]`. + ### Resolution Priority Configuration is resolved with the following precedence (highest to lowest): diff --git a/packages/b2c-cli/src/commands/content/export.ts b/packages/b2c-cli/src/commands/content/export.ts index 5ce79834a..14b668696 100644 --- a/packages/b2c-cli/src/commands/content/export.ts +++ b/packages/b2c-cli/src/commands/content/export.ts @@ -5,6 +5,7 @@ */ import {Args, Flags, ux} from '@oclif/core'; import {JobCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {resolveLibraryEntries} from '@salesforce/b2c-tooling-sdk/config'; import { LibraryNode, exportContent, @@ -36,15 +37,15 @@ export default class ContentExport extends JobCommand { static flags = { ...JobCommand.baseFlags, library: Flags.string({ - description: 'Library ID or site ID (also configurable via dw.json "content-library")', + description: 'Library ID or site ID (also configurable via dw.json "content-library" or "libraries")', }), output: Flags.string({ char: 'o', description: 'Output directory', }), 'site-library': Flags.boolean({ - description: 'Library is a site-private library', - default: false, + description: 'Library is a site-private library (defaults from a matching "libraries" config entry)', + allowNo: true, }), 'asset-query': Flags.string({ char: 'q', @@ -106,11 +107,17 @@ export default class ContentExport extends JobCommand { this.error('At least one content ID is required.'); } - const libraryId = flags.library ?? this.resolvedConfig.values.contentLibrary; + const libraryEntries = resolveLibraryEntries(this.resolvedConfig.values.libraries); + const libraryId = flags.library ?? this.resolvedConfig.values.contentLibrary ?? libraryEntries[0]?.id; if (!libraryId) { this.error('Library is required. Set via --library flag or "content-library" in dw.json.'); } + const isSiteLibrary = + flags['site-library'] === undefined + ? (libraryEntries.find((e) => e.id === libraryId)?.siteLibrary ?? false) + : flags['site-library']; + if (!flags['library-file']) { this.requireOAuthCredentials(); } @@ -121,7 +128,7 @@ export default class ContentExport extends JobCommand { if (flags['dry-run']) { const {library} = await this.operations.fetchContentLibrary(this.instance, libraryId, { libraryFile: flags['library-file'], - isSiteLibrary: flags['site-library'], + isSiteLibrary, assetQuery, keepOrphans: flags['keep-orphans'], waitOptions, @@ -221,7 +228,7 @@ export default class ContentExport extends JobCommand { } const result = await this.operations.exportContent(this.instance, pageIds, libraryId, outputPath, { - isSiteLibrary: flags['site-library'], + isSiteLibrary, assetQuery, libraryFile: flags['library-file'], offline: flags.offline, diff --git a/packages/b2c-cli/src/commands/content/list.ts b/packages/b2c-cli/src/commands/content/list.ts index 173de3640..6647b4b2c 100644 --- a/packages/b2c-cli/src/commands/content/list.ts +++ b/packages/b2c-cli/src/commands/content/list.ts @@ -12,6 +12,7 @@ import { selectColumns, type ColumnDef, } from '@salesforce/b2c-tooling-sdk/cli'; +import {resolveLibraryEntries} from '@salesforce/b2c-tooling-sdk/config'; import {fetchContentLibrary} from '@salesforce/b2c-tooling-sdk/operations/content'; interface ContentListItem { @@ -64,11 +65,11 @@ export default class ContentList extends JobCommand { static flags = { ...JobCommand.baseFlags, library: Flags.string({ - description: 'Library ID or site ID (also configurable via dw.json "content-library")', + description: 'Library ID or site ID (also configurable via dw.json "content-library" or "libraries")', }), 'site-library': Flags.boolean({ - description: 'Site-private library', - default: false, + description: 'Site-private library (defaults from a matching "libraries" config entry)', + allowNo: true, }), 'library-file': Flags.string({ description: 'Local XML file', @@ -98,11 +99,17 @@ export default class ContentList extends JobCommand { async run(): Promise<{data: ContentListItem[]}> { const {flags} = await this.parse(ContentList); - const libraryId = flags.library ?? this.resolvedConfig.values.contentLibrary; + const libraryEntries = resolveLibraryEntries(this.resolvedConfig.values.libraries); + const libraryId = flags.library ?? this.resolvedConfig.values.contentLibrary ?? libraryEntries[0]?.id; if (!libraryId) { this.error('Library is required. Set via --library flag or "content-library" in dw.json.'); } + const isSiteLibrary = + flags['site-library'] === undefined + ? (libraryEntries.find((e) => e.id === libraryId)?.siteLibrary ?? false) + : flags['site-library']; + if (!flags['library-file']) { this.requireOAuthCredentials(); } @@ -113,7 +120,7 @@ export default class ContentList extends JobCommand { const {library} = await this.operations.fetchContentLibrary(instance, libraryId, { libraryFile: flags['library-file'], - isSiteLibrary: flags['site-library'], + isSiteLibrary, waitOptions, }); diff --git a/packages/b2c-cli/test/commands/content/list.test.ts b/packages/b2c-cli/test/commands/content/list.test.ts index 5ad3a4160..14c9a797f 100644 --- a/packages/b2c-cli/test/commands/content/list.test.ts +++ b/packages/b2c-cli/test/commands/content/list.test.ts @@ -154,4 +154,72 @@ describe('content list', () => { expect(libraryId).to.equal('TestLib'); expect(options.isSiteLibrary).to.equal(true); }); + + it('defaults --site-library from a matching libraries config entry', async () => { + const command: any = await createCommand({library: 'homepage'}); + stubCommon(command); + sinon.stub(command, 'jsonEnabled').returns(true); + Object.defineProperty(command, 'resolvedConfig', { + value: { + values: { + libraries: ['RefArch', {id: 'homepage', siteLibrary: true}], + }, + }, + configurable: true, + }); + + const mockLibrary = createMockLibrary(); + const fetchStub = sinon.stub(command.operations, 'fetchContentLibrary').resolves({library: mockLibrary}); + + await command.run(); + + const [, libraryId, options] = fetchStub.firstCall.args; + expect(libraryId).to.equal('homepage'); + expect(options.isSiteLibrary).to.equal(true); + }); + + it('explicit --no-site-library overrides libraries config default', async () => { + const command: any = await createCommand({library: 'homepage', 'site-library': false}); + stubCommon(command); + sinon.stub(command, 'jsonEnabled').returns(true); + Object.defineProperty(command, 'resolvedConfig', { + value: { + values: { + libraries: [{id: 'homepage', siteLibrary: true}], + }, + }, + configurable: true, + }); + + const mockLibrary = createMockLibrary(); + const fetchStub = sinon.stub(command.operations, 'fetchContentLibrary').resolves({library: mockLibrary}); + + await command.run(); + + const options = fetchStub.firstCall.args[2]; + expect(options.isSiteLibrary).to.equal(false); + }); + + it('falls back to first libraries entry when --library and contentLibrary unset', async () => { + const command: any = await createCommand({}); + stubCommon(command); + sinon.stub(command, 'jsonEnabled').returns(true); + Object.defineProperty(command, 'resolvedConfig', { + value: { + values: { + libraries: [{id: 'RefArch'}, {id: 'homepage', siteLibrary: true}], + }, + }, + configurable: true, + }); + + const mockLibrary = createMockLibrary(); + const fetchStub = sinon.stub(command.operations, 'fetchContentLibrary').resolves({library: mockLibrary}); + + await command.run(); + + const [, libraryId, options] = fetchStub.firstCall.args; + expect(libraryId).to.equal('RefArch'); + expect(options.isSiteLibrary).to.equal(false); + }); }); diff --git a/packages/b2c-tooling-sdk/src/config/dw-json.ts b/packages/b2c-tooling-sdk/src/config/dw-json.ts index df69219f2..a90b1d433 100644 --- a/packages/b2c-tooling-sdk/src/config/dw-json.ts +++ b/packages/b2c-tooling-sdk/src/config/dw-json.ts @@ -15,6 +15,7 @@ import * as fsp from 'node:fs/promises'; import * as path from 'node:path'; import type {AuthMethod} from '../auth/types.js'; import {getLogger} from '../logging/logger.js'; +import type {LibraryEntry} from './types.js'; import {normalizeConfigKeys} from './mapping.js'; /** @@ -83,8 +84,14 @@ export interface DwJsonConfig { contentLibrary?: string; /** Catalog IDs for WebDAV browsing */ catalogs?: string[]; - /** Library IDs for WebDAV browsing */ - libraries?: string[]; + /** + * Library IDs for WebDAV browsing and the Content Libraries tree. + * + * Accepts either a string array or a mixed array of strings and + * `{id, siteLibrary?}` objects. Object entries can mark individual + * libraries as site-private. + */ + libraries?: (string | LibraryEntry)[]; /** JSON dot-paths for asset extraction during content library parsing (defaults to ['image.path']) */ assetQuery?: string[]; /** Optional CIP analytics host override */ diff --git a/packages/b2c-tooling-sdk/src/config/index.ts b/packages/b2c-tooling-sdk/src/config/index.ts index e72786198..7c1560363 100644 --- a/packages/b2c-tooling-sdk/src/config/index.ts +++ b/packages/b2c-tooling-sdk/src/config/index.ts @@ -102,6 +102,7 @@ export {resolveConfig, ConfigResolver, createConfigResolver} from './resolver.js export type { MaybePromise, NormalizedConfig, + LibraryEntry, ConfigSource, ConfigLoadResult, ConfigSourceInfo, @@ -116,7 +117,7 @@ export type { } from './types.js'; // Instance creation utility (public API for CLI commands) -export {createInstanceFromConfig, normalizeConfigKeys} from './mapping.js'; +export {createInstanceFromConfig, normalizeConfigKeys, resolveLibraryEntries} from './mapping.js'; // Low-level dw.json API (still available for advanced use) export { diff --git a/packages/b2c-tooling-sdk/src/config/mapping.ts b/packages/b2c-tooling-sdk/src/config/mapping.ts index bc9fcdedb..01946db3a 100644 --- a/packages/b2c-tooling-sdk/src/config/mapping.ts +++ b/packages/b2c-tooling-sdk/src/config/mapping.ts @@ -17,7 +17,7 @@ import {parseSafetyLevelString} from '../safety/safety-middleware.js'; import {isValidSafetyAction} from '../safety/types.js'; import type {SafetyRule} from '../safety/types.js'; import type {DwJsonConfig} from './dw-json.js'; -import type {NormalizedConfig, ConfigWarning} from './types.js'; +import type {LibraryEntry, NormalizedConfig, ConfigWarning} from './types.js'; /** * Normalizes a URL origin string by ensuring it has an `https://` protocol prefix. @@ -37,6 +37,18 @@ export function normalizeOriginUrl(origin: string | undefined): string | undefin return normalized.replace(/\/+$/, ''); } +/** + * Normalizes a {@link NormalizedConfig.libraries} value to an array of + * {@link LibraryEntry} objects. Bare strings are treated as + * `{id, siteLibrary: false}`. Returns an empty array when input is undefined. + */ +export function resolveLibraryEntries(libraries: (string | LibraryEntry)[] | undefined): LibraryEntry[] { + if (!libraries) return []; + return libraries.map((entry) => + typeof entry === 'string' ? {id: entry, siteLibrary: false} : {siteLibrary: false, ...entry}, + ); +} + /** * Converts a kebab-case string to camelCase. * diff --git a/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts b/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts index ae6c36ef2..1ad85fe61 100644 --- a/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts +++ b/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts @@ -13,7 +13,7 @@ */ import * as fsp from 'node:fs/promises'; import * as path from 'node:path'; -import type {ConfigSource, ConfigLoadResult, ResolveConfigOptions, NormalizedConfig} from '../types.js'; +import type {ConfigSource, ConfigLoadResult, LibraryEntry, NormalizedConfig, ResolveConfigOptions} from '../types.js'; import {getPopulatedFields, normalizeConfigKeys} from '../mapping.js'; import {getLogger} from '../../logging/logger.js'; @@ -25,6 +25,7 @@ const ALLOWED_FIELDS: (keyof NormalizedConfig)[] = [ 'shortCode', 'clientId', 'contentLibrary', + 'libraries', 'assetQuery', 'mrtProject', 'mrtOrigin', @@ -40,11 +41,13 @@ interface PackageJsonB2CConfig { shortCode?: string; clientId?: string; contentLibrary?: string; + libraries?: (string | LibraryEntry)[]; assetQuery?: string[]; mrtProject?: string; mrtOrigin?: string; accountManagerHost?: string; sandboxApiHost?: string; + realm?: string; [key: string]: unknown; } diff --git a/packages/b2c-tooling-sdk/src/config/types.ts b/packages/b2c-tooling-sdk/src/config/types.ts index 753f2b292..b747e4b2a 100644 --- a/packages/b2c-tooling-sdk/src/config/types.ts +++ b/packages/b2c-tooling-sdk/src/config/types.ts @@ -25,6 +25,19 @@ import type {SafetyRule} from '../safety/types.js'; */ export type MaybePromise = T | Promise; +/** + * Configured content library entry. When present in a {@link NormalizedConfig.libraries} + * array, it identifies a library by ID and optionally marks it as site-private. + * + * The simpler `string` form (an ID alone) is equivalent to `{id, siteLibrary: false}`. + */ +export interface LibraryEntry { + /** Library ID (or site ID for site-private libraries) */ + id: string; + /** True if this library is site-private (lookup uses the site-library API) */ + siteLibrary?: boolean; +} + /** * Normalized B2C configuration with camelCase fields. * @@ -112,8 +125,17 @@ export interface NormalizedConfig { /** Catalog IDs for WebDAV browsing */ catalogs?: string[]; - /** Library IDs for WebDAV browsing */ - libraries?: string[]; + /** + * Library IDs for WebDAV browsing and the Content Libraries tree. + * + * Accepts either a string array (all treated as shared libraries) or an + * array of {@link LibraryEntry} objects that can mark individual libraries + * as site-private. Both forms can be mixed in the same array. + * + * @example + * ["RefArch", { id: "homepage", siteLibrary: true }] + */ + libraries?: (string | LibraryEntry)[]; /** * JSON dot-paths for asset extraction from component data during diff --git a/packages/b2c-tooling-sdk/test/config/mapping.test.ts b/packages/b2c-tooling-sdk/test/config/mapping.test.ts index 5ba319d98..b3b8de699 100644 --- a/packages/b2c-tooling-sdk/test/config/mapping.test.ts +++ b/packages/b2c-tooling-sdk/test/config/mapping.test.ts @@ -8,6 +8,7 @@ import { kebabToCamelCase, normalizeConfigKeys, normalizeOriginUrl, + resolveLibraryEntries, CONFIG_KEY_ALIASES, } from '../../src/config/mapping.js'; @@ -32,6 +33,33 @@ describe('config/mapping', () => { }); }); + describe('resolveLibraryEntries', () => { + it('returns empty array for undefined', () => { + expect(resolveLibraryEntries(undefined)).to.deep.equal([]); + }); + + it('treats bare strings as shared libraries', () => { + expect(resolveLibraryEntries(['RefArch', 'OtherLib'])).to.deep.equal([ + {id: 'RefArch', siteLibrary: false}, + {id: 'OtherLib', siteLibrary: false}, + ]); + }); + + it('passes through object entries and defaults siteLibrary to false', () => { + expect(resolveLibraryEntries([{id: 'shared'}, {id: 'private', siteLibrary: true}])).to.deep.equal([ + {id: 'shared', siteLibrary: false}, + {id: 'private', siteLibrary: true}, + ]); + }); + + it('supports mixed string and object entries in the same array', () => { + expect(resolveLibraryEntries(['RefArch', {id: 'homepage', siteLibrary: true}])).to.deep.equal([ + {id: 'RefArch', siteLibrary: false}, + {id: 'homepage', siteLibrary: true}, + ]); + }); + }); + describe('CONFIG_KEY_ALIASES', () => { it('maps server to hostname', () => { expect(CONFIG_KEY_ALIASES['server']).to.equal('hostname'); diff --git a/packages/b2c-tooling-sdk/test/config/sources.test.ts b/packages/b2c-tooling-sdk/test/config/sources.test.ts index c374c9e20..6a18df25a 100644 --- a/packages/b2c-tooling-sdk/test/config/sources.test.ts +++ b/packages/b2c-tooling-sdk/test/config/sources.test.ts @@ -532,6 +532,38 @@ describe('config/sources', () => { expect(config.accountManagerHost).to.equal('account.demandware.com'); }); + it('loads libraries as a string array', async () => { + const packageJsonPath = path.join(tempDir, 'package.json'); + fs.writeFileSync( + packageJsonPath, + JSON.stringify({ + name: 'test-project', + b2c: {libraries: ['RefArch', 'OtherLib']}, + }), + ); + + const resolver = new ConfigResolver(); + const {config} = await resolver.resolve(); + + expect(config.libraries).to.deep.equal(['RefArch', 'OtherLib']); + }); + + it('loads libraries with site-library entries (mixed shapes allowed)', async () => { + const packageJsonPath = path.join(tempDir, 'package.json'); + fs.writeFileSync( + packageJsonPath, + JSON.stringify({ + name: 'test-project', + b2c: {libraries: ['RefArch', {id: 'homepage', siteLibrary: true}]}, + }), + ); + + const resolver = new ConfigResolver(); + const {config} = await resolver.resolve(); + + expect(config.libraries).to.deep.equal(['RefArch', {id: 'homepage', siteLibrary: true}]); + }); + it('ignores sensitive/instance-specific fields', async () => { const packageJsonPath = path.join(tempDir, 'package.json'); fs.writeFileSync( diff --git a/packages/b2c-vs-extension/src/content-tree/content-config.ts b/packages/b2c-vs-extension/src/content-tree/content-config.ts index 1860d66b9..e54e6a08c 100644 --- a/packages/b2c-vs-extension/src/content-tree/content-config.ts +++ b/packages/b2c-vs-extension/src/content-tree/content-config.ts @@ -4,6 +4,7 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ +import {resolveLibraryEntries} from '@salesforce/b2c-tooling-sdk/config'; import type {B2CInstance} from '@salesforce/b2c-tooling-sdk/instance'; import type {Library} from '@salesforce/b2c-tooling-sdk/operations/content'; import type {B2CExtensionConfig} from '../config-provider.js'; @@ -38,7 +39,19 @@ export class ContentConfigProvider { getContentLibrary(): string | undefined { const config = this.configProvider.getConfig(); - return config?.values.contentLibrary ?? config?.values.libraries?.[0]; + return config?.values.contentLibrary ?? resolveLibraryEntries(config?.values.libraries)[0]?.id; + } + + /** + * Configured library entries (from `libraries` config), normalized to + * `{id, siteLibrary}` form. Returns an empty array when nothing is configured. + */ + getConfiguredLibraries(): {id: string; siteLibrary: boolean}[] { + const config = this.configProvider.getConfig(); + return resolveLibraryEntries(config?.values.libraries).map((entry) => ({ + id: entry.id, + siteLibrary: entry.siteLibrary === true, + })); } getAssetQuery(): string[] | undefined { diff --git a/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts b/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts index 18bf821c5..3574f81bd 100644 --- a/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts +++ b/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts @@ -142,13 +142,21 @@ export class ContentTreeDataProvider implements vscode.TreeDataProvider 0) { + for (const entry of configured) { + this.configProvider.addLibrary(entry.id, entry.siteLibrary); + } + } else { + const contentLibrary = this.configProvider.getContentLibrary(); + if (contentLibrary) { + this.configProvider.addLibrary(contentLibrary, false); + } } } diff --git a/packages/b2c-vs-extension/src/webdav-tree/webdav-mappings.ts b/packages/b2c-vs-extension/src/webdav-tree/webdav-mappings.ts index f01479f9f..94dc8c5f0 100644 --- a/packages/b2c-vs-extension/src/webdav-tree/webdav-mappings.ts +++ b/packages/b2c-vs-extension/src/webdav-tree/webdav-mappings.ts @@ -4,6 +4,7 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ +import {resolveLibraryEntries} from '@salesforce/b2c-tooling-sdk/config'; import * as vscode from 'vscode'; import type {B2CExtensionConfig} from '../config-provider.js'; @@ -31,7 +32,7 @@ export class WebDavMappingsProvider { const config = this.configProvider.getConfig(); this.catalogIds = [...(config?.values.catalogs ?? [])]; - const libs = new Set(config?.values.libraries ?? []); + const libs = new Set(resolveLibraryEntries(config?.values.libraries).map((e) => e.id)); const contentLib = config?.values.contentLibrary; if (contentLib) libs.add(contentLib); this.libraryIds = [...libs]; From f800185911404623e7c490a576344d0bea411f2f Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Tue, 12 May 2026 14:30:59 -0400 Subject: [PATCH 2/4] docs: example libraries config in content commands Add a concrete example of the libraries config (mixed shared + site entries) under b2c content export, and a corresponding inferred-flag example under b2c content list, so the new behavior is visible in the CLI command docs and not just the configuration guide. --- docs/cli/content.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/cli/content.md b/docs/cli/content.md index badf1d11b..868fc9b26 100644 --- a/docs/cli/content.md +++ b/docs/cli/content.md @@ -102,6 +102,38 @@ export SFCC_SERVER=my-sandbox.demandware.net export SFCC_CLIENT_ID=your-client-id export SFCC_CLIENT_SECRET=your-client-secret b2c content export homepage + +# With a libraries config entry (see below) marking "homepage" as site-private, +# --site-library is inferred automatically: +b2c content export homepage --library homepage +``` + +### Configuring multiple libraries + +Listing libraries under `b2c.libraries` in `package.json` (or `libraries` in `dw.json`) lets the CLI pick a default library and infer `--site-library` per entry. Bare strings are shared libraries; `{id, siteLibrary: true}` marks a site-private library. Both forms can be mixed: + +```json +{ + "b2c": { + "libraries": [ + "RefArch", + { "id": "homepage", "siteLibrary": true } + ] + } +} +``` + +With this config: + +```bash +# Uses RefArch (first entry) as the default library +b2c content export hero-banner + +# --site-library is inferred from the matching entry +b2c content export homepage --library homepage + +# Explicit flag still overrides the config +b2c content export homepage --library homepage --no-site-library ``` ### Output @@ -166,6 +198,11 @@ b2c content list --library SharedLibrary --tree # List from a site-private library b2c content list --library RefArch --site-library +# With a libraries config entry marking "homepage" as site-private, +# --site-library is inferred automatically (see "b2c content export" +# above for an example b2c.libraries config): +b2c content list --library homepage + # List from a local XML file b2c content list --library SharedLibrary --library-file ./library.xml From 24e86edc47d27bb617018331d71e634036d54b6c Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Tue, 12 May 2026 14:33:21 -0400 Subject: [PATCH 3/4] docs: use idiomatic library/site IDs in libraries examples The site library example now uses SiteGenesis (a real site ID) and the shared library is RefArchSharedLibrary so the difference between a site-private and shared library is obvious from the IDs alone. --- .changeset/site-library-config-support.md | 2 +- docs/cli/content.md | 20 ++++++++++---------- docs/guide/configuration.md | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.changeset/site-library-config-support.md b/.changeset/site-library-config-support.md index 55ae056d3..0f14f02e6 100644 --- a/.changeset/site-library-config-support.md +++ b/.changeset/site-library-config-support.md @@ -4,4 +4,4 @@ 'b2c-vs-extension': minor --- -The `libraries` config field now accepts `{id, siteLibrary?}` objects in addition to bare strings (mixed forms allowed in the same array). This lets you mark site-private libraries in `dw.json` or `package.json` so `b2c content list` / `content export` can default `--site-library` based on which library you target, and the VS Code Content Libraries tree auto-loads every configured library on activation. To upgrade, optionally replace `"libraries": ["RefArch"]` with `"libraries": ["RefArch", {"id": "homepage", "siteLibrary": true}]`. The existing string-only form continues to work unchanged. Also adds `libraries`, `assetQuery`, and `realm` to the documented `package.json` allowed fields list (already supported in code). +The `libraries` config field now accepts `{id, siteLibrary?}` objects in addition to bare strings (mixed forms allowed in the same array). This lets you mark site-private libraries in `dw.json` or `package.json` so `b2c content list` / `content export` can default `--site-library` based on which library you target, and the VS Code Content Libraries tree auto-loads every configured library on activation. To upgrade, optionally replace `"libraries": ["RefArchSharedLibrary"]` with `"libraries": ["RefArchSharedLibrary", {"id": "SiteGenesis", "siteLibrary": true}]`. The existing string-only form continues to work unchanged. Also adds `libraries`, `assetQuery`, and `realm` to the documented `package.json` allowed fields list (already supported in code). diff --git a/docs/cli/content.md b/docs/cli/content.md index 868fc9b26..f2e7fd0dc 100644 --- a/docs/cli/content.md +++ b/docs/cli/content.md @@ -103,9 +103,9 @@ export SFCC_CLIENT_ID=your-client-id export SFCC_CLIENT_SECRET=your-client-secret b2c content export homepage -# With a libraries config entry (see below) marking "homepage" as site-private, -# --site-library is inferred automatically: -b2c content export homepage --library homepage +# With a libraries config entry (see below) marking the site library as +# site-private, --site-library is inferred automatically: +b2c content export homepage --library SiteGenesis ``` ### Configuring multiple libraries @@ -116,8 +116,8 @@ Listing libraries under `b2c.libraries` in `package.json` (or `libraries` in `dw { "b2c": { "libraries": [ - "RefArch", - { "id": "homepage", "siteLibrary": true } + "RefArchSharedLibrary", + { "id": "SiteGenesis", "siteLibrary": true } ] } } @@ -126,14 +126,14 @@ Listing libraries under `b2c.libraries` in `package.json` (or `libraries` in `dw With this config: ```bash -# Uses RefArch (first entry) as the default library +# Uses RefArchSharedLibrary (first entry) as the default library b2c content export hero-banner # --site-library is inferred from the matching entry -b2c content export homepage --library homepage +b2c content export homepage --library SiteGenesis # Explicit flag still overrides the config -b2c content export homepage --library homepage --no-site-library +b2c content export homepage --library SiteGenesis --no-site-library ``` ### Output @@ -198,10 +198,10 @@ b2c content list --library SharedLibrary --tree # List from a site-private library b2c content list --library RefArch --site-library -# With a libraries config entry marking "homepage" as site-private, +# With a libraries config entry marking the site library as site-private, # --site-library is inferred automatically (see "b2c content export" # above for an example b2c.libraries config): -b2c content list --library homepage +b2c content list --library SiteGenesis # List from a local XML file b2c content list --library SharedLibrary --library-file ./library.xml diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index db0ce57a2..d82903484 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -353,8 +353,8 @@ A bare string is treated as a shared library; an object can mark a library as si { "b2c": { "libraries": [ - "RefArch", - { "id": "homepage", "siteLibrary": true } + "RefArchSharedLibrary", + { "id": "SiteGenesis", "siteLibrary": true } ] } } @@ -362,10 +362,10 @@ A bare string is treated as a shared library; an object can mark a library as si With this config: -- `b2c content list --library homepage` calls the site-library API automatically (no need to pass `--site-library`). -- `b2c content list --library RefArch` treats `RefArch` as a shared library. +- `b2c content list --library SiteGenesis` calls the site-library API automatically (no need to pass `--site-library`); the library ID is the site ID. +- `b2c content list --library RefArchSharedLibrary` treats `RefArchSharedLibrary` as a shared library. - `--site-library` / `--no-site-library` on the command line still wins over the config default. -- The VS Code Content Libraries tree shows both entries on activation, with `homepage` marked `[site]`. +- The VS Code Content Libraries tree shows both entries on activation, with `SiteGenesis` marked `[site]`. ### Resolution Priority From 8483671ca70e24584fa23b836e8c2eea9d77b871 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Tue, 12 May 2026 14:37:30 -0400 Subject: [PATCH 4/4] fix(vs-extension): include contentLibrary alongside libraries in tree When both contentLibrary and libraries are configured, the Content Libraries tree now seeds the union of both rather than only the libraries array. Uses a new getExplicitContentLibrary accessor so the seeder doesn't double-add a library that's already in the libraries list via the existing libraries[0] fallback in getContentLibrary. --- .../src/content-tree/content-config.ts | 10 +++++++++ .../src/content-tree/content-tree-provider.ts | 21 ++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/b2c-vs-extension/src/content-tree/content-config.ts b/packages/b2c-vs-extension/src/content-tree/content-config.ts index e54e6a08c..97ddca2d4 100644 --- a/packages/b2c-vs-extension/src/content-tree/content-config.ts +++ b/packages/b2c-vs-extension/src/content-tree/content-config.ts @@ -42,6 +42,16 @@ export class ContentConfigProvider { return config?.values.contentLibrary ?? resolveLibraryEntries(config?.values.libraries)[0]?.id; } + /** + * The explicitly configured singular `contentLibrary` value (or undefined + * when only `libraries` is set). Use this when you need to distinguish an + * explicit `contentLibrary` from the `libraries[0]` fallback in + * {@link getContentLibrary}. + */ + getExplicitContentLibrary(): string | undefined { + return this.configProvider.getConfig()?.values.contentLibrary; + } + /** * Configured library entries (from `libraries` config), normalized to * `{id, siteLibrary}` form. Returns an empty array when nothing is configured. diff --git a/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts b/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts index 3574f81bd..b15c9c516 100644 --- a/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts +++ b/packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts @@ -143,20 +143,17 @@ export class ContentTreeDataProvider implements vscode.TreeDataProvider 0) { - for (const entry of configured) { - this.configProvider.addLibrary(entry.id, entry.siteLibrary); - } - } else { - const contentLibrary = this.configProvider.getContentLibrary(); - if (contentLibrary) { - this.configProvider.addLibrary(contentLibrary, false); - } + for (const entry of this.configProvider.getConfiguredLibraries()) { + this.configProvider.addLibrary(entry.id, entry.siteLibrary); + } + const contentLibrary = this.configProvider.getExplicitContentLibrary(); + if (contentLibrary) { + this.configProvider.addLibrary(contentLibrary, false); } }