sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
196 lines (188 loc) • 10.4 kB
JavaScript
/* jscpd:ignore-start */
import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import c from 'chalk';
import * as child from 'child_process';
import fs from 'fs-extra';
import * as path from 'path';
import * as util from 'util';
const exec = util.promisify(child.exec);
import { MetadataUtils } from '../../../../../common/metadata-utils/index.js';
import { uxLog } from '../../../../../common/utils/index.js';
import { WebSocketClient } from '../../../../../common/websocketClient.js';
import { setConfig } from '../../../../../config/index.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
export default class DxSources extends SfCommand {
static title = 'Retrieve sfdx sources from org';
static description = `
## Command Behavior
**Retrieves Salesforce metadata from an org and converts it into Salesforce DX (SFDX) source format.**
This command provides a flexible way to pull metadata from any Salesforce org into your local SFDX project. It's particularly useful for:
- **Initial Project Setup:** Populating a new SFDX project with existing org metadata.
- **Environment Synchronization:** Bringing changes from a Salesforce org (e.g., a sandbox) into your local development environment.
- **Selective Retrieval:** Allows you to specify which metadata types to retrieve, or to filter out certain types.
- **Org Shape Creation:** Can optionally create an org shape, which is useful for defining the characteristics of scratch orgs.
Key functionalities:
- **Metadata Retrieval:** Connects to a target Salesforce org and retrieves metadata based on specified filters.
- **MDAPI to SFDX Conversion:** Converts the retrieved metadata from Metadata API format to SFDX source format.
- **Org Shape Generation (Optional):** If the \`--shape\` flag is used, it also captures the org's shape and stores installed package information.
- **Temporary File Management:** Uses temporary folders for intermediate steps, ensuring a clean working directory.
<details markdown="1">
<summary>Technical explanations</summary>
The command's technical implementation involves:
- **Temporary Directory Management:** It creates and manages temporary directories (\`./tmp\`, \`mdapipkg\`, \`sfdx-project\`) to stage the retrieved metadata and the converted SFDX sources.
- **\`MetadataUtils.retrieveMetadatas\`:** This utility is used to connect to the Salesforce org and retrieve metadata in Metadata API format. It supports filtering by metadata types and excluding certain items.
- **SFDX Project Creation:** It executes \`sf project generate\` to create a new SFDX project structure within a temporary directory.
- **MDAPI to SFDX Conversion:** It then uses \`sf project convert mdapi\` to convert the retrieved metadata from the MDAPI format to the SFDX source format.
- **File System Operations:** It uses \`fs-extra\` to copy the converted SFDX sources to the main project folder, while preserving important project files like \`.gitignore\` and \`sfdx-project.json\`.
- **Org Shape Handling:** If \`--shape\` is enabled, it copies the generated \`package.xml\` and stores information about installed packages using \`setConfig\`.
- **Error Handling:** Includes robust error handling for Salesforce CLI commands and file system operations.
- **WebSocket Communication:** Uses \`WebSocketClient.sendRefreshCommandsMessage\` to notify connected VS Code clients about changes to the project.
</details>
`;
static examples = ['$ sf hardis:org:retrieve:sources:dx'];
static flags = {
folder: Flags.string({
char: 'f',
default: '.',
description: messages.getMessage('folder'),
}),
tempfolder: Flags.string({
char: 't',
default: './tmp',
description: messages.getMessage('tempFolder'),
}),
keepmetadatatypes: Flags.string({
char: 'k',
description: 'Comma separated list of metadatas types that will be the only ones to be retrieved',
}),
filteredmetadatas: Flags.string({
char: 'm',
description: messages.getMessage('filteredMetadatas'),
}),
shape: Flags.boolean({
char: 's',
default: false,
description: messages.getMessage('createOrgShape'),
}),
instanceurl: Flags.string({
char: 'r',
description: messages.getMessage('instanceUrl'),
}),
debug: Flags.boolean({
char: 'd',
default: false,
description: messages.getMessage('debugMode'),
}),
websocket: Flags.string({
description: messages.getMessage('websocket'),
}),
skipauth: Flags.boolean({
description: 'Skip authentication check when a default username is required',
}),
'target-org': requiredOrgFlagWithDeprecations,
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = false;
/* jscpd:ignore-end */
async run() {
const { flags } = await this.parse(DxSources);
const folder = path.resolve(flags.folder || '.');
const tempFolder = path.resolve(flags.tempfolder || './tmp');
const keepMetadataTypes = flags.keepmetadatatypes ? flags.keepmetadatatypes.split(',') : [];
const filteredMetadatas = flags.filteredmetadatas
? flags.filteredmetadatas.split(',')
: MetadataUtils.listMetadatasNotManagedBySfdx();
const shapeFlag = flags.shape || false;
const debug = flags.debug || false;
// Create working temp folders and define it as cwd
const prevCwd = process.cwd();
await fs.ensureDir(tempFolder);
await fs.emptyDir(tempFolder);
process.chdir(tempFolder);
const metadataFolder = path.join(tempFolder, 'mdapipkg');
await fs.ensureDir(metadataFolder);
await fs.emptyDir(metadataFolder);
const sfdxFolder = path.join(tempFolder, 'sfdx-project');
await fs.ensureDir(sfdxFolder);
await fs.emptyDir(sfdxFolder);
// Retrieve metadatas
const retrieveOptions = { filterManagedItems: true, removeStandard: false };
if (keepMetadataTypes) {
retrieveOptions.keepMetadataTypes = keepMetadataTypes;
}
const packageXml = path.resolve(path.join(tempFolder, 'package.xml'));
await MetadataUtils.retrieveMetadatas(packageXml, metadataFolder, true, filteredMetadatas, retrieveOptions, this, flags['target-org'].getUsername(), debug);
// Create sfdx project
if (fs.readdirSync(sfdxFolder).length === 0) {
uxLog("action", this, c.cyan('Creating SFDX project...'));
const projectCreateCommand = 'sf project generate --name "sfdx-project"';
uxLog("other", this, `[command] ${c.bold(c.grey(projectCreateCommand))}`);
const createProjectRes = await exec(projectCreateCommand, { maxBuffer: 1024 * 2000 });
if (debug) {
uxLog("other", this, createProjectRes.stdout + createProjectRes.stderr);
}
}
// Converting metadatas to sfdx
uxLog("action", this, c.cyan(`Converting metadatas into SFDX sources in ${c.green(sfdxFolder)}...`));
process.chdir(sfdxFolder);
const mdapiConvertCommand = `sf project convert mdapi --root-dir ${path.join(metadataFolder, 'unpackaged')} ${debug ? '--verbose' : ''}`;
uxLog("other", this, `[command] ${c.bold(c.grey(mdapiConvertCommand))}`);
try {
const convertRes = await exec(mdapiConvertCommand, {
maxBuffer: 10000 * 10000,
});
if (debug) {
uxLog("other", this, convertRes.stdout + convertRes.stderr);
}
}
catch (e) {
throw new SfError(JSON.stringify(e, null, 2));
}
// Move sfdx sources in main folder
uxLog("other", this, `[sfdx-hardis] Moving temp files to main folder ${c.green(path.resolve(folder))}...`);
process.chdir(prevCwd);
// Do not replace files if already defined
const filesToNotReplace = ['.gitignore', '.forceignore', 'sfdx-project.json', 'README.md'];
for (const fileToNotReplace of filesToNotReplace) {
if (fs.existsSync(path.join(path.resolve(folder), fileToNotReplace)) &&
fs.existsSync(path.join(sfdxFolder, fileToNotReplace))) {
await fs.remove(path.join(sfdxFolder, fileToNotReplace));
}
}
// Copy files
await fs.copy(sfdxFolder, path.resolve(folder));
// Manage org shape if requested
if (shapeFlag === true) {
// Copy package.xml
const packageXmlInConfig = path.resolve(folder) + '/manifest/package.xml'; // '/config/package.xml';
if (!fs.existsSync(packageXmlInConfig)) {
await fs.ensureDir(path.dirname(packageXmlInConfig));
uxLog("other", this, `[sfdx-hardis] Copying package.xml manifest ${c.green(packageXmlInConfig)}...`);
await fs.copy(packageXml, packageXmlInConfig);
}
// Store list of installed packages
const installedPackages = await MetadataUtils.listInstalledPackages(null, this);
await setConfig('project', {
installedPackages,
});
}
// Remove temporary files
uxLog("other", this, `Remove temporary folder ${tempFolder} ...`);
try {
await fs.rm(tempFolder, { recursive: true });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
uxLog("warning", this, c.yellow(`Unable to remove folder ${tempFolder}, please delete it manually`));
}
// Trigger commands refresh on VS Code WebSocket Client
WebSocketClient.sendRefreshCommandsMessage();
// Set bac initial cwd
const message = `[sfdx-hardis] Successfully retrieved sfdx project in ${folder}`;
uxLog("success", this, c.green(message));
return { orgId: flags['target-org'].getOrgId(), outputString: message };
}
}
//# sourceMappingURL=dx.js.map