UNPKG

@salesforce/source-deploy-retrieve

Version:

JavaScript library to run Salesforce metadata deploys and retrieves

222 lines 16.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.posixifyPaths = exports.stringToRegex = exports.envFilter = exports.matchesFile = exports.getReplacements = exports.getContentsOfReplacementFile = exports.getReplacementMarkingStream = exports.replacementIterations = exports.getReplacementStreamForReadable = 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 promises_1 = require("node:fs/promises"); const node_stream_1 = require("node:stream"); const node_path_1 = require("node:path"); const core_1 = require("@salesforce/core"); const minimatch_1 = require("minimatch"); const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const isbinaryfile_1 = require("isbinaryfile"); ; 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>"]])); const fileContentsCache = new Map(); // First do a quick check for common text extensions // If that fails, confirm that it is not a binary file const textExtensions = new Set(['.cls', '.xml', '.json', '.js', '.css', '.html', '.htm', '.txt', '.md']); const isTextFile = (path) => textExtensions.has((0, node_path_1.extname)(path)) || !(0, isbinaryfile_1.isBinaryFileSync)(path); /** If a component has replacements, you get it piped through the replacementStream * Otherwise, you'll get the original readable stream * Ignore binary files, they will get corrupted in the replacement process */ const getReplacementStreamForReadable = (component, path) => component.replacements?.[path] && isTextFile(path) ? component.tree.stream(path).pipe(new ReplacementStream(component.replacements?.[path])) : component.tree.stream(path); exports.getReplacementStreamForReadable = getReplacementStreamForReadable; /** * A stream for replacing the contents of a single SourceComponent. * */ class ReplacementStream extends node_stream_1.Transform { replacements; constructor(replacements) { super({ objectMode: true }); this.replacements = replacements; } async _transform(chunk, encoding, callback) { let error; // read and do the various replacements callback(error, Buffer.from(await (0, exports.replacementIterations)(chunk.toString(), this.replacements))); } } /** * perform an array of replacements on a string * emits warnings when an expected replacement target isn't found */ const replacementIterations = async (input, replacements) => { const lifecycleInstance = core_1.Lifecycle.getInstance(); let output = input; for (const replacement of replacements) { // TODO: node 16+ has String.replaceAll for non-regex scenarios const regex = typeof replacement.toReplace === 'string' ? new RegExp(replacement.toReplace, 'g') : replacement.toReplace; const replaced = output.replace(regex, replacement.replaceWith ?? ''); if (replaced !== output) { output = replaced; // eslint-disable-next-line no-await-in-loop await lifecycleInstance.emit('replacement', { filename: replacement.matchedFilename, replaced: replacement.toReplace.toString(), }); } else if (replacement.singleFile) { // replacements need to be done sequentially // eslint-disable-next-line no-await-in-loop await lifecycleInstance.emitWarning(`Your sfdx-project.json specifies that ${replacement.toReplace.toString()} should be replaced in ${replacement.matchedFilename}, but it was not found.`); } } return output; }; exports.replacementIterations = replacementIterations; /** * Reads the project, gets replacements, removes any that aren't applicable due to environment conditionals, and returns an instance of the ReplacementMarkingStream */ const getReplacementMarkingStream = async (projectDir) => { // remove any that don't agree with current env const filteredReplacements = (await readReplacementsFromProject(projectDir)).filter(exports.envFilter); return filteredReplacements.length ? new ReplacementMarkingStream(filteredReplacements) : undefined; }; exports.getReplacementMarkingStream = getReplacementMarkingStream; /** * Stream for marking replacements on a component. * Returns a mutated component with a `replacements` property if any replacements are found. * Throws if any replacements reference a file or env that does not exist */ class ReplacementMarkingStream extends node_stream_1.Transform { replacementConfigs; constructor(replacementConfigs) { super({ objectMode: true }); this.replacementConfigs = replacementConfigs; } async _transform(chunk, encoding, callback) { let err; // if deleting, or no configs, just pass through if (!chunk.isMarkedForDelete() && this.replacementConfigs?.length) { try { chunk.replacements = await (0, exports.getReplacements)(chunk, this.replacementConfigs); if (chunk.replacements && chunk.parent?.type.strategies?.transformer === 'nonDecomposed') { // Set replacements on the parent of a nonDecomposed CustomLabel as well so that recomposing // doesn't use the non-replaced content from parent cache. // See RecompositionFinalizer.recompose() in convertContext.ts chunk.parent.replacements = chunk.replacements; } } catch (e) { if (!(e instanceof Error)) { throw e; } err = e; } } callback(err, chunk); } } const getContentsOfReplacementFile = async (path) => { if (!fileContentsCache.has(path)) { try { fileContentsCache.set(path, (await (0, promises_1.readFile)(path, 'utf8')).trim()); } catch (e) { throw messages.createError('replacementsFileNotRead', [path]); } } const output = fileContentsCache.get(path); if (!output) { throw messages.createError('replacementsFileNotRead', [path]); } return output; }; exports.getContentsOfReplacementFile = getContentsOfReplacementFile; /** * Build the replacements property for a sourceComponent */ const getReplacements = async (cmp, replacementConfigs = []) => { // all possible filenames for this component const filenames = [cmp.xml, ...cmp.walkContent()].filter(ts_types_1.isString); const replacementsForComponent = (await Promise.all( // build a nested array that can be run through Object.fromEntries // one MarkedReplacement[] for each file in the component filenames.map(async (f) => [ f, await Promise.all(replacementConfigs // filter out any that don't match the current file .filter((0, exports.matchesFile)(f)) .map(async (r) => ({ matchedFilename: f, // used during replacement stream to limit warnings to explicit filenames, not globs singleFile: Boolean(r.filename), // Config is json which might use the regex. If so, turn it into an actual regex toReplace: typeof r.stringToReplace === 'string' ? (0, exports.stringToRegex)(r.stringToReplace) : new RegExp(r.regexToReplace, 'g'), // get the literal replacement (either from env or file contents) replaceWith: typeof r.replaceWithEnv === 'string' ? getEnvValue(r.replaceWithEnv, r.allowUnsetEnvVariable) : await (0, exports.getContentsOfReplacementFile)(r.replaceWithFile), }))), ]))) // filter out any that don't have any replacements .filter(([, replacements]) => replacements.length > 0); // turn into a Dictionary-style object so it's easier to lookup by filename return replacementsForComponent.length ? Object.fromEntries(replacementsForComponent) : undefined; }; exports.getReplacements = getReplacements; const matchesFile = (filename) => (r) => // filenames will be absolute. We don't have convenient access to the pkgDirs, // so we need to be more open than an exact match (typeof r.filename === 'string' && (0, exports.posixifyPaths)(filename).endsWith(r.filename)) || (typeof r.glob === 'string' && (0, minimatch_1.minimatch)(filename, `**/${r.glob}`)); exports.matchesFile = matchesFile; /** * Regardless of any components, return the ReplacementConfig that are valid with the current env. * These can be checked globally and don't need to be checked per component. */ const envFilter = (replacement) => !replacement.replaceWhenEnv || replacement.replaceWhenEnv.every((envConditional) => process.env[envConditional.env] === envConditional.value.toString()); exports.envFilter = envFilter; /** A "getter" for envs to throw an error when an expected env is not present */ const getEnvValue = (env, allowUnset = false) => allowUnset ? new kit_1.Env().getString(env, '') : (0, ts_types_1.ensureString)(new kit_1.Env().getString(env), `"${env}" is in sfdx-project.json as a value for "replaceWithEnv" property, but it's not set in your environment.`); /** * Read the `replacement` property from sfdx-project.json */ const readReplacementsFromProject = async (projectDir) => { try { const proj = await core_1.SfProject.resolve(projectDir); const projJson = (await proj.resolveProjectConfig()); const definiteProjectDir = proj.getPath(); return (projJson.replacements ?? []).map(makeAbsolute(definiteProjectDir)); } catch (e) { if (e instanceof core_1.SfError && e.name === 'InvalidProjectWorkspaceError') { return []; } throw e; } }; /** escape any special characters used in the string so it can be used as a regex */ const stringToRegex = (input) => // being overly conservative // eslint-disable-next-line no-useless-escape new RegExp(input.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'); exports.stringToRegex = stringToRegex; const posixifyPaths = (f) => f.split(node_path_1.sep).join(node_path_1.posix.sep); exports.posixifyPaths = posixifyPaths; /** if replaceWithFile is present, resolve it to an absolute path relative to the projectdir */ const makeAbsolute = (projectDir) => (replacementConfig) => replacementConfig.replaceWithFile ? { ...replacementConfig, // it could already be absolute? replaceWithFile: (0, node_path_1.isAbsolute)(replacementConfig.replaceWithFile) ? replacementConfig.replaceWithFile : (0, node_path_1.join)(projectDir, replacementConfig.replaceWithFile), } : replacementConfig; //# sourceMappingURL=replacements.js.map