@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
205 lines • 15.9 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StaticResourceMetadataTransformer = 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
*/
const node_path_1 = require("node:path");
const node_stream_1 = require("node:stream");
const jszip_1 = __importDefault(require("jszip"));
const mime_1 = require("mime");
const graceful_fs_1 = require("graceful-fs");
const core_1 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const path_1 = require("../../utils/path");
const fileSystemHandler_1 = require("../../utils/fileSystemHandler");
const streams_1 = require("../streams");
const replacements_1 = require("../replacements");
const baseMetadataTransformer_1 = require("./baseMetadataTransformer");
;
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>"]]));
let logger;
const getLogger = () => {
if (!logger) {
logger = core_1.Logger.childFromRoot('StaticResourceMetadataTransformer');
}
return logger;
};
class StaticResourceMetadataTransformer extends baseMetadataTransformer_1.BaseMetadataTransformer {
static ARCHIVE_MIME_TYPES = new Set([
'application/zip',
'application/x-zip-compressed',
'application/jar',
]);
// allowed to preserve API
// eslint-disable-next-line class-methods-use-this
async toMetadataFormat(component) {
const { content, type, xml } = component;
if (!content) {
throw messages.createError('noContentFound', [component.fullName, component.type.name]);
}
if (!xml) {
throw messages.createError('error_parsing_xml', [component.fullName, component.type.name]);
}
// Zip the static resource from disk to a stream, compressing at level 9.
const zipIt = () => {
getLogger().debug(`zipping static resource: ${content}`);
const zip = (0, jszip_1.default)();
// JSZip does not have an API for adding a directory of files recursively so we always
// have to walk the component content. Replacements only happen if set on the component.
for (const path of component.walkContent()) {
const replacementStream = (0, replacements_1.getReplacementStreamForReadable)(component, path);
const relPath = (0, node_path_1.relative)(content, path);
const relPosixPath = relPath.replace(/\\/g, '/');
zip.file(relPosixPath, replacementStream);
}
// If the generated zip is empty it means either no content files exist or they are
// all force-ignored. Throw an error in this case.
if ((0, kit_1.isEmpty)(zip.files)) {
throw messages.createError('noContentFound', [component.fullName, component.type.name]);
}
return new node_stream_1.Readable().wrap(zip
.generateNodeStream({
compression: 'DEFLATE',
compressionOptions: { level: 9 },
streamFiles: true,
})
.on('end', () => {
getLogger().debug(`zip complete for: ${content}`);
}));
};
return [
{
source: (await componentIsExpandedArchive(component))
? zipIt()
: (0, replacements_1.getReplacementStreamForReadable)(component, content),
output: (0, node_path_1.join)(type.directoryName, `${(0, path_1.baseName)(content)}.${type.suffix ?? ''}`),
},
{
source: (0, replacements_1.getReplacementStreamForReadable)(component, xml),
output: (0, node_path_1.join)(type.directoryName, (0, node_path_1.basename)(xml)),
},
];
}
async toSourceFormat({ component, mergeWith }) {
const { xml, content } = component;
if (!content) {
return [];
}
const componentContentType = await getContentType(component);
const mergeContentPath = mergeWith?.content;
const baseContentPath = getBaseContentPath(component, mergeWith);
// only unzip an archive component if there isn't a merge component, or the merge component is itself expanded
const shouldUnzipArchive = StaticResourceMetadataTransformer.ARCHIVE_MIME_TYPES.has(componentContentType) &&
(!mergeWith || (mergeContentPath && mergeWith.tree.isDirectory(mergeContentPath)));
if (shouldUnzipArchive) {
// for the bulk of static resource writing we'll start writing ASAP
// we'll still defer writing the resource-meta.xml file by pushing it onto the writeInfos
const srZip = await getStaticResourceZip(component, content);
const pipelinePromises = [];
for (const filePath of Object.keys(srZip.files)) {
const zipObj = srZip.file(filePath);
if (zipObj && !zipObj.dir) {
const path = (0, node_path_1.join)(baseContentPath, filePath);
const fullDest = (0, node_path_1.isAbsolute)(path)
? path
: (0, node_path_1.join)(this.defaultDirectory ?? component.getPackageRelativePath('', 'source'), path);
pipelinePromises.push(this.pipeline(new node_stream_1.Readable().wrap(zipObj.nodeStream()), fullDest));
}
}
await Promise.all(pipelinePromises);
}
if (!xml) {
throw messages.createError('error_parsing_xml', [component.fullName, component.type.name]);
}
return [
{
source: component.tree.stream(xml),
output: mergeWith?.xml ?? component.getPackageRelativePath((0, node_path_1.basename)(xml), 'source'),
},
].concat(shouldUnzipArchive
? []
: [
{
source: component.tree.stream(content),
output: `${baseContentPath}.${getExtensionFromType(componentContentType)}`,
},
]);
}
/**
* Only separated into its own method for unit testing purposes
* I was unable to find a way to stub/spy a pipline() call
*
* @param stream the data to be written
* @param destination the destination path to be written
* @private
*/
// eslint-disable-next-line class-methods-use-this
async pipeline(stream, destination) {
(0, fileSystemHandler_1.ensureFileExists)(destination);
await (0, streams_1.pipeline)(stream, (0, graceful_fs_1.createWriteStream)(destination));
}
}
exports.StaticResourceMetadataTransformer = StaticResourceMetadataTransformer;
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
const FALLBACK_TYPE_MAP = new Map([
['text/javascript', 'js'],
['application/x-javascript', 'js'],
['application/x-zip-compressed', 'zip'],
['text/x-haml', 'haml'],
['image/x-png', 'png'],
['text/xml', 'xml'],
]);
const getContentType = async (component) => {
const resource = (await component.parseXml()).StaticResource;
if (!resource || !Object.keys(resource).includes('contentType')) {
throw new core_1.SfError(messages.getMessage('error_static_resource_missing_resource_file', [
(0, node_path_1.join)('staticresources', component.name ?? component.xml ?? component.type.name),
]), 'LibraryError');
}
const output = resource.contentType ?? DEFAULT_CONTENT_TYPE;
if (typeof output !== 'string') {
throw new core_1.SfError(`Expected a string for contentType in ${component.name} (${component.xml ?? '<no xml>'}) but got ${JSON.stringify(output)}`);
}
return output;
};
const getBaseContentPath = (component, mergeWith) => {
if (mergeWith?.content) {
return (0, node_path_1.join)((0, node_path_1.dirname)(mergeWith.content), (0, path_1.baseName)(mergeWith?.content));
}
if (typeof component.content === 'string') {
const baseContentPath = component.getPackageRelativePath(component.content, 'source');
return (0, node_path_1.join)((0, node_path_1.dirname)(baseContentPath), (0, path_1.baseName)(baseContentPath));
}
throw new core_1.SfError(`Expected a content path for ${component.name} (${component.xml ?? '<no xml>'})`);
};
const getExtensionFromType = (contentType) =>
// return registered ext, fallback, or the default (application/octet-stream -> bin)
(0, mime_1.getExtension)(contentType) ?? FALLBACK_TYPE_MAP.get(contentType) ?? (0, mime_1.getExtension)(DEFAULT_CONTENT_TYPE) ?? 'bin';
const componentIsExpandedArchive = async (component) => {
const { content, tree } = component;
if (content && tree.isDirectory(content)) {
const contentType = await getContentType(component);
if (StaticResourceMetadataTransformer.ARCHIVE_MIME_TYPES.has(contentType)) {
return true;
}
throw new core_1.SfError(messages.getMessage('error_static_resource_expected_archive_type', [contentType, component.name]), 'LibraryError');
}
return false;
};
async function getStaticResourceZip(component, content) {
try {
const staticResourceZip = await component.tree.readFile(content);
return await jszip_1.default.loadAsync(staticResourceZip, { createFolders: true });
}
catch (e) {
throw new core_1.SfError(`Unable to open zip file ${content} for ${component.name} (${component.xml ?? '<no xml>'})`, 'BadZipFile', ['Check that your file really is a valid zip archive']);
}
}
//# sourceMappingURL=staticResourceMetadataTransformer.js.map
;