diff --git a/README.md b/README.md index 48cfc07..ccd7879 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,12 @@ Combine Salesforce manifest files together. ``` USAGE - $ sf sfpc combine [--json] [-f ] + $ sf sfpc combine [-f ] [-d ] [-c ] [--json] FLAGS -f, --package-file= The path to an existing package.xml file. This flag can be specified multiple times. + -d, --directory= The path to an existing directory with package.xml files. Only XML files in the immediate directory will be scanned. + This flag can be specified multiple times. -c, --combined-package= The path to save the combined package.xml to. Default name is "combinedPackage.xml" in the running directory. @@ -58,6 +60,10 @@ EXAMPLES Combine pack1.xml and pack2.xml into package.xml $ sf sfpc combine -f pack1.xml -f pack2.xml -c package.xml + + Combine pack1.xml, pack2.xml, and a directory with package XML files into package.xml + + $ sf sfpc combine -f pack1.xml -f pack2.xml -d "test/sample_dir" -c package.xml ``` diff --git a/messages/sfpc.combine.md b/messages/sfpc.combine.md index c371b47..930e74e 100644 --- a/messages/sfpc.combine.md +++ b/messages/sfpc.combine.md @@ -9,6 +9,7 @@ Read multiple package.xml files, then parse them and combine them to create 1 fi # examples - sf sfpc combine -f pack1.xml -f pack2.xml -c package.xml +- sf sfpc combine -f pack1.xml -d "test/directory" -c package.xml # flags.package-file.summary @@ -17,3 +18,7 @@ Path to a package.xml file. # flags.combined-package.summary Combined package file path. + +# flags.directory.summary + +Directory to look for package.xml files in. diff --git a/src/commands/sfpc/combine.ts b/src/commands/sfpc/combine.ts index bd80e15..8661caa 100644 --- a/src/commands/sfpc/combine.ts +++ b/src/commands/sfpc/combine.ts @@ -6,6 +6,7 @@ import { Messages } from '@salesforce/core'; import { PackageXmlObject, SfpcCombineResult } from '../../helpers/types.js'; import { buildPackage } from '../../helpers/buildPackage.js'; import { readPackageFiles } from '../../helpers/readPackageFiles.js'; +import { findFilesInDirectory } from '../../helpers/findFilesinDirectory.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sf-package-combiner', 'sfpc.combine'); @@ -28,22 +29,37 @@ export default class SfpcCombine extends SfCommand { exists: false, default: 'combinedPackage.xml', }), + directory: Flags.directory({ + summary: messages.getMessage('flags.directory.summary'), + char: 'd', + multiple: true, + exists: true, + }), }; public async run(): Promise { const { flags } = await this.parse(SfpcCombine); - const files = flags['package-file'] ?? null; + const files = flags['package-file'] ?? []; const combinedPackage = flags['combined-package']; + const directories = flags['directory'] ?? null; let packageContents: PackageXmlObject[] = []; let apiVersions: string[] = []; - let warnings: string[] = []; + const warnings: string[] = []; + + // Search directories for XML files + // Process directories to find XML files + if (directories && directories.length > 0) { + const { files: dirFiles, warnings: dirWarnings } = await findFilesInDirectory(directories); + files.push(...dirFiles); + warnings.push(...dirWarnings); + } // Load XML content from each file const result = await readPackageFiles(files); packageContents = result.packageContents; apiVersions = result.apiVersions; - warnings = result.warnings; + warnings.push(...result.warnings); // Print warnings if any if (warnings.length > 0) { diff --git a/src/helpers/findFilesinDirectory.ts b/src/helpers/findFilesinDirectory.ts new file mode 100644 index 0000000..dce5027 --- /dev/null +++ b/src/helpers/findFilesinDirectory.ts @@ -0,0 +1,21 @@ +/* eslint-disable no-await-in-loop */ +import { readdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +export async function findFilesInDirectory(directories: string[]): Promise<{ files: string[]; warnings: string[] }> { + const files: string[] = []; + const warnings: string[] = []; + for (const dir of directories) { + try { + const dirFiles = await readdir(dir, { withFileTypes: true }); + const xmlFiles = dirFiles + .filter((file) => file.isFile() && file.name.endsWith('.xml')) + .map((file) => join(dir, file.name)); + files.push(...xmlFiles); + } catch (error) { + warnings.push(`Failed to read directory ${dir}`); + } + } + + return { files, warnings }; +} diff --git a/test/commands/sfpc/combine.test.ts b/test/commands/sfpc/combine.test.ts index ac597fa..4dde85f 100644 --- a/test/commands/sfpc/combine.test.ts +++ b/test/commands/sfpc/combine.test.ts @@ -19,6 +19,9 @@ describe('sfpc combine', () => { const outputPackage = resolve('package.xml'); const baseline = resolve('test/samples/combinedPackage.xml'); const emptyPackageBaseline = resolve('test/samples/emptyPackage.xml'); + const packageDir = resolve('test/samples/dir_sample'); + const invalidDirPackage = resolve('test/samples/dir_sample/invalid1.xml'); + const dirBaseline = resolve('test/samples/combinedPackageDir.xml'); beforeEach(() => { sfCommandStubs = stubSfCommandUx($$.SANDBOX); @@ -111,4 +114,22 @@ describe('sfpc combine', () => { `File content is different between ${outputPackage} and ${emptyPackageBaseline}` ); }); + it('combine the valid packages together including the ones in a directory.', async () => { + await SfpcCombine.run(['-f', package1, '-f', package2, '-f', package3, '-d', packageDir, '-c', outputPackage]); + const output = sfCommandStubs.log + .getCalls() + .flatMap((c) => c.args) + .join('\n'); + expect(output).to.include(`Combined package.xml written to: ${outputPackage}`); + const warnings = sfCommandStubs.warn + .getCalls() + .flatMap((c) => c.args) + .join('\n'); + expect(warnings).to.include(`File ${invalidDirPackage} does not match expected Salesforce package structure.`); + }); + it('confirm the package created in the previous test using a directory is the same as the baseline.', async () => { + const testPackage = await readFile(outputPackage, 'utf-8'); + const baselinePackage = await readFile(dirBaseline, 'utf-8'); + strictEqual(testPackage, baselinePackage, `File content is different between ${outputPackage} and ${dirBaseline}`); + }); }); diff --git a/test/samples/combinedPackageDir.xml b/test/samples/combinedPackageDir.xml new file mode 100644 index 0000000..b29c8c6 --- /dev/null +++ b/test/samples/combinedPackageDir.xml @@ -0,0 +1,19 @@ + + + + Auto + customlabel + + + ABC + Account + Case + customobject + + + Glengarry_Leadz + Industry + standardvalueset + + 59.0 + diff --git a/test/samples/dir_sample/invalid1.xml b/test/samples/dir_sample/invalid1.xml new file mode 100644 index 0000000..7d65b93 --- /dev/null +++ b/test/samples/dir_sample/invalid1.xml @@ -0,0 +1,10 @@ + + + + Lead.Always_Be_Closing__c + Account.Coffee_is_for_closers__c + CustomField + + 59.0 + 57.0 + \ No newline at end of file diff --git a/test/samples/dir_sample/pack1.xml b/test/samples/dir_sample/pack1.xml new file mode 100644 index 0000000..641e293 --- /dev/null +++ b/test/samples/dir_sample/pack1.xml @@ -0,0 +1,8 @@ + + + + ABC + CustomObject + + 57.0 + \ No newline at end of file diff --git a/test/samples/dir_sample/pack2.xml b/test/samples/dir_sample/pack2.xml new file mode 100644 index 0000000..b3e94c2 --- /dev/null +++ b/test/samples/dir_sample/pack2.xml @@ -0,0 +1,8 @@ + + + + Glengarry_Leadz + StandardValueSet + + 59.0 + \ No newline at end of file