@@ -50,6 +50,14 @@ export interface SiteArchiveImportResult {
5050 * - A Buffer containing zip data
5151 * - A filename already on the instance (in Impex/src/instance/)
5252 *
53+ * **Buffer handling:** When passing a Buffer, the `archiveName` option controls
54+ * the contract:
55+ * - **Without `archiveName`:** The buffer should contain archive entries without
56+ * a root directory (e.g. `libraries/mylib/library.xml`). The SDK generates
57+ * an archive name and wraps the contents under it.
58+ * - **With `archiveName`:** The buffer must already be correctly structured with
59+ * `archiveName/` as the top-level directory. It is uploaded as-is.
60+ *
5361 * @param instance - B2C instance to import to
5462 * @param target - Source to import (directory path, zip file path, Buffer, or remote filename)
5563 * @param options - Import options
@@ -64,9 +72,17 @@ export interface SiteArchiveImportResult {
6472 * // Import from a zip file
6573 * const result = await siteArchiveImport(instance, './export.zip');
6674 *
67- * // Import from a buffer
68- * const zipBuffer = await fs.promises.readFile('./export.zip');
69- * const result = await siteArchiveImport(instance, zipBuffer, {
75+ * // Import from a buffer (SDK wraps contents automatically)
76+ * const zip = new JSZip();
77+ * zip.file('libraries/mylib/library.xml', xmlContent);
78+ * const buffer = await zip.generateAsync({type: 'nodebuffer'});
79+ * const result = await siteArchiveImport(instance, buffer);
80+ *
81+ * // Import from a buffer with explicit archive name (caller owns structure)
82+ * const zip = new JSZip();
83+ * zip.file('my-import/libraries/mylib/library.xml', xmlContent);
84+ * const buffer = await zip.generateAsync({type: 'nodebuffer'});
85+ * const result = await siteArchiveImport(instance, buffer, {
7086 * archiveName: 'my-import'
7187 * });
7288 *
@@ -94,13 +110,20 @@ export async function siteArchiveImport(
94110 zipFilename = target . remoteFilename ;
95111 needsUpload = false ;
96112 } else if ( Buffer . isBuffer ( target ) ) {
97- // Buffer - use provided archive name
98- if ( ! archiveName ) {
99- throw new Error ( 'archiveName is required when importing from a Buffer' ) ;
113+ if ( archiveName ) {
114+ // Caller provides name — buffer must already contain the correct
115+ // top-level directory structure (archiveName/...).
116+ const baseName = archiveName . endsWith ( '.zip' ) ? archiveName . slice ( 0 , - 4 ) : archiveName ;
117+ zipFilename = `${ baseName } .zip` ;
118+ archiveContent = target ;
119+ } else {
120+ // No name — SDK generates one and wraps the buffer contents under it.
121+ // The buffer should contain archive entries without a root directory
122+ // (e.g. libraries/mylib/library.xml, sites/RefArch/site.xml).
123+ const archiveDirName = `import-${ Date . now ( ) } ` ;
124+ zipFilename = `${ archiveDirName } .zip` ;
125+ archiveContent = await wrapArchiveContents ( target , archiveDirName , logger ) ;
100126 }
101- const baseName = archiveName . endsWith ( '.zip' ) ? archiveName . slice ( 0 , - 4 ) : archiveName ;
102- zipFilename = `${ baseName } .zip` ;
103- archiveContent = await ensureArchiveStructure ( target , baseName , logger ) ;
104127 } else {
105128 // File path - check if directory or zip file
106129 const targetPath = target as string ;
@@ -238,57 +261,28 @@ async function addDirectoryToZip(zipFolder: JSZip, dirPath: string): Promise<voi
238261}
239262
240263/**
241- * Ensures a zip buffer has the correct top-level directory structure required
242- * by B2C Commerce site archive import. The archive must contain a single
243- * top-level directory matching the archive name.
264+ * Wraps the contents of a zip buffer under a new top-level directory.
244265 *
245- * If the zip is already correctly structured, the original buffer is returned.
246- * Otherwise, the contents are re-wrapped under the expected directory name.
266+ * The input buffer should contain archive entries without a root directory
267+ * (e.g. `libraries/mylib/library.xml`). The output will have all entries
268+ * nested under `archiveDirName/` (e.g. `archiveDirName/libraries/mylib/library.xml`).
247269 */
248- async function ensureArchiveStructure (
270+ async function wrapArchiveContents (
249271 buffer : Buffer ,
250272 archiveDirName : string ,
251273 logger : ReturnType < typeof getLogger > ,
252274) : Promise < Buffer > {
253- let zip : JSZip ;
254- try {
255- zip = await JSZip . loadAsync ( buffer ) ;
256- } catch {
257- // If we can't parse the zip, pass it through as-is
258- logger . debug ( 'Could not parse zip buffer for structure check; passing through as-is' ) ;
259- return buffer ;
260- }
261-
262- // Determine the unique top-level directory names
263- const topLevelEntries = new Set < string > ( ) ;
264- for ( const filePath of Object . keys ( zip . files ) ) {
265- const topLevel = filePath . split ( '/' ) [ 0 ] ;
266- topLevelEntries . add ( topLevel ) ;
267- }
268-
269- if ( topLevelEntries . size === 1 && topLevelEntries . has ( archiveDirName ) ) {
270- return buffer ; // Already correctly structured
271- }
272-
273- // Re-wrap all entries under archiveDirName/
274- logger . debug (
275- { archiveDirName, topLevelEntries : [ ...topLevelEntries ] } ,
276- `Re-wrapping archive contents under ${ archiveDirName } /` ,
277- ) ;
275+ const zip = await JSZip . loadAsync ( buffer ) ;
278276
279- // When a single top-level directory exists with a different name, strip it
280- // to avoid nesting (e.g. newRoot/oldRoot/...).
281- const stripPrefix = topLevelEntries . size === 1 ? [ ...topLevelEntries ] [ 0 ] + '/' : undefined ;
277+ logger . debug ( { archiveDirName} , `Wrapping archive contents under ${ archiveDirName } /` ) ;
282278
283279 const newZip = new JSZip ( ) ;
284280 const rootFolder = newZip . folder ( archiveDirName ) ! ;
285281
286282 for ( const [ filePath , entry ] of Object . entries ( zip . files ) ) {
287283 if ( ! entry . dir ) {
288284 const content = await entry . async ( 'nodebuffer' ) ;
289- const adjustedPath =
290- stripPrefix && filePath . startsWith ( stripPrefix ) ? filePath . slice ( stripPrefix . length ) : filePath ;
291- rootFolder . file ( adjustedPath , content ) ;
285+ rootFolder . file ( filePath , content ) ;
292286 }
293287 }
294288
0 commit comments