@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
258 lines • 16.9 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.VirtualTreeContainer = exports.ZipTreeContainer = exports.NodeFSTreeContainer = exports.TreeContainer = void 0;
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/* eslint-disable class-methods-use-this */
const node_path_1 = require("node:path");
const node_stream_1 = require("node:stream");
const graceful_fs_1 = require("graceful-fs");
const jszip_1 = __importDefault(require("jszip"));
const core_1 = require("@salesforce/core");
const ts_types_1 = require("@salesforce/ts-types");
const path_1 = require("../utils/path");
;
const messages = new core_1.Messages('@salesforce/source-deploy-retrieve', 'sdr', new Map([["md_request_fail", "Metadata API request failed: %s"], ["error_convert_invalid_format", "Invalid conversion format '%s'"], ["error_could_not_infer_type", "%s: Could not infer a metadata type"], ["error_unexpected_child_type", "Unexpected child metadata [%s] found for parent type [%s]"], ["noParent", "Could not find parent type for %s (%s)"], ["error_expected_source_files", "%s: Expected source files for type '%s'"], ["error_failed_convert", "Component conversion failed: %s"], ["error_merge_metadata_target_unsupported", "Merge convert for metadata target format currently unsupported"], ["error_missing_adapter", "Missing adapter '%s' for metadata type '%s'"], ["error_missing_transformer", "Missing transformer '%s' for metadata type '%s'"], ["error_missing_type_definition", "Missing metadata type definition in registry for id '%s'."], ["error_missing_child_type_definition", "Type %s does not have a child type definition %s."], ["noChildTypes", "No child types found in registry for %s (reading %s at %s)"], ["error_no_metadata_xml_ignore", "Metadata xml file %s is forceignored but is required for %s."], ["noSourceIgnore", "%s metadata types require source files, but %s is forceignored."], ["noSourceIgnore.actions", "- Metadata types with content are composed of two files: a content file (ie MyApexClass.cls) and a -meta.xml file (i.e MyApexClass.cls-meta.xml). You must include both files in your .forceignore file. Or try appending \u201C\\*\u201D to your existing .forceignore entry.\n\nSee <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm> for examples"], ["error_path_not_found", "%s: File or folder not found"], ["noContentFound", "SourceComponent %s (metadata type = %s) is missing its content file."], ["noContentFound.actions", ["Ensure the content file exists in the expected location.", "If the content file is in your .forceignore file, ensure the meta-xml file is also ignored to completely exclude it."]], ["error_parsing_xml", "SourceComponent %s (metadata type = %s) does not have an associated metadata xml to parse"], ["error_expected_file_path", "%s: path is to a directory, expected a file"], ["error_expected_directory_path", "%s: path is to a file, expected a directory"], ["error_directory_not_found_or_not_directory", "%s: path is not a directory"], ["error_no_directory_stream", "%s doesn't support readable streams on directories."], ["error_no_source_to_deploy", "No source-backed components present in the package."], ["error_no_components_to_retrieve", "No components in the package to retrieve."], ["error_static_resource_expected_archive_type", "A StaticResource directory must have a content type of application/zip or application/jar - found %s for %s."], ["error_static_resource_missing_resource_file", "A StaticResource must have an associated .resource file, missing %s.resource-meta.xml"], ["error_no_job_id", "The %s operation is missing a job ID. Initialize an operation with an ID, or start a new job."], ["missingApiVersion", "Could not determine an API version to use for the generated manifest. Tried looking for sourceApiVersion in sfdx-project.json, apiVersion from config vars, and the highest apiVersion from the APEX REST endpoint. Using API version 58.0 as a last resort."], ["invalid_xml_parsing", "error parsing %s due to:\\n message: %s\\n line: %s\\n code: %s"], ["zipBufferError", "Zip buffer was not created during conversion"], ["undefinedComponentSet", "Unable to construct a componentSet. Check the logs for more information."], ["replacementsFileNotRead", "The file \"%s\" specified in the \"replacements\" property of sfdx-project.json could not be read."], ["unsupportedBundleType", "Unsupported Bundle Type: %s"], ["filePathGeneratorNoTypeSupport", "Type not supported for filepath generation: %s"], ["missingFolderType", "The registry has %s as is inFolder but it does not have a folderType"], ["tooManyFiles", "Multiple files found for path: %s."], ["cantGetName", "Unable to calculate fullName from path: %s (%s)"], ["missingMetaFileSuffix", "The metadata registry is configured incorrectly for %s. Expected a metaFileSuffix."], ["uniqueIdElementNotInRegistry", "No uniqueIdElement found in registry for %s (reading %s at %s)."], ["uniqueIdElementNotInChild", "The uniqueIdElement %s was not found the child (reading %s at %s)."], ["suggest_type_header", "A metadata type lookup for \"%s\" found the following close matches:"], ["suggest_type_did_you_mean", "-- Did you mean \".%s%s\" instead for the \"%s\" metadata type?"], ["suggest_type_more_suggestions", "Additional suggestions:\nConfirm the file name, extension, and directory names are correct. Validate against the registry at:\n<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>\n\nIf the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:\n<https://developer.salesforce.com/docs/metadata-coverage>\n\nIf the type is available via Metadata API but not in the registry\n\n- Open an issue <https://github.com/forcedotcom/cli/issues>\n- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>"], ["type_name_suggestions", "Confirm the metadata type name is correct. Validate against the registry at:\n<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>\n\nIf the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:\n<https://developer.salesforce.com/docs/metadata-coverage>\n\nIf the type is available via Metadata API but not in the registry\n\n- Open an issue <https://github.com/forcedotcom/cli/issues>\n- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>"]]));
/**
* A container for interacting with a file system. Operations such as component resolution,
* conversion, and packaging perform I/O against `TreeContainer` abstractions.
*
* Extend this base class to implement a custom container.
*/
class TreeContainer {
/**
* Searches for a metadata component file in a container directory.
*
* @param fileType - The type of component file
* @param name - The name of the file without a suffix
* @param directory - The directory to search in
* @returns The first path that meets the criteria, or `undefined` if none were found
*/
find(fileType, name, directory) {
const fileName = this.readDirectory(directory).find((entry) => {
const parsed = (0, path_1.parseMetadataXml)((0, node_path_1.join)(directory, entry));
const metaXmlCondition = fileType === 'metadataXml' ? !!parsed : !parsed;
return (0, path_1.baseName)(entry) === name && metaXmlCondition;
});
if (fileName) {
return (0, node_path_1.join)(directory, fileName);
}
}
}
exports.TreeContainer = TreeContainer;
/**
* A {@link TreeContainer} that wraps the NodeJS `fs` module.
*/
class NodeFSTreeContainer extends TreeContainer {
isDirectory(fsPath) {
// use stat instead of lstat to follow symlinks
return (0, graceful_fs_1.statSync)(fsPath).isDirectory();
}
exists(fsPath) {
return (0, graceful_fs_1.existsSync)(fsPath);
}
readDirectory(fsPath) {
return (0, graceful_fs_1.readdirSync)(fsPath);
}
readFile(fsPath) {
// significant enough performance increase using sync instead of fs.promise version
return Promise.resolve((0, graceful_fs_1.readFileSync)(fsPath));
}
readFileSync(fsPath) {
return (0, graceful_fs_1.readFileSync)(fsPath);
}
stream(fsPath) {
if (!this.exists(fsPath)) {
throw new Error(`File not found: ${fsPath}`);
}
return (0, graceful_fs_1.createReadStream)(fsPath);
}
}
exports.NodeFSTreeContainer = NodeFSTreeContainer;
/**
* A {@link TreeContainer} that performs I/O without unzipping it to the disk first.
*/
class ZipTreeContainer extends TreeContainer {
zip;
constructor(zip) {
super();
this.zip = zip;
}
static async create(buffer) {
const zip = await jszip_1.default.loadAsync(buffer, { createFolders: true });
return new ZipTreeContainer(zip);
}
exists(fsPath) {
return !!this.match(fsPath);
}
isDirectory(fsPath) {
const resolvedPath = this.match(fsPath);
if (resolvedPath) {
return this.ensureDirectory(resolvedPath);
}
throw new core_1.SfError(messages.getMessage('error_path_not_found', [fsPath]), 'LibraryError');
}
readDirectory(fsPath) {
const resolvedPath = this.match(fsPath);
if (resolvedPath && this.ensureDirectory(resolvedPath)) {
// Remove trailing path sep if it exists. JSZip always adds them for directories but
// when comparing we call `dirname()` which does not include them.
const dirPath = resolvedPath.endsWith('/') ? resolvedPath.slice(0, -1) : resolvedPath;
return Object.keys(this.zip.files)
.filter((filePath) => (0, node_path_1.dirname)(filePath) === dirPath)
.map((filePath) => (0, node_path_1.basename)(filePath));
}
throw new core_1.SfError(messages.getMessage('error_expected_directory_path', [fsPath]), 'LibraryError');
}
async readFile(fsPath) {
const resolvedPath = this.match(fsPath);
if (resolvedPath) {
const jsZipObj = this.zip.file(resolvedPath);
if (jsZipObj?.dir === false) {
return jsZipObj.async('nodebuffer');
}
throw new core_1.SfError(`Expected a file at path ${fsPath} but found a directory.`);
}
throw new core_1.SfError(messages.getMessage('error_expected_file_path', [fsPath]), 'LibraryError');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
readFileSync(fsPath) {
throw new Error('Method not implemented');
}
stream(fsPath) {
const resolvedPath = this.match(fsPath);
if (resolvedPath) {
const jsZipObj = this.zip.file(resolvedPath);
if (jsZipObj && !jsZipObj.dir) {
return new node_stream_1.Readable().wrap(jsZipObj.nodeStream());
}
throw new core_1.SfError(messages.getMessage('error_no_directory_stream', [this.constructor.name]), 'LibraryError');
}
throw new core_1.SfError(messages.getMessage('error_expected_file_path', [fsPath]), 'LibraryError');
}
// Finds a matching entry in the zip by first comparing basenames, then dirnames.
// Note that zip files always use forward slash separators, so paths within the
// zip files are normalized for the OS file system before comparing.
match(fsPath) {
// "dot" has a special meaning as a directory name and always matches. Just return it.
if (fsPath === '.') {
return fsPath;
}
const fsPathBasename = (0, node_path_1.basename)(fsPath);
const fsPathDirname = (0, node_path_1.dirname)(fsPath);
return Object.keys(this.zip.files).find((filePath) => {
const normFilePath = (0, node_path_1.normalize)(filePath);
if ((0, node_path_1.basename)(normFilePath) === fsPathBasename) {
return (0, node_path_1.dirname)(normFilePath) === fsPathDirname;
}
});
}
ensureDirectory(dirPath) {
if (dirPath) {
// JSZip can have directory entries or only file entries (with virtual directory entries)
const zipObj = this.zip.file(dirPath);
return zipObj?.dir === true || !zipObj;
}
throw new core_1.SfError(messages.getMessage('error_path_not_found', [dirPath]), 'LibraryError');
}
}
exports.ZipTreeContainer = ZipTreeContainer;
/**
* A {@link TreeContainer} useful for mocking a file system.
*/
class VirtualTreeContainer extends TreeContainer {
tree = new Map();
fileContents = new Map();
constructor(virtualFs) {
super();
this.populate(virtualFs);
}
/**
* Designed for recreating virtual files from deleted files where the only information we have is the file's former location
* Any use of MetadataResolver was trying to access the non-existent files and throwing
*
* @param paths full paths to files
* @returns VirtualTreeContainer
*/
static fromFilePaths(paths) {
// a map to reduce array iterations
const virtualDirectoryByFullPath = new Map();
paths
// defending against undefined being passed in. The metadata API sometimes responds missing fileName
.filter(ts_types_1.isString)
.map((filename) => {
const splits = filename.split(node_path_1.sep);
for (let i = 0; i < splits.length - 1; i++) {
const fullPathSoFar = splits.slice(0, i + 1).join(node_path_1.sep);
const existing = virtualDirectoryByFullPath.get(fullPathSoFar);
virtualDirectoryByFullPath.set(fullPathSoFar, {
dirPath: fullPathSoFar,
// only add to children if we don't already have it
children: Array.from(new Set(existing?.children ?? []).add(splits[i + 1])),
});
}
});
return new VirtualTreeContainer(Array.from(virtualDirectoryByFullPath.values()));
}
isDirectory(fsPath) {
if (this.exists(fsPath)) {
return this.tree.has(fsPath);
}
throw new core_1.SfError(messages.getMessage('error_path_not_found', [fsPath]), 'LibraryError');
}
exists(fsPath) {
const files = this.tree.get((0, node_path_1.dirname)(fsPath));
const isFile = files?.has(fsPath);
return this.tree.has(fsPath) || Boolean(isFile);
}
readDirectory(fsPath) {
if (this.isDirectory(fsPath)) {
return Array.from(this.tree.get(fsPath) ?? []).map((p) => (0, node_path_1.basename)(p));
}
throw new core_1.SfError(messages.getMessage('error_expected_directory_path', [fsPath]), 'LibraryError');
}
readFile(fsPath) {
return Promise.resolve(this.readFileSync(fsPath));
}
readFileSync(fsPath) {
if (this.exists(fsPath)) {
let data = this.fileContents.get(fsPath);
if (!data) {
data = Buffer.from('');
this.fileContents.set(fsPath, data);
}
return data;
}
throw new core_1.SfError(messages.getMessage('error_path_not_found', [fsPath]), 'LibraryError');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
stream(fsPath) {
throw new Error('Method not implemented');
}
populate(virtualFs) {
for (const dir of virtualFs) {
const { dirPath, children } = dir;
this.tree.set(dirPath, new Set());
for (const child of children) {
const childPath = (0, ts_types_1.isString)(child) ? (0, node_path_1.join)(dirPath, child) : (0, node_path_1.join)(dirPath, child.name);
const dirPathFromTree = this.tree.get(dirPath);
if (!dirPathFromTree) {
throw new core_1.SfError(`The directory at path ${dirPath} does not exist in the virtual file system.`);
}
dirPathFromTree.add(childPath);
if (typeof child === 'object' && child.data) {
this.fileContents.set(childPath, child.data);
}
}
}
}
}
exports.VirtualTreeContainer = VirtualTreeContainer;
//# sourceMappingURL=treeContainers.js.map
;