Skip to content

Commit 005338c

Browse files
authored
Add SourceProcessor class (#28)
* sourcemap tools: add sourcemap processor for adding source snippets and sourcemap keys * sourcemap tools: fix Prettier issues * sourcemap tools: add semicolon to end of source snippet in DebugIdGenerator * sourcemap tools: renames SourceProcessor tests * sourcemap tools: add comment about addMapping * sourcemap tools: add docs to SourceProcessor * sourcemap tools: add more explanation to offsetting sourcemaps * sourcemap tools: update SourceProcessor tests to test for number of newlines in modified sourcemaps --------- Co-authored-by: Sebastian Alex <sebastian.alex@saucelabs.com>
1 parent 14dc49f commit 005338c

File tree

8 files changed

+207
-1
lines changed

8 files changed

+207
-1
lines changed

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/sourcemap-tools/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,8 @@
4242
"nock": "^13.3.1",
4343
"ts-jest": "^29.1.0",
4444
"typescript": "^5.0.4"
45+
},
46+
"dependencies": {
47+
"source-map": "^0.7.4"
4548
}
4649
}

tools/sourcemap-tools/src/DebugIdGenerator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const SOURCEMAP_DEBUG_ID_KEY = 'debugId';
44

55
export class DebugIdGenerator {
66
public generateSourceSnippet(uuid: string) {
7-
return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e.${SOURCE_DEBUG_ID_VARIABLE}=e.${SOURCE_DEBUG_ID_VARIABLE}||{},e.${SOURCE_DEBUG_ID_VARIABLE}[n]="${uuid}")}catch(e){}}()`;
7+
return `;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e.${SOURCE_DEBUG_ID_VARIABLE}=e.${SOURCE_DEBUG_ID_VARIABLE}||{},e.${SOURCE_DEBUG_ID_VARIABLE}[n]="${uuid}")}catch(e){}}();`;
88
}
99

1010
public generateSourceComment(uuid: string) {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import fs from 'fs';
2+
import { BasicSourceMapConsumer, Position, RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
3+
import { DebugIdGenerator } from './DebugIdGenerator';
4+
import { stringToUuid } from './helpers/stringToUuid';
5+
6+
export class SourceProcessor {
7+
constructor(private readonly _debugIdGenerator: DebugIdGenerator) {}
8+
9+
/**
10+
* Adds required snippets and comments to source, and modifies sourcemap to include debug ID.
11+
* @param source Source content.
12+
* @param sourceMap Sourcemap object or JSON.
13+
* @param debugId Debug ID. If not provided, one will be generated from `source`.
14+
* @returns Used debug ID, new source and new sourcemap.
15+
*/
16+
public async processSourceAndSourceMap(source: string, sourceMap: string | RawSourceMap, debugId?: string) {
17+
if (!debugId) {
18+
debugId = stringToUuid(source);
19+
}
20+
21+
const sourceSnippet = this._debugIdGenerator.generateSourceSnippet(debugId);
22+
const sourceComment = this._debugIdGenerator.generateSourceComment(debugId);
23+
24+
const newSource = sourceSnippet + '\n' + source + '\n' + sourceComment;
25+
26+
// We need to offset the source map by amount of lines that we're inserting to the source code
27+
// Sourcemaps map code like this:
28+
// original code X:Y => generated code A:B
29+
// So if we add any code to generated code, mappings after that code will become invalid
30+
// We need to offset the mapping lines by sourceSnippetNewlineCount:
31+
// original code X:Y => generated code (A + sourceSnippetNewlineCount):B
32+
const sourceSnippetNewlineCount = sourceSnippet.match(/\n/g)?.length ?? 0;
33+
const offsetSourceMap = await this.offsetSourceMap(sourceMap, 0, sourceSnippetNewlineCount + 1);
34+
const newSourceMap = this._debugIdGenerator.addSourceMapKey(offsetSourceMap, debugId);
35+
36+
return { debugId, source: newSource, sourceMap: newSourceMap };
37+
}
38+
39+
/**
40+
* Adds required snippets and comments to source, and modifies sourcemap to include debug ID.
41+
* Will write modified content to the files.
42+
* @param sourcePath Path to the source.
43+
* @param sourceMapPath Path to the sourcemap.
44+
* @param debugId Debug ID. If not provided, one will be generated from `source`.
45+
* @returns Used debug ID.
46+
*/
47+
public async processSourceAndSourceMapFiles(sourcePath: string, sourceMapPath: string, debugId?: string) {
48+
const source = await fs.promises.readFile(sourcePath, 'utf8');
49+
const sourceMap = await fs.promises.readFile(sourceMapPath, 'utf8');
50+
51+
const result = await this.processSourceAndSourceMap(source, sourceMap, debugId);
52+
53+
await fs.promises.writeFile(sourcePath, result.source, 'utf8');
54+
await fs.promises.writeFile(sourceMapPath, JSON.stringify(result.sourceMap), 'utf8');
55+
56+
return result.debugId;
57+
}
58+
59+
private async offsetSourceMap(
60+
sourceMap: string | RawSourceMap,
61+
fromLine: number,
62+
count: number,
63+
): Promise<RawSourceMap> {
64+
const consumer = (await new SourceMapConsumer(sourceMap)) as BasicSourceMapConsumer;
65+
const newSourceMap = new SourceMapGenerator({
66+
file: consumer.file,
67+
sourceRoot: consumer.sourceRoot,
68+
});
69+
70+
consumer.eachMapping((m) => {
71+
if (m.generatedLine < fromLine) {
72+
return;
73+
}
74+
75+
// Despite how the mappings are written, addMapping expects here a null value if the column/line is not set
76+
newSourceMap.addMapping({
77+
source: m.source,
78+
name: m.name,
79+
generated:
80+
m?.generatedColumn != null && m?.generatedLine != null
81+
? { column: m.generatedColumn, line: m.generatedLine + count }
82+
: (null as unknown as Position),
83+
original:
84+
m?.originalColumn != null && m?.originalLine != null
85+
? { column: m.originalColumn, line: m.originalLine }
86+
: (null as unknown as Position),
87+
});
88+
});
89+
90+
return newSourceMap.toJSON();
91+
}
92+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function bytesToUuid(bytes: Buffer) {
2+
return (
3+
bytes.slice(0, 4).toString('hex') +
4+
'-' +
5+
bytes.slice(4, 6).toString('hex') +
6+
'-' +
7+
bytes.slice(6, 8).toString('hex') +
8+
'-' +
9+
bytes.slice(8, 10).toString('hex') +
10+
'-' +
11+
bytes.slice(10, 16).toString('hex')
12+
);
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import crypto from 'crypto';
2+
import { bytesToUuid } from './bytesToUuid';
3+
4+
export function stringToUuid(str: string) {
5+
const bytes = crypto.createHash('sha1').update(str).digest();
6+
return bytesToUuid(bytes);
7+
}

tools/sourcemap-tools/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './ContentAppender';
22
export * from './DebugIdGenerator';
33
export * from './SourceMapUploader';
4+
export * from './SourceProcessor';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { SourceMapConsumer } from 'source-map';
2+
import { DebugIdGenerator, SOURCEMAP_DEBUG_ID_KEY, SourceProcessor } from '../src';
3+
4+
describe('SourceProcessor', () => {
5+
const source = `function foo(){console.log("Hello World!")}foo();`;
6+
const sourceMap = {
7+
version: 3,
8+
file: 'source.js',
9+
sources: ['source.js'],
10+
names: ['foo', 'console', 'log'],
11+
mappings: 'AAAA,SAASA,MACLC,QAAQC,IAAI,cAAc,CAC9B,CAEAF,IAAI',
12+
};
13+
14+
it('should append source snippet to the source on the first line', async () => {
15+
const expected = 'APPENDED_SOURCE';
16+
const debugIdGenerator = new DebugIdGenerator();
17+
18+
jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(expected);
19+
20+
const sourceProcessor = new SourceProcessor(debugIdGenerator);
21+
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);
22+
23+
expect(result.source).toMatch(new RegExp(`^${expected}\n`));
24+
});
25+
26+
it('should append comment snippet to the source on the last line', async () => {
27+
const expected = 'APPENDED_COMMENT';
28+
const debugIdGenerator = new DebugIdGenerator();
29+
30+
jest.spyOn(debugIdGenerator, 'generateSourceComment').mockReturnValue(expected);
31+
32+
const sourceProcessor = new SourceProcessor(debugIdGenerator);
33+
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);
34+
35+
expect(result.source).toMatch(new RegExp(`\n${expected}$`));
36+
});
37+
38+
it('should return sourcemap from DebugIdGenerator', async () => {
39+
const expected = { [SOURCEMAP_DEBUG_ID_KEY]: 'debugId' };
40+
const debugIdGenerator = new DebugIdGenerator();
41+
42+
jest.spyOn(debugIdGenerator, 'addSourceMapKey').mockReturnValue(expected);
43+
44+
const sourceProcessor = new SourceProcessor(debugIdGenerator);
45+
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);
46+
47+
expect(result.sourceMap).toStrictEqual(expected);
48+
});
49+
50+
it('should offset sourcemap lines by number of newlines in source snippet + 1', async () => {
51+
const debugIdGenerator = new DebugIdGenerator();
52+
const sourceProcessor = new SourceProcessor(debugIdGenerator);
53+
const snippet = 'a\nb\nc\nd';
54+
const expectedNewLineCount = (snippet.match(/\n/g)?.length ?? 0) + 1;
55+
56+
jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(snippet);
57+
58+
const unmodifiedConsumer = await new SourceMapConsumer(sourceMap);
59+
const expectedPosition = unmodifiedConsumer.originalPositionFor({ line: 1, column: source.indexOf('foo();') });
60+
61+
const result = await sourceProcessor.processSourceAndSourceMap(source, sourceMap);
62+
63+
const modifiedConsumer = await new SourceMapConsumer(result.sourceMap);
64+
const actualPosition = modifiedConsumer.originalPositionFor({
65+
line: 1 + expectedNewLineCount,
66+
column: source.indexOf('foo();'),
67+
});
68+
69+
expect(actualPosition).toEqual(expectedPosition);
70+
});
71+
});

0 commit comments

Comments
 (0)