-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathprocessAndUploadAssetsCommand.ts
More file actions
212 lines (189 loc) · 9.2 KB
/
processAndUploadAssetsCommand.ts
File metadata and controls
212 lines (189 loc) · 9.2 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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import path from 'path';
import { RawSourceMap } from 'source-map';
import { DebugIdGenerator } from '../DebugIdGenerator';
import { SourceProcessor } from '../SourceProcessor';
import { SymbolUploader, SymbolUploaderOptions, UploadResult } from '../SymbolUploader';
import { inspect, mapAsync, pass } from '../helpers/common';
import { flow, pipe } from '../helpers/flow';
import { Asset, AssetWithContent } from '../models/Asset';
import { ProcessAssetError, ProcessAssetResult } from '../models/ProcessAssetResult';
import { R, Result, ResultOk } from '../models/Result';
import { createArchive, finalizeArchive } from './archiveSourceMaps';
import { loadSourceMap, stripSourcesContent } from './loadSourceMaps';
import { processAsset } from './processAsset';
import { uploadArchive } from './uploadArchive';
import { writeAsset } from './writeAsset';
interface BacktracePluginUploadOptions {
/**
* By default, `sourcesContent` in sourcemaps will not be uploaded to Backtrace, even if available in the sourcemap.
* Set this to `true` to upload sourcemaps with `sourcesContent` if available.
*/
readonly includeSources: boolean;
}
/**
* Determines what happens when an individual asset fails to process.
*
* - `'exit'` — abort the entire process and upload (default).
* - `'skip'` — silently skip the failed asset; successfully processed assets are still uploaded.
* - `'warn'` — log the failure via the `assetError` callback and continue uploading the rest.
*/
export type AssetErrorBehavior = 'exit' | 'skip' | 'warn';
export interface BacktracePluginOptions {
/**
* Upload URL for uploading sourcemap files.
* See Source Maps Integration Guide for your instance for more information.
*
* If not set, the sourcemaps will not be uploaded. The sources will be still processed and ready for manual upload.
*/
readonly uploadUrl?: string | URL;
/**
* Additional upload options.
*/
readonly uploadOptions?: SymbolUploaderOptions & BacktracePluginUploadOptions;
/**
* What to do when an individual asset fails to process (e.g. a missing sourcemap file).
*
* - `'warn'` — report the failure via the `assetError` callback and continue uploading the rest. (default)
* - `'skip'` — silently skip the failed asset. Successfully processed assets are still uploaded.
* - `'exit'` — abort the entire process and upload. No sourcemaps will be uploaded.
*
* @default 'warn'
*/
readonly assetErrorBehavior?: AssetErrorBehavior;
}
interface ProcessResult {
readonly assetResults: Result<ProcessAssetResult, ProcessAssetError>[];
readonly uploadResult?: Result<UploadResult, string>;
}
export interface ProcessAndUploadAssetsCommandOptions {
beforeAll?(assets: Asset[]): unknown;
afterAll?(result: ProcessResult): unknown;
beforeProcess?(asset: Asset): unknown;
afterProcess?(asset: ProcessAssetResult): unknown;
beforeWrite?(asset: ProcessAssetResult): unknown;
afterWrite?(asset: ProcessAssetResult): unknown;
assetFinished?(asset: ProcessAssetResult): unknown;
beforeLoad?(asset: Asset): unknown;
afterLoad?(asset: AssetWithContent<RawSourceMap>): unknown;
beforeUpload?(assets: AssetWithContent<RawSourceMap>[]): unknown;
afterUpload?(result: UploadResult): unknown;
assetError?(error: ProcessAssetError): unknown;
assetSkipped?(error: ProcessAssetError): unknown;
processingSummary?(message: string): unknown;
uploadError?(error: string): unknown;
}
export function processAndUploadAssetsCommand(
pluginOptions: BacktracePluginOptions,
options?: ProcessAndUploadAssetsCommandOptions,
) {
const sourceProcessor = new SourceProcessor(new DebugIdGenerator());
const sourceMapUploader = pluginOptions?.uploadUrl
? new SymbolUploader(pluginOptions.uploadUrl, pluginOptions.uploadOptions)
: undefined;
const processCommand = processAsset(sourceProcessor);
const uploadCommand = sourceMapUploader ? uploadArchive(sourceMapUploader) : undefined;
return async function processAndUploadAssets(assets: Asset[]): Promise<ProcessResult> {
options?.beforeAll && options.beforeAll(assets);
const assetResults = await Promise.all(
assets.map((asset) =>
pipe(
asset,
options?.beforeProcess ? inspect(options.beforeProcess) : pass,
processCommand,
R.map(options?.afterProcess ? inspect(options.afterProcess) : pass),
R.map(options?.beforeWrite ? inspect(options.beforeWrite) : pass),
R.map(writeAsset),
R.map(options?.afterWrite ? inspect(options.afterWrite) : pass),
R.map(options?.assetFinished ? inspect(options.assetFinished) : pass),
),
),
);
const assetErrorBehavior = pluginOptions.assetErrorBehavior ?? 'warn';
const failedAssets = assetResults.filter((r) => r.isErr());
const successfulAssets = assetResults.filter((r): r is ResultOk<ProcessAssetResult> => r.isOk());
if (failedAssets.length > 0) {
const summary =
`${failedAssets.length} of ${assets.length} asset(s) failed to process, ` +
`${successfulAssets.length} succeeded`;
switch (assetErrorBehavior) {
case 'exit':
// Report each failure via assetError, then throw to fail the build.
for (const failed of failedAssets) {
if (failed.isErr()) {
options?.assetError && options.assetError(failed.data);
}
}
options?.afterAll && options.afterAll({ assetResults });
throw new Error(
`Backtrace: ${summary}. ` +
`Upload aborted. Set assetErrorBehavior to 'skip' or 'warn' to upload the remaining assets.`,
);
case 'warn':
// Report each failure via assetError and continue uploading the rest.
for (const failed of failedAssets) {
if (failed.isErr()) {
options?.assetError && options.assetError(failed.data);
}
}
options?.processingSummary &&
options.processingSummary(
`${summary}. Continuing with ${successfulAssets.length} successfully processed asset(s).`,
);
break;
case 'skip':
// Silently skip — report via assetSkipped (not assetError).
for (const failed of failedAssets) {
if (failed.isErr()) {
options?.assetSkipped && options.assetSkipped(failed.data);
}
}
options?.processingSummary &&
options.processingSummary(
`${summary}. Skipped failed asset(s), continuing with ${successfulAssets.length} successfully processed asset(s).`,
);
break;
}
}
if (!uploadCommand || successfulAssets.length === 0) {
const result: ProcessResult = { assetResults };
options?.afterAll && options.afterAll(result);
return result;
}
const sourceMapAssets = successfulAssets
.map((r) => r.data)
.map<Asset>((r) => ({
name: path.basename(r.result.sourceMapPath),
path: r.result.sourceMapPath,
}));
const includeSources = pluginOptions?.uploadOptions?.includeSources;
const uploadResult = await pipe(
sourceMapAssets,
flow(
mapAsync(
flow(
options?.beforeLoad ? inspect(options?.beforeLoad) : pass,
loadSourceMap,
R.map(options?.afterLoad ? inspect(options?.afterLoad) : pass),
R.map(includeSources ? pass : stripSourcesContent),
),
),
R.flatMap,
),
R.map(options?.beforeUpload ? inspect(options.beforeUpload) : pass),
R.map(createArchive(sourceProcessor)),
R.map(async ({ assets, archive }) => {
// We first create the upload request, which pipes the archive to itself
const promise = uploadCommand(archive);
// Next we finalize the archive, which causes the assets to be written to the archive, and consequently to the request
await finalizeArchive({ assets, archive });
// Finally, we return the upload request promise
return promise;
}),
R.map(options?.afterUpload ? inspect(options.afterUpload) : pass),
R.mapErr(options?.uploadError ? inspect(options.uploadError) : pass),
);
const result: ProcessResult = { assetResults, uploadResult };
options?.afterAll && options.afterAll(result);
return result;
};
}