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
83 changes: 28 additions & 55 deletions tools/sourcemap-tools/src/SourceProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fs from 'fs';
import path from 'path';
import { BasicSourceMapConsumer, Position, RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
import { DebugIdGenerator } from './DebugIdGenerator';
import { parseJSON, readFile } from './helpers/common';
import { appendBeforeWhitespaces } from './helpers/stringHelpers';
import { stringToUuid } from './helpers/stringToUuid';
import { ResultPromise } from './models/AsyncResult';
import { AsyncResult, ResultPromise } from './models/AsyncResult';
import { Err, Ok, Result } from './models/Result';

export interface ProcessResult {
Expand All @@ -30,28 +30,13 @@ export class SourceProcessor {
}

public async isSourceFileProcessed(sourcePath: string): ResultPromise<boolean, string> {
const readResult = await this.readFile(sourcePath);
if (readResult.isErr()) {
return readResult;
}

return Ok(this.isSourceProcessed(readResult.data));
return AsyncResult.equip(readFile(sourcePath)).then((v) => this.isSourceProcessed(v)).inner;
}

public async isSourceMapFileProcessed(sourceMapPath: string): ResultPromise<boolean, string> {
const readResult = await this.readFile(sourceMapPath);
if (readResult.isErr()) {
return readResult;
}

let sourcemap: RawSourceMap;
try {
sourcemap = JSON.parse(readResult.data) as RawSourceMap;
} catch (err) {
return Err('failed to parse sourcemap JSON');
}

return Ok(this.isSourceMapProcessed(sourcemap));
return AsyncResult.equip(readFile(sourceMapPath))
.then(parseJSON<RawSourceMap>)
.then((v) => this.isSourceMapProcessed(v)).inner;
}

public getSourceMapDebugId(sourceMap: RawSourceMap): Result<string, string> {
Expand All @@ -64,24 +49,9 @@ export class SourceProcessor {
}

public async getSourceMapFileDebugId(sourceMapPath: string): ResultPromise<string, string> {
const readResult = await this.readFile(sourceMapPath);
if (readResult.isErr()) {
return readResult;
}

let sourcemap: RawSourceMap;
try {
sourcemap = JSON.parse(readResult.data) as RawSourceMap;
} catch (err) {
return Err('failed to parse sourcemap JSON');
}

const debugId = this._debugIdGenerator.getSourceMapDebugId(sourcemap);
if (!debugId) {
return Err('sourcemap does not have a debug ID');
}

return Ok(debugId);
return AsyncResult.equip(readFile(sourceMapPath))
.then(parseJSON<RawSourceMap>)
.then((sourceMap) => this.getSourceMapDebugId(sourceMap)).inner;
}

/**
Expand Down Expand Up @@ -139,7 +109,7 @@ export class SourceProcessor {
sourceMapPath?: string,
debugId?: string,
): ResultPromise<ProcessResultWithPaths, string> {
const sourceReadResult = await this.readFile(sourcePath);
const sourceReadResult = await readFile(sourcePath);
if (sourceReadResult.isErr()) {
return sourceReadResult;
}
Expand All @@ -154,7 +124,7 @@ export class SourceProcessor {
sourceMapPath = path.resolve(path.dirname(sourcePath), match[1]);
}

const sourceMapReadResult = await this.readFile(sourceMapPath);
const sourceMapReadResult = await readFile(sourceMapPath);
if (sourceMapReadResult.isErr()) {
return sourceMapReadResult;
}
Expand All @@ -178,7 +148,11 @@ export class SourceProcessor {
sourceMapPath: string,
): ResultPromise<RawSourceMap, string> {
if (typeof sourceMap === 'string') {
sourceMap = JSON.parse(sourceMap) as RawSourceMap;
const parseResult = parseJSON<RawSourceMap>(sourceMap);
if (parseResult.isErr()) {
return parseResult;
}
sourceMap = parseResult.data;
}

const sourceRoot = sourceMap.sourceRoot
Expand All @@ -187,7 +161,7 @@ export class SourceProcessor {

const sourcesContent: string[] = [];
for (const sourcePath of sourceMap.sources) {
const readResult = await this.readFile(path.resolve(sourceRoot, sourcePath));
const readResult = await readFile(path.resolve(sourceRoot, sourcePath));
if (readResult.isErr()) {
return readResult;
}
Expand All @@ -202,16 +176,23 @@ export class SourceProcessor {
}

public doesSourceMapHaveSources(sourceMap: RawSourceMap): boolean {
return sourceMap.sources.length === sourceMap.sourcesContent?.length;
return sourceMap.sources?.length === sourceMap.sourcesContent?.length;
}

private async offsetSourceMap(
sourceMap: string | RawSourceMap,
fromLine: number,
count: number,
): ResultPromise<RawSourceMap, string> {
const sourceMapObj = typeof sourceMap === 'string' ? (JSON.parse(sourceMap) as RawSourceMap) : sourceMap;
const consumer = (await new SourceMapConsumer(sourceMapObj)) as BasicSourceMapConsumer;
if (typeof sourceMap === 'string') {
const parseResult = parseJSON<RawSourceMap>(sourceMap);
if (parseResult.isErr()) {
return parseResult;
}
sourceMap = parseResult.data;
}

const consumer = (await new SourceMapConsumer(sourceMap)) as BasicSourceMapConsumer;
const newSourceMap = new SourceMapGenerator({
file: consumer.file,
sourceRoot: consumer.sourceRoot,
Expand All @@ -238,14 +219,6 @@ export class SourceProcessor {
});

const newSourceMapJson = newSourceMap.toJSON();
return Ok({ ...sourceMapObj, ...newSourceMapJson });
}

private async readFile(file: string): ResultPromise<string, string> {
try {
return Ok(await fs.promises.readFile(file, 'utf-8'));
} catch (err) {
return Err(`failed to read file: ${err instanceof Error ? err.message : 'unknown error'}`);
}
return Ok({ ...sourceMap, ...newSourceMapJson });
}
}
42 changes: 42 additions & 0 deletions tools/sourcemap-tools/src/commands/archiveSourceMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import fs from 'fs';
import path from 'path';
import { SourceProcessor } from '../SourceProcessor';
import { ZipArchive } from '../ZipArchive';
import { AsyncResult, ResultPromise } from '../models/AsyncResult';
import { Ok, Result, flatMap } from '../models/Result';

export function archiveSourceMaps(sourceProcessor: SourceProcessor) {
return function archiveSourceMaps(sourceMaps: string[]) {
return AsyncResult.fromValue<string[], string>(sourceMaps)
.then(readDebugIds(sourceProcessor))
.then(createArchive).inner;
};
}

function readDebugIds(sourceProcessor: SourceProcessor) {
return async function readDebugIds(files: string[]): Promise<Result<(readonly [string, string])[], string>> {
return flatMap(
await Promise.all(
files.map(
(file) =>
AsyncResult.equip(sourceProcessor.getSourceMapFileDebugId(file)).then(
(result) => [file, result] as const,
).inner,
),
),
);
};
}

async function createArchive(pathsToArchive: (readonly [string, string])[]): ResultPromise<ZipArchive, string> {
const archive = new ZipArchive();

for (const [filePath, debugId] of pathsToArchive) {
const fileName = path.basename(filePath);
const readStream = fs.createReadStream(filePath);
archive.append(`${debugId}-${fileName}`, readStream);
}

await archive.finalize();
return Ok(archive);
}
5 changes: 5 additions & 0 deletions tools/sourcemap-tools/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './archiveSourceMaps';
export * from './processAndUploadAssetsCommand';
export * from './processAsset';
export * from './uploadArchive';
export * from './writeAsset';
109 changes: 109 additions & 0 deletions tools/sourcemap-tools/src/commands/processAndUploadAssetsCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { DebugIdGenerator } from '../DebugIdGenerator';
import { SourceProcessor } from '../SourceProcessor';
import { SymbolUploader, SymbolUploaderOptions, UploadResult } from '../SymbolUploader';
import { ZipArchive } from '../ZipArchive';
import { inspect, pass } from '../helpers/common';
import { Asset } from '../models/Asset';
import { AsyncResult } from '../models/AsyncResult';
import { ProcessAssetError, ProcessAssetResult } from '../models/ProcessAssetResult';
import { Result, flatMap, isErr } from '../models/Result';
import { archiveSourceMaps } from './archiveSourceMaps';
import { processAsset } from './processAsset';
import { uploadArchive } from './uploadArchive';
import { writeAsset } from './writeAsset';

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;
}

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;
beforeArchive?(paths: string[]): void;
afterArchive?(archive: ZipArchive): void;
beforeUpload?(archive: ZipArchive): void;
afterUpload?(result: UploadResult): void;
assetError?(error: ProcessAssetError): void;
uploadError?(error: string): void;
}

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 archiveCommand = archiveSourceMaps(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) =>
AsyncResult.fromValue<Asset, ProcessAssetError>(asset)
.then(options?.beforeProcess ? inspect(options.beforeProcess) : pass)
.then(processCommand)
.then(options?.afterProcess ? inspect(options.afterProcess) : pass)
.then(options?.beforeWrite ? inspect(options.beforeWrite) : pass)
.then(writeAsset)
.then(options?.afterWrite ? inspect(options.afterWrite) : pass)
.then(options?.assetFinished ? inspect(options.assetFinished) : pass)
.thenErr(options?.assetError ? inspect(options.assetError) : pass).inner,
),
);

const assetsResult = flatMap(assetResults);
if (isErr(assetsResult)) {
const result: ProcessResult = { assetResults };
options?.afterAll && options.afterAll(result);
return result;
}

if (!uploadCommand) {
const result: ProcessResult = { assetResults };
options?.afterAll && options.afterAll(result);
return result;
}

const sourceMapPaths = assetsResult.data.map((r) => r.result.sourceMapPath);
const uploadResult = await AsyncResult.fromValue<string[], string>(sourceMapPaths)
.then(options?.beforeArchive ? inspect(options.beforeArchive) : pass)
.then(archiveCommand)
.then(options?.afterArchive ? inspect(options.afterArchive) : pass)
.then(options?.beforeUpload ? inspect(options.beforeUpload) : pass)
.then(uploadCommand)
.then(options?.afterUpload ? inspect(options.afterUpload) : pass)
.thenErr(options?.uploadError ? inspect(options.uploadError) : pass).inner;

const result: ProcessResult = { assetResults, uploadResult };
options?.afterAll && options.afterAll(result);
return result;
};
}
13 changes: 13 additions & 0 deletions tools/sourcemap-tools/src/commands/processAsset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SourceProcessor } from '../SourceProcessor';
import { Asset } from '../models/Asset';
import { AsyncResult } from '../models/AsyncResult';
import { ProcessAssetError, ProcessAssetResult } from '../models/ProcessAssetResult';
import { Result } from '../models/Result';

export function processAsset(sourceProcessor: SourceProcessor) {
return function processAsset(asset: Asset): Promise<Result<ProcessAssetResult, ProcessAssetError>> {
return AsyncResult.equip(sourceProcessor.processSourceAndSourceMapFiles(asset.path))
.then<ProcessAssetResult>((result) => ({ asset, result }))
.thenErr<ProcessAssetError>((error) => ({ asset, error })).inner;
};
}
8 changes: 8 additions & 0 deletions tools/sourcemap-tools/src/commands/uploadArchive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SymbolUploader } from '../SymbolUploader';
import { ZipArchive } from '../ZipArchive';

export function uploadArchive(symbolUploader: SymbolUploader) {
return function uploadArchive(archive: ZipArchive) {
return symbolUploader.uploadSymbol(archive);
};
}
12 changes: 12 additions & 0 deletions tools/sourcemap-tools/src/commands/writeAsset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { writeFile } from '../helpers/common';
import { AsyncResult } from '../models/AsyncResult';
import { ProcessAssetError, ProcessAssetResult } from '../models/ProcessAssetResult';

export function writeAsset(result: ProcessAssetResult) {
const { source, sourcePath: path, sourceMap, sourceMapPath } = result.result;

return AsyncResult.equip(writeFile([source, path]))
.then(() => writeFile([JSON.stringify(sourceMap), sourceMapPath]))
.then(() => result)
.thenErr<ProcessAssetError>((error) => ({ asset: result.asset, error })).inner;
}
Loading