Skip to content

Commit 46f3b42

Browse files
committed
fix review findings: config reset, writeFile flags, archive stripping, plugin opts
- Refresh content tree on config reset (matching webdav-tree pattern) - Honour create/overwrite flags in WebDAV writeFile per VS Code contract - Strip single existing top-level root in ensureArchiveStructure to avoid double-nesting when re-wrapping site archives - Add accountManagerHost to PluginHookOptions and resolveOptions - Use Uri.from instead of Uri.parse for correct special-char handling - Guard applyMiddleware against duplicate registration
1 parent 5a50f7c commit 46f3b42

6 files changed

Lines changed: 94 additions & 8 deletions

File tree

packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,13 +276,19 @@ async function ensureArchiveStructure(
276276
`Re-wrapping archive contents under ${archiveDirName}/`,
277277
);
278278

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;
282+
279283
const newZip = new JSZip();
280284
const rootFolder = newZip.folder(archiveDirName)!;
281285

282286
for (const [filePath, entry] of Object.entries(zip.files)) {
283287
if (!entry.dir) {
284288
const content = await entry.async('nodebuffer');
285-
rootFolder.file(filePath, content);
289+
const adjustedPath =
290+
stripPrefix && filePath.startsWith(stripPrefix) ? filePath.slice(stripPrefix.length) : filePath;
291+
rootFolder.file(adjustedPath, content);
286292
}
287293
}
288294

