|
| 1 | +import * as fs from "fs"; |
| 2 | +import * as path from "path"; |
| 3 | +import { statSync } from "fs-extra"; |
| 4 | +import { readdirRecursive } from "../fsAsync"; |
| 5 | +import { Command } from "../command"; |
| 6 | +import { FirebaseError } from "../error"; |
| 7 | +import { logLabeledBullet, logLabeledWarning } from "../utils"; |
| 8 | +import { needProjectId } from "../projectUtils"; |
| 9 | +import * as gcs from "../gcp/storage"; |
| 10 | +import { getProjectNumber } from "../getProjectNumber"; |
| 11 | +import { Options } from "../options"; |
| 12 | +import { archiveFile } from "../archiveFile"; |
| 13 | + |
| 14 | +interface CommandOptions extends Options { |
| 15 | + app?: string; |
| 16 | + bucketLocation?: string; |
| 17 | + appVersion?: string; |
| 18 | +} |
| 19 | + |
| 20 | +export const command = new Command("crashlytics:sourcemap:upload <mappingFiles>") |
| 21 | + .description("upload javascript source maps to de-minify stack traces") |
| 22 | + .option("--app <appID>", "the app id of your Firebase app") |
| 23 | + .option( |
| 24 | + "--bucket-location <bucketLocation>", |
| 25 | + 'the location of the Google Cloud Storage bucket (default: "US-CENTRAL1"', |
| 26 | + ) |
| 27 | + .option( |
| 28 | + "--app-version <appVersion>", |
| 29 | + "the version of your Firebase app (defaults to Git commit hash, if available)", |
| 30 | + ) |
| 31 | + .action(async (mappingFiles: string, options: CommandOptions) => { |
| 32 | + checkGoogleAppID(options); |
| 33 | + |
| 34 | + // App version |
| 35 | + const appVersion = getAppVersion(); |
| 36 | + |
| 37 | + // Get project identifiers |
| 38 | + const projectId = needProjectId(options); |
| 39 | + const projectNumber = await getProjectNumber(options); |
| 40 | + |
| 41 | + // Upsert default GCS bucket |
| 42 | + const bucketName = await upsertBucket(projectId, projectNumber, options); |
| 43 | + |
| 44 | + // Find and upload mapping files |
| 45 | + const rootDir = options.projectRoot ?? process.cwd(); |
| 46 | + const filePath = path.relative(rootDir, mappingFiles); |
| 47 | + let fstat: fs.Stats; |
| 48 | + try { |
| 49 | + fstat = statSync(filePath); |
| 50 | + } catch (e) { |
| 51 | + throw new FirebaseError( |
| 52 | + "provide a valid file path or directory to mapping file(s), e.g. app/build/outputs/app.js.map or app/build/outputs", |
| 53 | + ); |
| 54 | + } |
| 55 | + if (fstat.isFile()) { |
| 56 | + await uploadMap(mappingFiles, bucketName, appVersion, options); |
| 57 | + } else if (fstat.isDirectory()) { |
| 58 | + logLabeledBullet("crashlytics", "Looking for mapping files in your directory..."); |
| 59 | + const files = ( |
| 60 | + await readdirRecursive({ path: filePath, ignore: ["node_modules", ".git"], maxDepth: 20 }) |
| 61 | + ).filter((f) => f.name.endsWith(".js.map")); |
| 62 | + await Promise.all(files.map((f) => uploadMap(f.name, bucketName, appVersion, options))); |
| 63 | + } else { |
| 64 | + throw new FirebaseError( |
| 65 | + "provide a valid file path or directory to mapping file(s), e.g. app/build/outputs/app.js.map or app/build/outputs", |
| 66 | + ); |
| 67 | + } |
| 68 | + |
| 69 | + // TODO: notify Firebase Telemetry service of the new mapping file |
| 70 | + }); |
| 71 | + |
| 72 | +function checkGoogleAppID(options: CommandOptions): void { |
| 73 | + if (!options.app) { |
| 74 | + throw new FirebaseError( |
| 75 | + "set --app <appId> to a valid Firebase application id, e.g. 1:00000000:android:0000000", |
| 76 | + ); |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +function getAppVersion(): string { |
| 81 | + // TODO: implement app version lookup |
| 82 | + return "default"; |
| 83 | +} |
| 84 | + |
| 85 | +async function upsertBucket( |
| 86 | + projectId: string, |
| 87 | + projectNumber: string, |
| 88 | + options: CommandOptions, |
| 89 | +): Promise<string> { |
| 90 | + let loc: string = "US-CENTRAL1"; |
| 91 | + if (options.bucketLocation) { |
| 92 | + loc = (options.bucketLocation as string).toUpperCase(); |
| 93 | + } else { |
| 94 | + logLabeledBullet( |
| 95 | + "crashlytics", |
| 96 | + "No Google Cloud Storage bucket location specified. Defaulting to US-CENTRAL1.", |
| 97 | + ); |
| 98 | + } |
| 99 | + |
| 100 | + const baseName = `firebasecrashlytics-sourcemaps-${projectNumber}-${loc.toLowerCase()}`; |
| 101 | + return await gcs.upsertBucket({ |
| 102 | + product: "crashlytics", |
| 103 | + createMessage: `Creating Cloud Storage bucket in ${loc} to store Crashlytics source maps at ${baseName}...`, |
| 104 | + projectId, |
| 105 | + req: { |
| 106 | + baseName, |
| 107 | + purposeLabel: `crashlytics-sourcemaps-${loc.toLowerCase()}`, |
| 108 | + location: loc, |
| 109 | + lifecycle: { |
| 110 | + rule: [ |
| 111 | + { |
| 112 | + action: { |
| 113 | + type: "Delete", |
| 114 | + }, |
| 115 | + condition: { |
| 116 | + age: 30, |
| 117 | + }, |
| 118 | + }, |
| 119 | + ], |
| 120 | + }, |
| 121 | + }, |
| 122 | + }); |
| 123 | +} |
| 124 | + |
| 125 | +async function uploadMap( |
| 126 | + mappingFile: string, |
| 127 | + bucketName: string, |
| 128 | + appVersion: string, |
| 129 | + options: CommandOptions, |
| 130 | +) { |
| 131 | + logLabeledBullet("crashlytics", `Found mapping file ${mappingFile}...`); |
| 132 | + try { |
| 133 | + const filePath = path.relative(options.projectRoot ?? process.cwd(), mappingFile); |
| 134 | + const tmpArchive = await archiveFile(filePath, { archivedFileName: "mapping.js.map" }); |
| 135 | + const gcsFile = `${options.app}-${appVersion}-${normalizeFileName(mappingFile)}.zip`; |
| 136 | + |
| 137 | + const { bucket, object } = await gcs.uploadObject( |
| 138 | + { |
| 139 | + file: gcsFile, |
| 140 | + stream: fs.createReadStream(tmpArchive), |
| 141 | + }, |
| 142 | + bucketName, |
| 143 | + ); |
| 144 | + logLabeledBullet("crashlytics", `Uploaded to gs://${bucket}/${object}`); |
| 145 | + } catch (e) { |
| 146 | + logLabeledWarning("crashlytics", `Failed to upload mapping file ${mappingFile}:\n${e}`); |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +function normalizeFileName(fileName: string): string { |
| 151 | + return fileName.replaceAll(/\//g, "-"); |
| 152 | +} |
0 commit comments