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
11 changes: 0 additions & 11 deletions tools/sourcemap-tools/src/ContentAppender.ts

This file was deleted.

12 changes: 9 additions & 3 deletions tools/sourcemap-tools/src/SourceProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { BasicSourceMapConsumer, Position, RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
import { DebugIdGenerator } from './DebugIdGenerator';
import { appendBeforeWhitespaces } from './helpers/stringHelpers';
import { stringToUuid } from './helpers/stringToUuid';
import { ResultPromise } from './models/AsyncResult';
import { Err, Ok, Result } from './models/Result';
Expand Down Expand Up @@ -100,17 +101,22 @@ export class SourceProcessor {
}

const sourceSnippet = this._debugIdGenerator.generateSourceSnippet(debugId);
const sourceComment = this._debugIdGenerator.generateSourceComment(debugId);

const newSource = sourceSnippet + '\n' + source + '\n' + sourceComment;
const shebang = source.match(/^(#!.+\n)/)?.[1];
const sourceWithSnippet = shebang
? shebang + sourceSnippet + '\n' + source.substring(shebang.length)
: sourceSnippet + '\n' + source;

const sourceComment = this._debugIdGenerator.generateSourceComment(debugId);
const newSource = appendBeforeWhitespaces(sourceWithSnippet, '\n' + sourceComment);

// We need to offset the source map by amount of lines that we're inserting to the source code
// Sourcemaps map code like this:
// original code X:Y => generated code A:B
// So if we add any code to generated code, mappings after that code will become invalid
// 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 sourceSnippetNewlineCount = (sourceSnippet.match(/\n/g)?.length ?? 0) + (shebang ? 1 : 0);
const offsetSourceMapResult = await this.offsetSourceMap(sourceMap, 0, sourceSnippetNewlineCount + 1);
if (offsetSourceMapResult.isErr()) {
return offsetSourceMapResult;
Expand Down
17 changes: 17 additions & 0 deletions tools/sourcemap-tools/src/helpers/stringHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Appends `value` to `str` before trailing whitespaces in `str`.
* @param str String to append to.
* @param value String to append.
* @example
* const str = 'abc\n\n';
* const value = 'def';
* const appended = appendBeforeWhitespaces(str, value); // 'abcdef\n\n'
*/
export function appendBeforeWhitespaces(str: string, value: string) {
const whitespaces = str.match(/\s*$/)?.[0];
if (!whitespaces) {
return str + value;
}

return str.substring(0, str.length - whitespaces.length) + value + whitespaces;
}
1 change: 0 additions & 1 deletion tools/sourcemap-tools/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './ContentAppender';
export * from './DebugIdGenerator';
export * from './FileFinder';
export * from './Logger';
Expand Down
119 changes: 0 additions & 119 deletions tools/sourcemap-tools/tests/ContentAppender.spec.ts

This file was deleted.

157 changes: 157 additions & 0 deletions tools/sourcemap-tools/tests/SourceProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ describe('SourceProcessor', () => {
mappings: 'AAAA,SAASA,MACLC,QAAQC,IAAI,cAAc,CAC9B,CAEAF,IAAI',
};

const sourceWithShebang = `#!shebang
function foo(){console.log("Hello World!")}foo();`;
const sourceWithShebangMap = {
version: 3,
file: 'source.js',
sources: ['source.js'],
names: ['foo', 'console', 'log'],
mappings: ';AACA,SAASA,MACLC,QAAQC,IAAI,cAAc,CAC9B,CACAF,IAAI',
};

const sourceWithShebangElsewhere = `function foo(){console.log("Hello World!")}foo();
#!shebang`;
const sourceWithShebangElsewhereMap = {
version: 3,
file: 'source.js',
sources: ['source.js'],
names: ['foo', 'console', 'log'],
mappings: 'AACA,SAASA,MACLC,QAAQC,IAAI,cAAc,CAC9B,CACAF,IAAI',
};

describe('processSourceAndSourceMap', () => {
it('should append source snippet to the source on the first line', async () => {
const expected = 'APPENDED_SOURCE';
Expand All @@ -28,6 +48,35 @@ describe('SourceProcessor', () => {
expect(result.data.source).toMatch(new RegExp(`^${expected}\n`));
});

it('should append source snippet to the source on the first line with source having shebang not on the first line', async () => {
const expected = 'APPENDED_SOURCE';
const debugIdGenerator = new DebugIdGenerator();

jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(expected);

const sourceProcessor = new SourceProcessor(debugIdGenerator);
const result = await sourceProcessor.processSourceAndSourceMap(
sourceWithShebangElsewhere,
sourceWithShebangElsewhereMap,
);

assert(result.isOk());
expect(result.data.source).toMatch(new RegExp(`^${expected}\n`));
});

it('should append source snippet to the source after shebang', async () => {
const expected = 'APPENDED_SOURCE';
const debugIdGenerator = new DebugIdGenerator();

jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(expected);

const sourceProcessor = new SourceProcessor(debugIdGenerator);
const result = await sourceProcessor.processSourceAndSourceMap(sourceWithShebang, sourceWithShebangMap);

assert(result.isOk());
expect(result.data.source).toMatch(new RegExp(`^(#!.+\n)${expected}\n`));
});

it('should append comment snippet to the source on the last line', async () => {
const expected = 'APPENDED_COMMENT';
const debugIdGenerator = new DebugIdGenerator();
Expand All @@ -41,6 +90,59 @@ describe('SourceProcessor', () => {
expect(result.data.source).toMatch(new RegExp(`\n${expected}$`));
});

it('should not add any whitespaces at end if there were none before when appending comment snippet', async () => {
const source = `abc`;
const expected = 'APPENDED_COMMENT';
const debugIdGenerator = new DebugIdGenerator();

jest.spyOn(debugIdGenerator, 'generateSourceComment').mockReturnValue(expected);

const sourceProcessor = new SourceProcessor(debugIdGenerator);
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);

assert(result.isOk());
expect(result.data.source).not.toMatch(/\s+$/);
});

it('should leave end whitespaces as they are when appending comment snippet', async () => {
const whitespaces = `\n\n\n \n\t \n\r`;
const source = `abc${whitespaces}`;
const expected = 'APPENDED_COMMENT';
const debugIdGenerator = new DebugIdGenerator();

jest.spyOn(debugIdGenerator, 'generateSourceComment').mockReturnValue(expected);

const sourceProcessor = new SourceProcessor(debugIdGenerator);
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);

assert(result.isOk());
expect(result.data.source).toMatch(new RegExp(`${whitespaces}$`));
});

it('should not touch the original source', async () => {
const debugIdGenerator = new DebugIdGenerator();

jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue('APPENDED_SOURCE');

const sourceProcessor = new SourceProcessor(debugIdGenerator);
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);

assert(result.isOk());
expect(result.data.source).toContain(source);
});

it('should not touch the original sourcemap keys apart from mappings', async () => {
const debugIdGenerator = new DebugIdGenerator();

jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue('APPENDED_SOURCE');

const sourceProcessor = new SourceProcessor(debugIdGenerator);
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);

assert(result.isOk());
expect(result.data.sourceMap).toMatchObject({ ...sourceMap, mappings: result.data.sourceMap.mappings });
});

it('should return sourcemap from DebugIdGenerator', async () => {
const expected = { [SOURCEMAP_DEBUG_ID_KEY]: 'debugId' };
const debugIdGenerator = new DebugIdGenerator();
Expand Down Expand Up @@ -80,6 +182,61 @@ describe('SourceProcessor', () => {
expect(actualPosition).toEqual(expectedPosition);
});

it('should offset sourcemap lines by number of newlines in source snippet + 1 with source having shebang not on the first line', async () => {
const debugIdGenerator = new DebugIdGenerator();
const sourceProcessor = new SourceProcessor(debugIdGenerator);
const snippet = 'a\nb\nc\nd';
const expectedNewLineCount = (snippet.match(/\n/g)?.length ?? 0) + 1;

jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(snippet);

const unmodifiedConsumer = await new SourceMapConsumer(sourceMap);
const expectedPosition = unmodifiedConsumer.originalPositionFor({
line: 1,
column: source.indexOf('foo();'),
});

const result = await sourceProcessor.processSourceAndSourceMap(
sourceWithShebangElsewhere,
sourceWithShebangElsewhereMap,
);
assert(result.isOk());

const modifiedConsumer = await new SourceMapConsumer(result.data.sourceMap);
const actualPosition = modifiedConsumer.originalPositionFor({
line: 1 + expectedNewLineCount,
column: source.indexOf('foo();'),
});

expect(actualPosition).toEqual(expectedPosition);
});

it('should offset sourcemap lines by number of newlines in source with shebang with snippet + 3', async () => {
const debugIdGenerator = new DebugIdGenerator();
const sourceProcessor = new SourceProcessor(debugIdGenerator);
const snippet = 'a\nb\nc\nd';
const expectedNewLineCount = (snippet.match(/\n/g)?.length ?? 0) + 3;

jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(snippet);

const unmodifiedConsumer = await new SourceMapConsumer(sourceMap);
const expectedPosition = unmodifiedConsumer.originalPositionFor({
line: 1,
column: source.indexOf('foo();'),
});

const result = await sourceProcessor.processSourceAndSourceMap(sourceWithShebang, sourceWithShebangMap);
assert(result.isOk());

const modifiedConsumer = await new SourceMapConsumer(result.data.sourceMap);
const actualPosition = modifiedConsumer.originalPositionFor({
line: 1 + expectedNewLineCount,
column: source.indexOf('foo();'),
});

expect(actualPosition).toEqual(expectedPosition);
});

it('should call process function with content from files', async () => {
const sourcePath = path.join(__dirname, './testFiles/source.js');
const sourceMapPath = path.join(__dirname, './testFiles/source.js.map');
Expand Down
Loading