packages/b2c-tooling-sdk/src/plugins/manager.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface PluginHookOptions {
2525
instance?: string;
2626
/** Explicit config file path (if known) */
2727
configPath?: string;
28+
/** Account Manager host override */
29+
accountManagerHost?: string;
2830
/** CLI flags or equivalent options */
2931
flags?: Record<string, unknown>;
3032
}
@@ -38,6 +40,7 @@ export interface PluginHookOptions {
3840
*/
3941
export class B2CPluginManager {
4042
private _initialized = false;
43+
private _middlewareApplied = false;
4144
private _pluginNames: string[] = [];
4245
private _sourcesBefore: ConfigSource[] = [];
4346
private _sourcesAfter: ConfigSource[] = [];
@@ -84,6 +87,7 @@ export class B2CPluginManager {
8487
resolveOptions: {
8588
instance: hookOptions?.instance,
8689
configPath: hookOptions?.configPath,
90+
accountManagerHost: hookOptions?.accountManagerHost,
8791
},
8892
};
8993
const result = await invokeHook<ConfigSourcesHookResult>(absPath, context, options, this.logger);
@@ -173,6 +177,9 @@ export class B2CPluginManager {
173177
* Registers collected middleware providers with the global registries.
174178
*/
175179
applyMiddleware(): void {
180+
if (this._middlewareApplied) return;
181+
this._middlewareApplied = true;
182+
176183
for (const provider of this._httpMiddleware) {
177184
globalMiddlewareRegistry.register(provider);
178185
this.logger?.debug(`Registered HTTP middleware provider: ${provider.name}`);

packages/b2c-tooling-sdk/test/operations/jobs/site-archive.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as path from 'node:path';
1414
import {WebDavClient} from '../../../src/clients/webdav.js';
1515
import {createOcapiClient} from '../../../src/clients/ocapi.js';
1616
import {MockAuthStrategy} from '../../helpers/mock-auth.js';
17+
import JSZip from 'jszip';
1718
import {
1819
siteArchiveImport,
1920
siteArchiveExport,
@@ -268,6 +269,62 @@ describe('operations/jobs/site-archive', () => {
268269
expect(deleteRequested).to.be.false;
269270
});
270271

272+
it('should strip single existing top-level root when re-wrapping archive', async () => {
273+
// Create a zip with a different top-level directory name
274+
const srcZip = new JSZip();
275+
srcZip.file('oldRoot/meta/system-objecttype-extensions.xml', '<metadata/>');
276+
srcZip.file('oldRoot/sites/RefArch/site.xml', '<site/>');
277+
const zipBuffer = await srcZip.generateAsync({type: 'nodebuffer'});
278+
279+
let uploadedZip: Buffer | null = null;
280+
281+
server.use(
282+
http.all(`${WEBDAV_BASE}/*`, async ({request}) => {
283+
const url = new URL(request.url);
284+
if (request.method === 'PUT' && url.pathname.includes('Impex/src/instance/')) {
285+
uploadedZip = Buffer.from(await request.arrayBuffer());
286+
return new HttpResponse(null, {status: 201});
287+
}
288+
if (request.method === 'DELETE') {
289+
return new HttpResponse(null, {status: 204});
290+
}
291+
return new HttpResponse(null, {status: 404});
292+
}),
293+
http.post(`${OCAPI_BASE}/jobs/sfcc-site-archive-import/executions`, () => {
294+
return HttpResponse.json({
295+
id: 'exec-rewrap',
296+
execution_status: 'finished',
297+
exit_status: {code: 'OK'},
298+
});
299+
}),
300+
http.get(`${OCAPI_BASE}/jobs/sfcc-site-archive-import/executions/exec-rewrap`, () => {
301+
return HttpResponse.json({
302+
id: 'exec-rewrap',
303+
execution_status: 'finished',
304+
exit_status: {code: 'OK'},
305+
is_log_file_existing: false,
306+
});
307+
}),
308+
);
309+
310+
await siteArchiveImport(mockInstance, zipBuffer, {
311+
archiveName: 'my-import',
312+
waitOptions: FAST_WAIT_OPTIONS,
313+
});
314+
315+
expect(uploadedZip).to.not.be.null;
316+
317+
// Verify the uploaded archive has the correct structure:
318+
// my-import/meta/... and my-import/sites/... (not my-import/oldRoot/...)
319+
const resultZip = await JSZip.loadAsync(uploadedZip!);
320+
const paths = Object.keys(resultZip.files).filter((p) => !resultZip.files[p].dir);
321+
expect(paths).to.include('my-import/meta/system-objecttype-extensions.xml');
322+
expect(paths).to.include('my-import/sites/RefArch/site.xml');
323+
// Ensure the old root was stripped
324+
const hasOldRoot = paths.some((p) => p.includes('oldRoot'));
325+
expect(hasOldRoot).to.be.false;
326+
});
327+
271328
it('should throw error when archiveName is missing for Buffer', async () => {
272329
const zipBuffer = Buffer.from('PK\x03\x04test-data');
273330

packages/b2c-vs-extension/src/content-tree/content-fs-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function parseContentUri(uri: vscode.Uri): ParsedContentUri {
3737

3838
export function contentItemUri(libraryId: string, isSiteLibrary: boolean, contentId: string): vscode.Uri {
3939
const uriPath = isSiteLibrary ? `/site/${libraryId}/${contentId}.xml` : `/${libraryId}/${contentId}.xml`;
40-
return vscode.Uri.parse(`${CONTENT_SCHEME}:${uriPath}`);
40+
return vscode.Uri.from({scheme: CONTENT_SCHEME, path: uriPath});
4141
}
4242

4343
/**

packages/b2c-vs-extension/src/content-tree/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,10 @@ export function registerContentTree(context: vscode.ExtensionContext, configProv
3535

3636
const commandDisposables = registerContentCommands(context, contentConfig, treeProvider, fsProvider);
3737

38+
configProvider.onDidReset(() => {
39+
contentConfig.clearCache();
40+
treeProvider.refresh();
41+
});
42+
3843
context.subscriptions.push(fsRegistration, treeView, ...commandDisposables);
3944
}

packages/b2c-vs-extension/src/webdav-tree/webdav-fs-provider.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function uriToWebdavPath(uri: vscode.Uri): string {
5252

5353
/** Build a b2c-webdav URI from a WebDAV path. */
5454
export function webdavPathToUri(webdavPath: string): vscode.Uri {
55-
return vscode.Uri.parse(`${WEBDAV_SCHEME}:/${webdavPath}`);
55+
return vscode.Uri.from({scheme: WEBDAV_SCHEME, path: `/${webdavPath}`});
5656
}
5757

5858
function isStale(timestamp: number): boolean {
@@ -203,17 +203,28 @@ export class WebDavFileSystemProvider implements vscode.FileSystemProvider {
203203
}
204204
}
205205

206-
async writeFile(
207-
uri: vscode.Uri,
208-
content: Uint8Array,
209-
_options: {create: boolean; overwrite: boolean},
210-
): Promise<void> {
206+
async writeFile(uri: vscode.Uri, content: Uint8Array, options: {create: boolean; overwrite: boolean}): Promise<void> {
211207
const webdavPath = uriToWebdavPath(uri);
212208
const instance = this.configProvider.getInstance();
213209
if (!instance) {
214210
throw vscode.FileSystemError.Unavailable('No B2C Commerce instance configured');
215211
}
216212

213+
// Honour create/overwrite flags per VS Code FileSystemProvider contract
214+
let exists = false;
215+
try {
216+
await this.stat(uri);
217+
exists = true;
218+
} catch {
219+
// stat throws FileNotFound when file doesn't exist
220+
}
221+
if (exists && !options.overwrite) {
222+
throw vscode.FileSystemError.FileExists(uri);
223+
}
224+
if (!exists && !options.create) {
225+
throw vscode.FileSystemError.FileNotFound(uri);
226+
}
227+
217228
try {
218229
const ext = path.extname(webdavPath).toLowerCase();
219230
const contentType = MIME_BY_EXT[ext];

0 commit comments

Comments
 (0)