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
21 changes: 20 additions & 1 deletion tools/sourcemap-tools/src/DebugIdGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,29 @@ export class DebugIdGenerator {
return `//# ${SOURCE_DEBUG_ID_COMMENT}=${uuid}`;
}

public addSourceMapKey<T extends object>(sourceMap: T, uuid: string): T & { [SOURCEMAP_DEBUG_ID_KEY]: string } {
public getSourceDebugId(source: string): string | undefined {
const regex = new RegExp(`^//# ${SOURCE_DEBUG_ID_COMMENT}=(.+)$`, 'm');
const match = source.match(regex);
if (!match) {
return undefined;
}

return match[1];
}

public addSourceMapDebugId<T extends object>(sourceMap: T, uuid: string): T & { [SOURCEMAP_DEBUG_ID_KEY]: string } {
return {
...sourceMap,
[SOURCEMAP_DEBUG_ID_KEY]: uuid,
};
}

public getSourceMapDebugId(sourcemap: object): string | undefined {
const debugId = (sourcemap as Record<string, unknown>)[SOURCEMAP_DEBUG_ID_KEY];
if (typeof debugId !== 'string') {
return undefined;
}

return debugId;
}
}
38 changes: 38 additions & 0 deletions tools/sourcemap-tools/src/FileFinder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import fs from 'fs';
import path from 'path';
import { ResultPromise } from './models/AsyncResult';
import { Ok } from './models/Result';

interface SearchOptions {
readonly recursive?: boolean;
readonly match?: RegExp;
}

export class FileFinder {
public async find(dir: string, options?: SearchOptions): ResultPromise<string[], string> {
const result: string[] = [];
const files = await fs.promises.readdir(dir);

for (const file of files) {
const fullPath = path.resolve(dir, file);
const stat = await fs.promises.stat(fullPath);
if (stat.isDirectory()) {
if (options?.recursive) {
const innerFindResult = await this.find(fullPath, options);
if (innerFindResult.isErr()) {
return innerFindResult;
}
files.push(...innerFindResult.data);
}

continue;
}

if (!options?.match || fullPath.match(options.match)) {
result.push(fullPath);
}
}

return Ok(result);
}
}
10 changes: 10 additions & 0 deletions tools/sourcemap-tools/src/Logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Logger {
error(value: unknown | Error, ...args: unknown[]): void;
warn(value: unknown | Error, ...args: unknown[]): void;
info(value: unknown | Error, ...args: unknown[]): void;
debug(value: unknown | Error, ...args: unknown[]): void;
trace(value: unknown | Error, ...args: unknown[]): void;
log(level: LogLevel, value: unknown | Error, ...args: unknown[]): void;
}

