Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tools/rollup-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export function BacktracePlugin(options?: BacktracePluginOptions): Plugin {
afterWrite: (asset) => debug(`[${asset.asset.name}] wrote source and sourcemap to file`),
assetFinished: (asset) => info(`[${asset.asset.name}] asset processed successfully`),
assetError: (asset) => this.warn(`[${asset.asset.name}] ${asset.error}`),
assetSkipped: (asset) => debug(`[${asset.asset.name}] skipped: ${asset.error}`),
processingSummary: (message) => info(message),

beforeUpload: (paths) => info(`uploading ${paths.length} sourcemaps...`),
afterUpload: (result) => info(`sourcemaps uploaded to Backtrace: ${result.rxid}`),
Expand Down
91 changes: 79 additions & 12 deletions tools/sourcemap-tools/src/commands/processAndUploadAssetsCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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 } from '../models/Result';
import { R, Result, ResultOk } from '../models/Result';
import { createArchive, finalizeArchive } from './archiveSourceMaps';
import { loadSourceMap, stripSourcesContent } from './loadSourceMaps';
import { processAsset } from './processAsset';
Expand All @@ -22,6 +22,15 @@ interface BacktracePluginUploadOptions {
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.
Expand All @@ -35,6 +44,17 @@ export interface BacktracePluginOptions {
* 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 {
Expand All @@ -55,6 +75,8 @@ export interface ProcessAndUploadAssetsCommandOptions {
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;
}

Expand Down Expand Up @@ -84,28 +106,73 @@ export function processAndUploadAssetsCommand(
R.map(writeAsset),
R.map(options?.afterWrite ? inspect(options.afterWrite) : pass),
R.map(options?.assetFinished ? inspect(options.assetFinished) : pass),
R.mapErr(options?.assetError ? inspect(options.assetError) : pass),
),
),
);

const assetsResult = R.flatMap(assetResults);
if (assetsResult.isErr()) {
const result: ProcessResult = { assetResults };
options?.afterAll && options.afterAll(result);
return result;
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) {
if (!uploadCommand || successfulAssets.length === 0) {
const result: ProcessResult = { assetResults };
options?.afterAll && options.afterAll(result);
return result;
}

const sourceMapAssets = assetsResult.data.map<Asset>((r) => ({
name: path.basename(r.result.sourceMapPath),
path: r.result.sourceMapPath,
}));
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;

Expand Down
Loading
Loading