Skip to content

Commit 2f8e35c

Browse files
committed
fix: output-dir for hidden directory
1 parent a94c413 commit 2f8e35c

File tree

2 files changed

+88
-14
lines changed

2 files changed

+88
-14
lines changed

src/commands/project/retrieve/start.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,16 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
176176
// eslint-disable-next-line complexity
177177
public async run(): Promise<RetrieveResultJson> {
178178
const { flags } = await this.parse(RetrieveMetadata);
179+
179180
let resolvedTargetDir: string | undefined;
180181
if (flags['output-dir']) {
181182
resolvedTargetDir = resolve(flags['output-dir']);
182-
if (SfProject.getInstance()?.getPackageNameFromPath(resolvedTargetDir)) {
183+
184+
await fs.promises.mkdir(resolvedTargetDir, { recursive: true });
185+
186+
const packageName = SfProject.getInstance()?.getPackageNameFromPath(resolvedTargetDir);
187+
188+
if (packageName && !flags['output-dir'].startsWith('.')) {
183189
throw messages.createError('retrieveTargetDirOverlapsPackage', [flags['output-dir']]);
184190
}
185191
}
@@ -271,9 +277,42 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
271277

272278
this.ms.stop();
273279

274-
// flags['output-dir'] will set resolvedTargetDir var, so this check is redundant, but allows for nice typings in the moveResultsForRetrieveTargetDir method
275280
if (flags['output-dir'] && resolvedTargetDir) {
276-
await this.moveResultsForRetrieveTargetDir(flags['output-dir'], resolvedTargetDir);
281+
// Determine the actual source directory where files were retrieved
282+
const actualSourceDir = flags['output-dir'].startsWith('.')
283+
? join((await SfProject.resolve()).getPath(), 'tempRetrieve')
284+
: resolvedTargetDir;
285+
286+
const mainDefaultPath = join(actualSourceDir, 'main', 'default');
287+
const hasMainDefaultStructure = await fs.promises
288+
.access(mainDefaultPath)
289+
.then(() => true)
290+
.catch(() => false);
291+
292+
if (hasMainDefaultStructure) {
293+
await this.moveResultsForRetrieveTargetDir(flags['output-dir'], actualSourceDir);
294+
295+
// Update file response paths to reflect the actual destination for hidden directories
296+
if (flags['output-dir'].startsWith('.')) {
297+
const outputDir = flags['output-dir'];
298+
this.retrieveResult.getFileResponses().forEach((fileResponse) => {
299+
if (fileResponse.filePath?.includes('tempRetrieve/')) {
300+
fileResponse.filePath = fileResponse.filePath.replace('tempRetrieve/', `${outputDir}/`);
301+
}
302+
});
303+
}
304+
305+
if (flags['output-dir'].startsWith('.')) {
306+
await fs.promises.rm(actualSourceDir, { recursive: true });
307+
} else {
308+
this.retrieveResult.getFileResponses().forEach((fileResponse) => {
309+
// Update the path to reflect the actual location
310+
if (fileResponse.filePath?.includes('main/default/')) {
311+
fileResponse.filePath = fileResponse.filePath.replace(/.*main\/default\//, '');
312+
}
313+
});
314+
}
315+
}
277316
}
278317

279318
// reference the flag instead of `format` so we get correct type
@@ -362,7 +401,8 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
362401
await promisesQueue(
363402
files,
364403
async (file: string): Promise<string> => {
365-
const dest = join(src.replace(join('main', 'default'), ''), file);
404+
const relativePath = src.replace(join(resolvedTargetDir, 'main', 'default'), '');
405+
const dest = join(targetDir, relativePath, file);
366406
const destDir = dirname(dest);
367407
await fs.promises.mkdir(destDir, { recursive: true });
368408
await fs.promises.rename(join(src, file), dest);
@@ -387,8 +427,16 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
387427
});
388428
// move contents of 'main/default' to 'retrievetargetdir'
389429
await promisesQueue([join(resolvedTargetDir, 'main', 'default')], mv, 5, true);
390-
// remove 'main/default'
391-
await fs.promises.rm(join(targetDir, 'main'), { recursive: true });
430+
431+
try {
432+
await fs.promises.access(join(targetDir, 'main'));
433+
// remove 'main/default'
434+
await fs.promises.rm(join(targetDir, 'main'), { recursive: true });
435+
} catch (e) {
436+
const err = SfError.wrap(e);
437+
getLogger().debug(`Error directory does not exist: ${err.message}
438+
Due to: ${err.stack ?? 'unknown (no error stack)'}`);
439+
}
392440
}
393441
}
394442

@@ -518,6 +566,18 @@ const buildRetrieveOptions = async (
518566
output: string | undefined
519567
): Promise<RetrieveSetOptions> => {
520568
const apiVersion = await resolveApiVersion(flags);
569+
570+
let retrieveOutput: string;
571+
572+
if (flags['output-dir'] && format === 'source' && flags['output-dir'].startsWith('.')) {
573+
// Use a temporary regular directory for hidden directories
574+
const projectRoot = await SfProject.resolve();
575+
retrieveOutput = join(projectRoot.getPath(), 'tempRetrieve');
576+
await fs.promises.mkdir(retrieveOutput, { recursive: true });
577+
} else {
578+
retrieveOutput = output ?? (await SfProject.resolve()).getDefaultPackage().fullPath;
579+
}
580+
521581
return {
522582
usernameOrConnection: flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
523583
merge: true,
@@ -533,7 +593,7 @@ const buildRetrieveOptions = async (
533593
output: flags['target-metadata-dir'] as string,
534594
}
535595
: {
536-
output: output ?? (await SfProject.resolve()).getDefaultPackage().fullPath,
596+
output: retrieveOutput,
537597
}),
538598
};
539599
};