export type LogLevel = keyof Pick<Logger, 'error' | 'warn' | 'info' | 'debug' | 'trace'>;
151 changes: 136 additions & 15 deletions tools/sourcemap-tools/src/SourceProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,70 @@
import fs from 'fs';
import path from 'path';
import { BasicSourceMapConsumer, Position, RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
import { DebugIdGenerator } from './DebugIdGenerator';
import { stringToUuid } from './helpers/stringToUuid';
import { ResultPromise } from './models/AsyncResult';
import { Err, Ok } from './models/Result';

export interface ProcessResult {
readonly debugId: string;
readonly source: string;
readonly sourceMap: RawSourceMap;
}

export interface ProcessResultWithPaths extends ProcessResult {
readonly sourcePath: string;
readonly sourceMapPath: string;
}

export class SourceProcessor {
constructor(private readonly _debugIdGenerator: DebugIdGenerator) {}

public isSourceProcessed(source: string): boolean {
return !!this._debugIdGenerator.getSourceDebugId(source);
}

public isSourceMapProcessed(sourceMap: RawSourceMap): boolean {
return !!this._debugIdGenerator.getSourceMapDebugId(sourceMap);
}

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

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

/**
* Adds required snippets and comments to source, and modifies sourcemap to include debug ID.
* @param source Source content.
* @param sourceMap Sourcemap object or JSON.
* @param debugId Debug ID. If not provided, one will be generated from `source`.
* @returns Used debug ID, new source and new sourcemap.
*/
public async processSourceAndSourceMap(source: string, sourceMap: string | RawSourceMap, debugId?: string) {
public async processSourceAndSourceMap(
source: string,
sourceMap: string | RawSourceMap,
debugId?: string,
): ResultPromise<ProcessResult, string> {
if (!debugId) {
debugId = stringToUuid(source);
}
Expand All @@ -30,38 +81,100 @@ export class SourceProcessor {
// We need to offset the mapping lines by sourceSnippetNewlineCount:
// original code X:Y => generated code (A + sourceSnippetNewlineCount):B
const sourceSnippetNewlineCount = sourceSnippet.match(/\n/g)?.length ?? 0;
const offsetSourceMap = await this.offsetSourceMap(sourceMap, 0, sourceSnippetNewlineCount + 1);
const newSourceMap = this._debugIdGenerator.addSourceMapKey(offsetSourceMap, debugId);
const offsetSourceMapResult = await this.offsetSourceMap(sourceMap, 0, sourceSnippetNewlineCount + 1);
if (offsetSourceMapResult.isErr()) {
return offsetSourceMapResult;
}

return { debugId, source: newSource, sourceMap: newSourceMap };
const newSourceMap = this._debugIdGenerator.addSourceMapDebugId(offsetSourceMapResult.data, debugId);
return Ok({ debugId, source: newSource, sourceMap: newSourceMap });
}

/**
* Adds required snippets and comments to source, and modifies sourcemap to include debug ID.
* Will write modified content to the files.
* @param sourcePath Path to the source.
* @param sourceMapPath Path to the sourcemap.
* @param sourceMapPath Path to the sourcemap. If not specified, will try to resolve from sourceMapURL.
* @param debugId Debug ID. If not provided, one will be generated from `source`.
* @returns Used debug ID.
*/
public async processSourceAndSourceMapFiles(sourcePath: string, sourceMapPath: string, debugId?: string) {
const source = await fs.promises.readFile(sourcePath, 'utf8');
const sourceMap = await fs.promises.readFile(sourceMapPath, 'utf8');
public async processSourceAndSourceMapFiles(
sourcePath: string,
sourceMapPath?: string,
debugId?: string,
): ResultPromise<ProcessResultWithPaths, string> {
const sourceReadResult = await this.readFile(sourcePath);
if (sourceReadResult.isErr()) {
return sourceReadResult;
}

const source = sourceReadResult.data;
if (!sourceMapPath) {
const match = source.match(/^\/\/# sourceMappingURL=(.+)$/m);
if (!match || !match[1]) {
return Err('Could not find source map for source.');
}

sourceMapPath = path.resolve(path.dirname(sourcePath), match[1]);
}

const sourceMapReadResult = await this.readFile(sourceMapPath);
if (sourceMapReadResult.isErr()) {
return sourceMapReadResult;
}

const sourceMap = sourceMapReadResult.data;

const processResult = await this.processSourceAndSourceMap(source, sourceMap, debugId);
if (processResult.isErr()) {
return processResult;
}

return Ok({
...processResult.data,
sourcePath,
sourceMapPath,
} as ProcessResultWithPaths);
}

public async addSourcesToSourceMap(
sourceMap: string | RawSourceMap,
sourceMapPath: string,
): ResultPromise<RawSourceMap, string> {
if (typeof sourceMap === 'string') {
sourceMap = JSON.parse(sourceMap) as RawSourceMap;
}

const sourceRoot = sourceMap.sourceRoot
? path.resolve(path.dirname(sourceMapPath), sourceMap.sourceRoot)
: path.resolve(path.dirname(sourceMapPath));

const result = await this.processSourceAndSourceMap(source, sourceMap, debugId);
const sourcesContent: string[] = [];
for (const sourcePath of sourceMap.sources) {
const readResult = await this.readFile(path.resolve(sourceRoot, sourcePath));
if (readResult.isErr()) {
return readResult;
}

await fs.promises.writeFile(sourcePath, result.source, 'utf8');
await fs.promises.writeFile(sourceMapPath, JSON.stringify(result.sourceMap), 'utf8');
sourcesContent.push(readResult.data);
}

return result.debugId;
return Ok({
...sourceMap,
sourcesContent,
});
}

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

private async offsetSourceMap(
sourceMap: string | RawSourceMap,
fromLine: number,
count: number,
): Promise<RawSourceMap> {
const sourceMapObj = typeof sourceMap === 'string' ? JSON.parse(sourceMap) : sourceMap;
): ResultPromise<RawSourceMap, string> {
const sourceMapObj = typeof sourceMap === 'string' ? (JSON.parse(sourceMap) as RawSourceMap) : sourceMap;
const consumer = (await new SourceMapConsumer(sourceMapObj)) as BasicSourceMapConsumer;
const newSourceMap = new SourceMapGenerator({
file: consumer.file,
Expand Down Expand Up @@ -89,6 +202,14 @@ export class SourceProcessor {
});

const newSourceMapJson = newSourceMap.toJSON();
return { ...sourceMapObj, ...newSourceMapJson };
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'}`);
}
}
}
28 changes: 16 additions & 12 deletions tools/sourcemap-tools/src/SymbolUploader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import http from 'http';
import https from 'https';
import { Readable } from 'stream';
import { ResultPromise } from './models/AsyncResult';
import { Err, Ok, Result } from './models/Result';

interface CoronerUploadResponse {
response: 'ok' | string;
Expand Down Expand Up @@ -32,10 +34,10 @@ export class SymbolUploader {
* Uploads the symbol to Backtrace.
* @param content Symbol stream.
*/
public async uploadSymbol(readable: Readable): Promise<UploadResult> {
public async uploadSymbol(readable: Readable): ResultPromise<UploadResult, string> {
const protocol = this._url.protocol === 'https:' ? https : http;

return new Promise<UploadResult>((resolve, reject) => {
return new Promise<Result<UploadResult, string>>((resolve, reject) => {
const request = protocol.request(
this._url,
{
Expand All @@ -45,35 +47,37 @@ export class SymbolUploader {
},
(response) => {
if (!response.statusCode) {
return reject(new Error('Failed to upload symbol: failed to make the request.'));
return resolve(Err('Failed to upload symbol: failed to make the request.'));
}

const data: Buffer[] = [];
response.on('data', (chunk) => {
data.push(chunk);
});

response.on('error', reject);

response.on('end', () => {
const rawResponse = Buffer.concat(data).toString('utf-8');
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
return reject(
new Error(
`Failed to upload symbol: ${response.statusCode}. Response data: ${rawResponse}`,
),
return resolve(
Err(`Failed to upload symbol: ${response.statusCode}. Response data: ${rawResponse}`),
);
}

try {
const responseData = JSON.parse(rawResponse) as CoronerUploadResponse;
if (responseData.response === 'ok') {
return resolve({
rxid: responseData._rxid,
});
return resolve(
Ok({
rxid: responseData._rxid,
}),
);
} else {
return reject(new Error(`Non-OK response received from Coroner: ${rawResponse}`));
return resolve(Err(`Non-OK response received from Coroner: ${rawResponse}`));
}
} catch (err) {
return reject(new Error(`Cannot parse response from Coroner: ${rawResponse}`));
return resolve(Err(`Cannot parse response from Coroner: ${rawResponse}`));
}
});
},
Expand Down
4 changes: 4 additions & 0 deletions tools/sourcemap-tools/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export * from './ContentAppender';
export * from './DebugIdGenerator';
export * from './FileFinder';
export * from './Logger';
export * from './SourceProcessor';
export * from './SymbolUploader';
export * from './ZipArchive';
export * from './models/AsyncResult';
export * from './models/Result';
Loading