Skip to content
Closed
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
8 changes: 6 additions & 2 deletions packages/b2c-tooling-sdk/src/operations/content/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,14 @@ function processContent(
}
}

// Recurse into content-links
// Recurse into content-links (sorted by position)
const contentLinks = content['content-links'] as Array<Record<string, unknown>> | undefined;
if (contentLinks?.[0]?.['content-link']) {
const links = contentLinks[0]['content-link'] as Array<Record<string, unknown>>;
const links = (contentLinks[0]['content-link'] as Array<Record<string, unknown>>).slice().sort((a, b) => {
const posA = parseFloat((a['position'] as string[] | undefined)?.[0] ?? 'Infinity');
const posB = parseFloat((b['position'] as string[] | undefined)?.[0] ?? 'Infinity');
return posA - posB;
});
for (const link of links) {
const linkAttrs = link['$'] as Record<string, string>;
const linkId = linkAttrs['content-id'];
Expand Down
61 changes: 59 additions & 2 deletions packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ export async function siteArchiveImport(
if (!archiveName) {
throw new Error('archiveName is required when importing from a Buffer');
}
zipFilename = archiveName.endsWith('.zip') ? archiveName : `${archiveName}.zip`;
archiveContent = target;
const baseName = archiveName.endsWith('.zip') ? archiveName.slice(0, -4) : archiveName;
zipFilename = `${baseName}.zip`;
archiveContent = await ensureArchiveStructure(target, baseName, logger);
} else {
// File path - check if directory or zip file
const targetPath = target as string;
Expand Down Expand Up @@ -236,6 +237,62 @@ async function addDirectoryToZip(zipFolder: JSZip, dirPath: string): Promise<voi
}
}

/**
* Ensures a zip buffer has the correct top-level directory structure required
* by B2C Commerce site archive import. The archive must contain a single
* top-level directory matching the archive name.
*
* If the zip is already correctly structured, the original buffer is returned.
* Otherwise, the contents are re-wrapped under the expected directory name.
*/
async function ensureArchiveStructure(
buffer: Buffer,
archiveDirName: string,
logger: ReturnType<typeof getLogger>,
): Promise<Buffer> {
let zip: JSZip;
try {
zip = await JSZip.loadAsync(buffer);
} catch {
// If we can't parse the zip, pass it through as-is
logger.debug('Could not parse zip buffer for structure check; passing through as-is');
return buffer;
}

// Determine the unique top-level directory names
const topLevelEntries = new Set<string>();
for (const filePath of Object.keys(zip.files)) {
const topLevel = filePath.split('/')[0];
topLevelEntries.add(topLevel);
}

if (topLevelEntries.size === 1 && topLevelEntries.has(archiveDirName)) {
return buffer; // Already correctly structured
}

// Re-wrap all entries under archiveDirName/
logger.debug(
{archiveDirName, topLevelEntries: [...topLevelEntries]},
`Re-wrapping archive contents under ${archiveDirName}/`,
);

const newZip = new JSZip();
const rootFolder = newZip.folder(archiveDirName)!;

for (const [filePath, entry] of Object.entries(zip.files)) {
if (!entry.dir) {
const content = await entry.async('nodebuffer');
rootFolder.file(filePath, content);
}
}

return newZip.generateAsync({
type: 'nodebuffer',
compression: 'DEFLATE',
compressionOptions: {level: 9},
});
}

/**
* Configuration for sites in export.
*/
Expand Down
44 changes: 44 additions & 0 deletions packages/b2c-tooling-sdk/test/operations/content/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,50 @@ export const WILDCARD_ASSET_LIBRARY_XML = `<?xml version="1.0" encoding="UTF-8"?
</content>
</library>`;

/**
* Library XML with out-of-order content-link positions.
*
* The content-links are listed in XML order: comp-b, comp-d, comp-a, comp-c,
* but their positions specify: comp-a=1, comp-b=2, comp-c=3, comp-d=4.
*/
export const POSITION_LIBRARY_XML = `<?xml version="1.0" encoding="UTF-8"?>
<library xmlns="http://www.demandware.com/xml/impex/library/2006-10-31" library-id="PositionLib">
<content content-id="ordered-page">
<type>page.storePage</type>
<data xml:lang="x-default"><![CDATA[{"title": "Ordered"}]]></data>
<content-links>
<content-link content-id="comp-b" type="page.storePage.main">
<position>2.0</position>
</content-link>
<content-link content-id="comp-d" type="page.storePage.main">
<position>4.0</position>
</content-link>
<content-link content-id="comp-a" type="page.storePage.main">
<position>1.0</position>
</content-link>
<content-link content-id="comp-c" type="page.storePage.main">
<position>3.0</position>
</content-link>
</content-links>
</content>
<content content-id="comp-a">
<type>component.alpha</type>
<data xml:lang="x-default"><![CDATA[{"label": "A"}]]></data>
</content>
<content content-id="comp-b">
<type>component.beta</type>
<data xml:lang="x-default"><![CDATA[{"label": "B"}]]></data>
</content>
<content content-id="comp-c">
<type>component.gamma</type>
<data xml:lang="x-default"><![CDATA[{"label": "C"}]]></data>
</content>
<content content-id="comp-d">
<type>component.delta</type>
<data xml:lang="x-default"><![CDATA[{"label": "D"}]]></data>
</content>
</library>`;

/**
* Library XML with a missing content-link target.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
MINIMAL_LIBRARY_XML,
WILDCARD_ASSET_LIBRARY_XML,
MISSING_LINK_LIBRARY_XML,
POSITION_LIBRARY_XML,
} from './fixtures.js';

describe('operations/content/library', () => {
Expand Down Expand Up @@ -426,6 +427,17 @@ describe('operations/content/library', () => {
});
});

describe('content-link position sorting', () => {
it('should sort children by position rather than XML document order', async () => {
const library = await Library.parse(POSITION_LIBRARY_XML);
const page = library.tree.children.find((n) => n.id === 'ordered-page');
expect(page).to.exist;

const childIds = page!.children.map((n) => n.id);
expect(childIds).to.deep.equal(['comp-a', 'comp-b', 'comp-c', 'comp-d']);
});
});

describe('missing content-link', () => {
it('should parse without error when content-link target is missing', async () => {
const library = await Library.parse(MISSING_LINK_LIBRARY_XML);
Expand Down
127 changes: 125 additions & 2 deletions packages/b2c-vs-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
},
"activationEvents": [
"onView:b2cWebdavExplorer",
"onView:b2cContentExplorer",
"onFileSystem:b2c-webdav",
"onFileSystem:b2c-content",
"onCommand:b2c-dx.openUI",
"onCommand:b2c-dx.handleStorefrontNextCartridge",
"onCommand:b2c-dx.promptAgent",
Expand All @@ -29,11 +31,17 @@
],
"main": "./dist/extension.js",
"contributes": {
"submenus": [
{
"id": "b2c-dx.submenu",
"label": "B2C-DX"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "b2c-dx",
"title": "B2C-DX WebDAV",
"title": "B2C-DX",
"icon": "media/b2c-icon.svg"
}
]
Expand All @@ -42,16 +50,26 @@
"b2c-dx": [
{
"id": "b2cWebdavExplorer",
"name": "Browser",
"name": "WebDAV Browser",
"icon": "media/b2c-icon.svg",
"contextualTitle": "B2C Commerce"
},
{
"id": "b2cContentExplorer",
"name": "Libraries",
"icon": "media/b2c-icon.svg",
"contextualTitle": "B2C Commerce Content"
}
]
},
"viewsWelcome": [
{
"view": "b2cWebdavExplorer",
"contents": "No B2C Commerce instance configured.\n\nCreate a dw.json file in your workspace or set SFCC_* environment variables.\n\n[Refresh](command:b2c-dx.webdav.refresh)"
},
{
"view": "b2cContentExplorer",
"contents": "No content libraries configured.\n\nSet \"contentLibrary\" in dw.json or add a library manually.\n\n[Add Library](command:b2c-dx.content.addLibrary)"
}
],
"commands": [
Expand Down Expand Up @@ -132,6 +150,60 @@
"title": "Open as Workspace Folder",
"icon": "$(root-folder-opened)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.refresh",
"title": "Refresh",
"icon": "$(refresh)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.addLibrary",
"title": "Add Library",
"icon": "$(add)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.removeLibrary",
"title": "Remove Library",
"icon": "$(remove)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.export",
"title": "Export",
"icon": "$(cloud-download)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.exportNoAssets",
"title": "Export without Assets",
"icon": "$(file-code)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.exportAssets",
"title": "Export Assets Only",
"icon": "$(file-media)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.filter",
"title": "Filter",
"icon": "$(filter)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.clearFilter",
"title": "Clear Filter",
"icon": "$(clear-all)",
"category": "B2C DX"
},
{
"command": "b2c-dx.content.import",
"title": "Import Site Archive",
"icon": "$(cloud-upload)",
"category": "B2C DX"
}
],
"menus": {
Expand All @@ -140,6 +212,26 @@
"command": "b2c-dx.webdav.refresh",
"when": "view == b2cWebdavExplorer",
"group": "navigation"
},
{
"command": "b2c-dx.content.refresh",
"when": "view == b2cContentExplorer",
"group": "navigation"
},
{
"command": "b2c-dx.content.addLibrary",
"when": "view == b2cContentExplorer",
"group": "navigation"
},
{
"command": "b2c-dx.content.filter",
"when": "view == b2cContentExplorer && !b2cContentFilterActive",
"group": "navigation"
},
{
"command": "b2c-dx.content.clearFilter",
"when": "view == b2cContentExplorer && b2cContentFilterActive",
"group": "navigation"
}
],
"view/item/context": [
Expand Down Expand Up @@ -177,13 +269,44 @@
"command": "b2c-dx.webdav.mountWorkspace",
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
"group": "3_workspace@1"
},
{
"command": "b2c-dx.content.export",
"when": "view == b2cContentExplorer && viewItem =~ /^(page|content|component)$/",
"group": "1_export@1"
},
{
"command": "b2c-dx.content.exportNoAssets",
"when": "view == b2cContentExplorer && viewItem =~ /^(page|content|component)$/",
"group": "1_export@2"
},
{
"command": "b2c-dx.content.exportAssets",
"when": "view == b2cContentExplorer && viewItem =~ /^(page|content|component)$/",
"group": "1_export@3"
},
{
"command": "b2c-dx.content.removeLibrary",
"when": "view == b2cContentExplorer && viewItem == library",
"group": "2_manage@1"
}
],
"explorer/context": [
{
"command": "b2c-dx.webdav.download",
"when": "resourceScheme == b2c-webdav && !explorerResourceIsFolder",
"group": "navigation"
},
{
"submenu": "b2c-dx.submenu",
"when": "explorerResourceIsFolder",
"group": "7_modification@9"
}
],
"b2c-dx.submenu": [
{
"command": "b2c-dx.content.import",
"when": "explorerResourceIsFolder"
}
]
}
Expand Down
Loading