test/commands/retrieve/start.test.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { resolve } from 'node:path';
9-
8+
import { resolve, join } from 'node:path';
109
import fs from 'node:fs/promises';
1110
import { Stats } from 'node:fs';
1211
import sinon from 'sinon';
@@ -144,10 +143,6 @@ describe('project retrieve start', () => {
144143
ensureRetrieveArgs({ format: 'source' });
145144
});
146145
it('should pass along retrievetargetdir', async () => {
147-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
148-
// @ts-ignore
149-
const renameStub = $$.SANDBOX.stub(RetrieveMetadata.prototype, 'moveResultsForRetrieveTargetDir').resolves();
150-
151146
const sourcepath = ['somepath'];
152147
const metadata = ['ApexClass:MyClass'];
153148
const result = await RetrieveMetadata.run(['--output-dir', sourcepath[0], '--metadata', metadata[0], '--json']);
@@ -160,7 +155,26 @@ describe('project retrieve start', () => {
160155
},
161156
});
162157
ensureRetrieveArgs({ output: resolve(sourcepath[0]), format: 'source' });
163-
expect(renameStub.calledOnce).to.be.true;
158+
});
159+
160+
it.only('should pass along retrievetargetdir with hidden directory', async () => {
161+
const sourcepath = ['.hidden'];
162+
const metadata = ['ApexClass:MyClass'];
163+
164+
// For hidden directories, uses a temp directory
165+
const projectRoot = SfProject.getInstance();
166+
const tempRetrievePath = join(projectRoot.getPath(), 'tempRetrieve');
167+
168+
const result = await RetrieveMetadata.run(['--output-dir', sourcepath[0], '--metadata', metadata[0], '--json']);
169+
expect(result).to.deep.equal(expectedResults);
170+
ensureCreateComponentSetArgs({
171+
sourcepath: undefined,
172+
metadata: {
173+
directoryPaths: [],
174+
metadataEntries: ['ApexClass:MyClass'],
175+
},
176+
});
177+
ensureRetrieveArgs({ output: tempRetrievePath, format: 'source' });
164178
});
165179

166180
it('should pass along metadata', async () => {

0 commit comments

Comments
 (0)