-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathsource-map-uploader-plugin.js
More file actions
179 lines (155 loc) · 6.24 KB
/
source-map-uploader-plugin.js
File metadata and controls
179 lines (155 loc) · 6.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
'use strict'
const parallel = require('run-parallel-limit')
const extname = require('path').extname
const join = require('path').join
const webpackVersion = require('webpack').version
const BugsnagCLI = require('@bugsnag/cli')
const LOG_PREFIX = '[BugsnagSourceMapUploaderPlugin]'
const PUBLIC_PATH_WARN =
'`publicPath` is not set.\n\n' +
' Source maps must be uploaded with the pattern that matches the file path in stacktraces.\n\n' +
' To make this message go away, set "publicPath" in Webpack config ("output" section)\n' +
' or set "publicPath" in BugsnagSourceMapUploaderPlugin constructor.\n\n' +
' In some cases, such as in a Node environment, it is safe to ignore this message.\n'
class BugsnagSourceMapUploaderPlugin {
constructor (options) {
this.apiKey = options.apiKey
this.publicPath = options.publicPath
this.appVersion = options.appVersion
this.codeBundleId = options.codeBundleId
this.overwrite = options.overwrite
this.endpoint = options.endpoint
this.bundle = options.bundle
this.bundleUrl = options.bundleUrl
this.ignoredBundleExtensions = options.ignoredBundleExtensions || ['.css']
this.dryRun = options.dryRun
this.logLevel = options.logLevel
this.verbose = options.verbose
this.retries = options.retries
this.timeout = options.timeout
this.validate()
}
validate () {
if (typeof this.apiKey !== 'string' || this.apiKey.length < 1) {
throw new Error(`${LOG_PREFIX} "apiKey" is required`)
}
}
apply (compiler) {
// considering this is used to check for a version >= 5, it's fine to default to 0.0.0 in case it's not set
const webpackMajorVersion = parseInt((webpackVersion || '0.0.0').split('.')[0], 10)
const plugin = (compilation, callback) => {
const compiler = compilation.compiler
const stats = compilation.getStats().toJson()
const publicPath = this.publicPath || stats.publicPath || ''
const outputPath = compilation.getPath(compiler.outputPath)
const logger = compiler.getInfrastructureLogger ? compiler.getInfrastructureLogger('BugsnagSourceMapUploaderPlugin') : console
const logPrefix = compiler.getInfrastructureLogger ? '' : `${LOG_PREFIX} `
const chunkToSourceMapDescriptors = chunk => {
// find .map files in this chunk
const maps = chunk[webpackMajorVersion >= 5 ? 'auxiliaryFiles' : 'files'].filter(file => /.+\.map(\?.*)?$/.test(file))
if (!publicPath) {
logger.warn(`${logPrefix}${PUBLIC_PATH_WARN}`)
}
return maps.map(map => {
// for each *.map file, find a corresponding source file in the chunk
const source = chunk.files.find(file => map.replace('.map', '').endsWith(file))
if (!source) {
logger.warn(`${logPrefix}no corresponding source found for "${map}" in chunk "${chunk.id}"`)
return null
}
if (!compilation.assets[source]) {
logger.debug(`${logPrefix}source asset not found in compilation output "${source}"`)
return null
}
if (!compilation.assets[map]) {
logger.debug(`${logPrefix}source map not found in compilation output "${map}"`)
return null
}
const outputChunkLocation = stripQuery(join(outputPath, source))
const outputSourceMapLocation = stripQuery(join(outputPath, map))
// only include this file if its extension is not in the ignore list
if (this.ignoredBundleExtensions.indexOf(extname(outputChunkLocation)) !== -1) {
return null
}
let url = '' +
// ensure publicPath has a trailing slash
publicPath.replace(/[^/]$/, '$&/') +
// remove leading / or ./ from source
source.replace(/^\.?\//, '')
// replace parent directory references with empty string
url = url.replace(/\.\.\//g, '')
return {
source: outputChunkLocation,
map: outputSourceMapLocation,
url: url
}
}).filter(Boolean)
}
const sourceMaps = stats.chunks.map(chunkToSourceMapDescriptors).reduce((accum, ds) => accum.concat(ds), [])
parallel(sourceMaps.map(sm => callback => {
const cmdOpts = this.bugsnagCliUploadOpts(sm)
logger.info(`${logPrefix}uploading sourcemap for "${sm.url}" using the bugsnag-cli`)
for (const [key, value] of Object.entries(cmdOpts)) {
logger.debug(`${logPrefix}${key}: ${value}`)
}
BugsnagCLI.Upload.Js(cmdOpts, outputPath)
.then((output) => {
// Split output by lines, prefix each line, and log them
output.split('\n').forEach((line) => {
logger.info(`${logPrefix}${line}`)
})
callback()
}, callback)
.catch((error) => {
// Split error by lines, prefix each line, and log them
error.toString().split('\n').forEach((line) => {
logger.error(`${logPrefix}${line}`)
})
callback()
})
}), 10, callback)
}
if (compiler.hooks) {
// webpack v4
compiler.hooks.afterEmit.tapAsync('BugsnagSourceMapUploaderPlugin', plugin)
} else {
// webpack v3
compiler.plugin('after-emit', plugin)
}
}
bugsnagCliUploadOpts (sm) {
// Command base
const cmdOpts = {
apiKey: this.apiKey,
projectRoot: process.cwd()
}
// Optional options
const optionalParams = {
uploadApiRootUrl: this.endpoint,
bundleUrl: this.bundleUrl || sm.url,
versionName: this.appVersion,
sourceMap: sm.map,
bundle: this.bundle || sm.source,
codeBundleId: this.codeBundleId,
overwrite: this.overwrite,
dryRun: this.dryRun,
logLevel: this.logLevel,
verbose: this.verbose,
retries: this.retries,
timeout: this.timeout
}
for (const [key, value] of Object.entries(optionalParams)) {
if (value !== undefined) {
cmdOpts[key] = value
}
}
return cmdOpts
}
}
module.exports = BugsnagSourceMapUploaderPlugin
// removes a querystring from a file path
const stripQuery = file => {
const queryStringIdx = file.indexOf('?')
if (queryStringIdx < 0) return file
return file.substr(0, queryStringIdx)
}