UNPKG

firebase-tools

Version:
271 lines (270 loc) 9.99 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CONCURRENCY = void 0; exports.checkGoogleAppID = checkGoogleAppID; exports.getAppVersion = getAppVersion; exports.getGitCommit = getGitCommit; exports.getPackageVersion = getPackageVersion; exports.upsertBucket = upsertBucket; exports.findSourceMapMappings = findSourceMapMappings; exports.getLinkedSourceMapPath = getLinkedSourceMapPath; exports.uploadSourceMaps = uploadSourceMaps; exports.uploadMap = uploadMap; exports.normalizeFileName = normalizeFileName; exports.registerSourceMap = registerSourceMap; const fs = require("fs"); const path = require("path"); const node_child_process_1 = require("node:child_process"); const pLimit = require("p-limit"); const apiv2_1 = require("../apiv2"); const error_1 = require("../error"); const logger_1 = require("../logger"); const utils_1 = require("../utils"); const gcs = require("../gcp/storage"); const archiveFile_1 = require("../archiveFile"); exports.CONCURRENCY = 25; function checkGoogleAppID(options) { if (!options.app) { throw new error_1.FirebaseError("set --app <appId> to a valid Firebase application id, e.g. 1:00000000:android:0000000"); } } function getAppVersion(options) { if (options.appVersion) { return options.appVersion; } const gitCommit = getGitCommit(); if (gitCommit) { (0, utils_1.logLabeledBullet)("crashlytics", `Using git commit as app version: ${gitCommit}`); return gitCommit; } const packageVersion = getPackageVersion(); if (packageVersion) { (0, utils_1.logLabeledBullet)("crashlytics", `Using package version as app version: ${packageVersion}`); return packageVersion; } return "unset"; } function getGitCommit() { if (!(0, utils_1.commandExistsSync)("git")) { return undefined; } try { return (0, node_child_process_1.execSync)("git rev-parse HEAD").toString().trim(); } catch (error) { return undefined; } } function getPackageVersion() { if (!(0, utils_1.commandExistsSync)("npm")) { return undefined; } try { return (0, node_child_process_1.execSync)("npm pkg get version").toString().trim().replaceAll('"', ""); } catch (error) { return undefined; } } async function upsertBucket(projectId, projectNumber, options) { let loc = "US-CENTRAL1"; if (options.bucketLocation) { loc = options.bucketLocation.toUpperCase(); } else { (0, utils_1.logLabeledBullet)("crashlytics", "No Google Cloud Storage bucket location specified. Defaulting to US-CENTRAL1."); } const baseName = `firebasecrashlytics-sourcemaps-${projectNumber}-${loc.toLowerCase()}`; return await gcs.upsertBucket({ product: "crashlytics", createMessage: `Creating Cloud Storage bucket in ${loc} to store Crashlytics source maps at ${baseName}...`, projectId, req: { baseName, purposeLabel: `crashlytics-sourcemaps-${loc.toLowerCase()}`, location: loc, lifecycle: { rule: [ { action: { type: "Delete", }, condition: { age: 30, }, }, ], }, }, }); } async function findSourceMapMappings(files, rootDir) { const jsFiles = files.filter((f) => f.name.endsWith(".js")); const mapFiles = files.filter((f) => f.name.endsWith(".js.map")); const mappings = []; const mapFilePathsSet = new Set(mapFiles.map((f) => f.name)); const mapFilesLinkedInJsComment = new Set(); const limit = pLimit(exports.CONCURRENCY); const results = await Promise.all(jsFiles.map((jsFile) => limit(async () => { const mapFilePath = await getLinkedSourceMapPath(jsFile.name); return { jsFile, mapFilePath }; }))); for (const { jsFile, mapFilePath } of results) { if (mapFilePath && mapFilePathsSet.has(mapFilePath)) { mappings.push({ mapFilePath, obfuscatedFilePath: path.relative(rootDir, path.resolve(jsFile.name)), }); mapFilesLinkedInJsComment.add(mapFilePath); } } for (const mapFile of mapFiles) { if (!mapFilesLinkedInJsComment.has(mapFile.name)) { mappings.push({ mapFilePath: mapFile.name, obfuscatedFilePath: path.relative(rootDir, path.resolve(mapFile.name)), }); } } return mappings; } async function getLinkedSourceMapPath(jsFilePath) { let fileHandle; try { const stat = await fs.promises.stat(jsFilePath); const size = stat.size; const bufferSize = Math.min(size, 4096); if (bufferSize === 0) { return undefined; } fileHandle = await fs.promises.open(jsFilePath, "r"); const buffer = Buffer.alloc(bufferSize); const { bytesRead } = await fileHandle.read(buffer, 0, bufferSize, size - bufferSize); const tail = buffer.toString("utf-8", 0, bytesRead); const regex = /^\/\/\s*[#@]\s*sourceMappingURL=(?<sourceMappingURL>.+)\s*$/m; const match = regex.exec(tail); const sourceMappingURL = match?.groups?.sourceMappingURL?.trim(); if (sourceMappingURL) { return path.join(path.dirname(jsFilePath), sourceMappingURL); } } catch (e) { logger_1.logger.debug(`Error reading sourceMappingURL from ${jsFilePath}: ${e instanceof Error ? e.message : String(e)}`); } finally { if (fileHandle) { try { await fileHandle.close(); } catch (e) { } } } return undefined; } async function uploadSourceMaps(mappings, request) { const { projectId, bucketName, appVersion, options } = request; const limit = pLimit(exports.CONCURRENCY); const results = await Promise.all(mappings.map((mapping) => limit(async () => { const uploadRequest = { projectId, mappingFile: mapping.mapFilePath, obfuscatedFilePath: mapping.obfuscatedFilePath, bucketName, appVersion, options, }; let success = await uploadMap(uploadRequest, 1); if (!success) { await new Promise((res) => setTimeout(res, options.retryDelay || 5000)); success = await uploadMap(uploadRequest); } return success; }))); let successCount = 0; const failedFiles = []; for (const [i, success] of results.entries()) { if (success) { successCount++; } else { failedFiles.push(mappings[i].mapFilePath); } } return { successCount, failedFiles, }; } async function uploadMap(request, attemptsRemaining = 0) { const { projectId, mappingFile, obfuscatedFilePath, bucketName, appVersion, options } = request; const filePath = path.relative(options.projectRoot ?? process.cwd(), mappingFile); const obfuscatedPath = obfuscatedFilePath .split(path.sep) .map((p) => (p === ".next" ? "_next" : p)) .filter((p) => p !== "dev") .join("/"); const tmpArchive = await (0, archiveFile_1.archiveFile)(filePath, { archivedFileName: "mapping.js.map" }); const appId = options.app || ""; const gcsFile = `${appId}-${appVersion}-${normalizeFileName(obfuscatedPath)}.zip`; const uid = (0, utils_1.murmurHashV3)(`${appId}-${appVersion}-${obfuscatedPath}`); const name = `projects/${projectId}/locations/global/mappingFiles/${uid}`; const stream = fs.createReadStream(tmpArchive); stream.on("error", (err) => { logger_1.logger.debug(`Stream error on tmpArchive: ${err instanceof Error ? err.message : String(err)}`); }); try { const { bucket, object } = await gcs.uploadObject({ file: gcsFile, stream, }, bucketName); const fileUri = `gs://${bucket}/${object}`; logger_1.logger.debug(`Uploaded mapping file ${filePath} to ${fileUri}`); await registerSourceMap({ name, appId, version: appVersion, obfuscatedFilePath: `/${obfuscatedPath}`, fileUri, }); logger_1.logger.debug(`Registered mapping file ${filePath}`); return true; } catch (e) { if (attemptsRemaining === 0) { (0, utils_1.logLabeledWarning)("crashlytics", `Failed to upload mapping file ${filePath}:\n${e instanceof Error ? e.message : String(e)}`); } return false; } finally { stream.destroy(); try { fs.rmSync(tmpArchive, { force: true }); } catch (err) { logger_1.logger.debug(`Failed to delete temporary archive ${tmpArchive}: ${err instanceof Error ? err.message : String(err)}`); } } } function normalizeFileName(fileName) { return fileName.replaceAll(/\//g, "-"); } async function registerSourceMap(sourceMap) { const client = new apiv2_1.Client({ urlPrefix: "https://firebasetelemetryadmin.googleapis.com", auth: true, apiVersion: "v1", }); try { await client.patch(sourceMap.name, sourceMap, { queryParams: { allowMissing: "true" } }); logger_1.logger.debug(`Registered source map ${sourceMap.obfuscatedFilePath} with Firebase Telemetry service`); } catch (e) { if (e instanceof error_1.FirebaseError) { if (e.status === 409) { return; } } throw new error_1.FirebaseError(`Failed to register source map ${sourceMap.obfuscatedFilePath} with Firebase Telemetry service:\n${e instanceof Error ? e.message : String(e)}`); } }