UNPKG

@splunk/rum-cli

Version:

Tools for handling symbol and mapping files for symbolication

223 lines (222 loc) 11.6 kB
"use strict"; /* * Copyright Splunk Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runSourcemapInject = runSourcemapInject; exports.runSourcemapUpload = runSourcemapUpload; const filesystem_1 = require("../utils/filesystem"); const utils_1 = require("./utils"); const constants_1 = require("../utils/constants"); const userFriendlyErrors_1 = require("../utils/userFriendlyErrors"); const discoverJsMapFilePath_1 = require("./discoverJsMapFilePath"); const computeSourceMapId_1 = require("./computeSourceMapId"); const injectFile_1 = require("./injectFile"); const httpUtils_1 = require("../utils/httpUtils"); const axios_1 = __importDefault(require("axios")); const stringUtils_1 = require("../utils/stringUtils"); const wasInjectAlreadyRun_1 = require("./wasInjectAlreadyRun"); const apiInterceptor_1 = require("../utils/apiInterceptor"); /** * Inject sourceMapIds into all applicable JavaScript files inside the given directory. * * For each JS file in the directory: * 1. Determine where its source map file lives * 2. Compute the sourceMapId (by hashing its source map file) * 3. Inject the sourceMapId into the JS file */ function runSourcemapInject(options, ctx) { return __awaiter(this, void 0, void 0, function* () { const { directory, include, exclude } = options; const { logger } = ctx; /* * Read the provided directory to collect a list of all possible files the script will be working with. */ let jsFilePaths; let jsMapFilePaths; try { jsFilePaths = yield (0, filesystem_1.readdirRecursive)(directory, include, exclude); jsMapFilePaths = yield (0, filesystem_1.readdirRecursive)(directory, utils_1.DEFAULT_JS_MAP_GLOB_PATTERN); } catch (err) { throwDirectoryReadErrorDuringInject(err, directory); } // don't trust user-provided glob results. apply our own file-type filters before moving on jsFilePaths = jsFilePaths.filter(utils_1.isJsFilePath); jsMapFilePaths = jsMapFilePaths.filter(utils_1.isJsMapFilePath); logger.info(`Found ${jsFilePaths.length} JavaScript file(s) in ${directory}`); /* * Inject a code snippet into each JS file, whenever applicable. */ const injectedJsFilePaths = []; for (const jsFilePath of jsFilePaths) { const matchingSourceMapFilePath = yield (0, discoverJsMapFilePath_1.discoverJsMapFilePath)(jsFilePath, jsMapFilePaths, options, logger); if (!matchingSourceMapFilePath) { logger.info(`No source map was detected for ${jsFilePath}. Skipping injection.`); continue; } const sourceMapId = yield (0, computeSourceMapId_1.computeSourceMapId)(matchingSourceMapFilePath, options); yield (0, injectFile_1.injectFile)(jsFilePath, sourceMapId, options, logger); injectedJsFilePaths.push(jsFilePath); } // If we reach here, the only reason for temporary files to be leftover is if a previous invocation of // sourcemaps inject had terminated unexpectedly in the middle of writing to a temp file. // But we should make sure to clean up those older files, too, before exiting this successful run. yield (0, filesystem_1.cleanupTemporaryFiles)(directory); /* * Print summary of results */ logger.info(`Finished source map injection for ${injectedJsFilePaths.length} JavaScript file(s) in ${directory}`); if (jsFilePaths.length === 0) { logger.warn(`No JavaScript files were found. Verify that the provided directory contains JavaScript files and that any provided file patterns are correct:`); logger.warn({ directory, include, exclude }); } else if (injectedJsFilePaths.length === 0) { logger.warn(`No JavaScript files were injected. Verify that your build is configured to generate source maps for your JavaScript files.`); } }); } /** * Upload all source map files in the provided directory. * * For each source map file in the directory: * 1. Compute the sourceMapId (by hashing the file) * 2. Upload the file to the appropriate URL */ function runSourcemapUpload(options, ctx) { return __awaiter(this, void 0, void 0, function* () { const { logger, spinner } = ctx; const { directory, include, exclude, realm, appName, appVersion, token } = options; /* * Read the provided directory to collect a list of all possible files the script will be working with. */ let jsMapFilePaths; try { jsMapFilePaths = yield (0, filesystem_1.readdirRecursive)(directory, include, exclude); } catch (err) { throwDirectoryReadErrorDuringUpload(err, directory); } // don't trust user-provided glob results. apply our own file-type filter before moving on jsMapFilePaths = jsMapFilePaths.filter(utils_1.isJsMapFilePath); /* * Upload files to the server */ let success = 0; logger.info('Upload URL: %s', getSourceMapUploadUrl(realm, '{id}')); logger.info('Found %s source map(s) to upload', jsMapFilePaths.length); if (!options.dryRun) { spinner.start(''); } for (let i = 0; i < jsMapFilePaths.length; i++) { const filesRemaining = jsMapFilePaths.length - i; const path = jsMapFilePaths[i]; const sourceMapId = yield (0, computeSourceMapId_1.computeSourceMapId)(path, { directory }); const url = getSourceMapUploadUrl(realm, sourceMapId); const file = { filePath: path, fieldName: 'file' }; const parameters = Object.fromEntries([ ['appName', appName], ['appVersion', appVersion], // eslint-disable-next-line @typescript-eslint/no-unused-vars ].filter(([_, value]) => typeof value !== 'undefined')); logger.debug('Uploading %s', path); logger.debug('PUT', url); const dryRunUploadFile = () => __awaiter(this, void 0, void 0, function* () { logger.info('sourceMapId %s would be used to upload %s', sourceMapId, path); }); const uploadFileFn = options.dryRun ? dryRunUploadFile : httpUtils_1.uploadFile; // notify user if we cannot be certain the "sourcemaps inject" command was already run const alreadyInjected = yield (0, wasInjectAlreadyRun_1.wasInjectAlreadyRun)(path, logger); if (!alreadyInjected.result) { logger.warn(alreadyInjected.message); } // upload a single file try { const axiosInstance = axios_1.default.create(); (0, apiInterceptor_1.attachApiInterceptor)(axiosInstance, logger, url, { userFriendlyMessage: 'An error occurred during source map upload.' }); yield uploadFileFn({ url, file, token, onProgress: ({ loaded, total }) => { const { totalFormatted } = (0, stringUtils_1.formatUploadProgress)(loaded, total); spinner.updateText(`Uploading ${path} | ${totalFormatted} | ${filesRemaining} file(s) remaining`); }, parameters, }, axiosInstance); success++; } catch (e) { spinner.stop(); throw e; } } spinner.stop(); /* * Print summary of results */ if (!options.dryRun) { logger.info(`${success} source map(s) were uploaded successfully`); } if (jsMapFilePaths.length === 0) { logger.warn(`No source map files were found. Verify that the provided directory contains source map files and that any provided file patterns are correct:`); logger.warn({ directory, include, exclude }); } }); } function getSourceMapUploadUrl(realm, idPathParam) { const API_BASE_URL = `${constants_1.BASE_URL_PREFIX}.${realm}.signalfx.com`; const PATH_FOR_SOURCEMAPS = constants_1.SOURCEMAPS_CONSTANTS.PATH_FOR_UPLOAD; return `${API_BASE_URL}/${constants_1.API_VERSION_STRING}/${PATH_FOR_SOURCEMAPS}/id/${idPathParam}`; } function throwDirectoryReadErrorDuringInject(err, directory) { (0, userFriendlyErrors_1.throwAsUserFriendlyErrnoException)(err, { EACCES: `Failed to inject JavaScript files in "${directory} because of missing permissions.\nMake sure that the CLI tool will have "read" and "write" access to the directory and all files inside it, then rerun the inject command.`, ENOENT: `Unable to start the inject command because the directory "${directory}" does not exist.\nMake sure the correct path is being passed to --path, then rerun the inject command.`, ENOTDIR: `Unable to start the inject command because the path "${directory}" is not a directory.\nMake sure a valid directory path is being passed to --path, then rerun the inject command.`, }); } function throwDirectoryReadErrorDuringUpload(err, directory) { (0, userFriendlyErrors_1.throwAsUserFriendlyErrnoException)(err, { EACCES: `Failed to upload the source map files in "${directory} because of missing permissions.\nMake sure that the CLI tool will have "read" and "write" access to the directory and all files inside it, then rerun the upload command.`, ENOENT: `Unable to start the upload command because the directory "${directory}" does not exist.\nMake sure the correct path is being passed to --path, then rerun the upload command.`, ENOTDIR: `Unable to start the upload command because the path "${directory}" is not a directory.\nMake sure a valid directory path is being passed to --path, then rerun the upload command.`, }); }