@splunk/rum-cli
Version:
Tools for handling symbol and mapping files for symbolication
223 lines (222 loc) • 11.6 kB
JavaScript
;
/*
* 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.`,
});
}