Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sectioned sourcemaps with AnyMap #3

Merged
merged 1 commit into from Apr 20, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/any-map.ts
@@ -0,0 +1,128 @@
import { TraceMap, presortedDecodedMap, decodedMappings } from './trace-mapping';
import {
COLUMN,
SOURCES_INDEX,
SOURCE_LINE,
SOURCE_COLUMN,
NAMES_INDEX,
} from './sourcemap-segment';

import type { Section, DecodedSourceMap, SectionedSourceMapInput } from './types';
import type { SourceMapSegment } from './sourcemap-segment';

type AnyMap = {
new (map: SectionedSourceMapInput, mapUrl?: string | null): TraceMap;
(map: SectionedSourceMapInput, mapUrl?: string | null): TraceMap;
};

export const AnyMap: AnyMap = function (map, mapUrl) {
const parsed =
typeof map === 'string' ? (JSON.parse(map) as Exclude<SectionedSourceMapInput, string>) : map;

if (!('sections' in parsed)) return new TraceMap(parsed, mapUrl);

const mappings: SourceMapSegment[][] = [];
const sources: string[] = [];
const sourcesContent: (string | null)[] = [];
const names: string[] = [];
const { sections } = parsed;

let i = 0;
for (; i < sections.length - 1; i++) {
const no = sections[i + 1].offset;
addSection(sections[i], mapUrl, mappings, sources, sourcesContent, names, no.line, no.column);
}
for (; i < sections.length; i++) {
addSection(sections[i], mapUrl, mappings, sources, sourcesContent, names, -1, -1);
}

const joined: DecodedSourceMap = {
version: 3,
file: parsed.file,
names,
sources,
sourcesContent,
mappings,
};

return presortedDecodedMap(joined);
} as AnyMap;

function addSection(
section: Section,
mapUrl: string | null | undefined,
mappings: SourceMapSegment[][],
sources: string[],
sourcesContent: (string | null)[],
names: string[],
stopLine: number,
stopColumn: number,
) {
const { offset, map } = section;
const { line: lineOffset, column: columnOffset } = offset;

// If this section jumps forwards several lines, we need to add lines to the output mappings catch up.
for (let i = mappings.length; i <= lineOffset; i++) mappings.push([]);

const trace = AnyMap(map, mapUrl);
const sourcesOffset = sources.length;
const namesOffset = names.length;
const decoded = decodedMappings(trace);
const { resolvedSources } = trace;
append(sources, resolvedSources);
append(sourcesContent, trace.sourcesContent || fillSourcesContent(resolvedSources.length));
append(names, trace.names);

// We can only add so many lines before we step into the range that the next section's map
// controls. When we get to the last line, then we'll start checking the segments to see if
// they've crossed into the column range.
const len = stopLine === -1 ? decoded.length : Math.min(decoded.length, stopLine + 1);

for (let i = 0; i < len; i++) {
const line = decoded[i];
// On the 0th loop, the line will already exist due to a previous section, or the line catch up
// loop above.
const out = i === 0 ? mappings[lineOffset] : (mappings[lineOffset + i] = []);
// On the 0th loop, the section's column offset shifts us forward. On all other lines (since the
// map can be multiple lines), it doesn't.
const cOffset = i === 0 ? columnOffset : 0;

for (let j = 0; j < line.length; j++) {
const seg = line[j];
const column = cOffset + seg[COLUMN];

// If this segment steps into the column range that the next section's map controls, we need
// to stop early.
if (i === stopLine && column >= stopColumn) break;

if (seg.length === 1) {
out.push([column]);
continue;
}

const sourcesIndex = sourcesOffset + seg[SOURCES_INDEX];
const sourceLine = seg[SOURCE_LINE];
const sourceColumn = seg[SOURCE_COLUMN];
if (seg.length === 4) {
out.push([column, sourcesIndex, sourceLine, sourceColumn]);
continue;
}

out.push([column, sourcesIndex, sourceLine, sourceColumn, namesOffset + seg[NAMES_INDEX]]);
}
}
}

function append<T>(arr: T[], other: T[]) {
for (let i = 0; i < other.length; i++) arr.push(other[i]);
}

// Sourcemaps don't need to have sourcesContent, and if they don't, we need to create an array of
// equal length to the sources. This is because the sources and sourcesContent are paired arrays,
// where `sourcesContent[i]` is the content of the `sources[i]` file. If we didn't, then joined
// sourcemap would desynchronize the sources/contents.
function fillSourcesContent(len: number): null[] {
const sourcesContent = [];
for (let i = 0; i < len; i++) sourcesContent[i] = null;
return sourcesContent;
}
4 changes: 4 additions & 0 deletions src/trace-mapping.ts
Expand Up @@ -41,8 +41,10 @@ import type { MemoState } from './binary-search';
export type { SourceMapSegment } from './sourcemap-segment';
export type {
SourceMapInput,
SectionedSourceMapInput,
DecodedSourceMap,
EncodedSourceMap,
SectionedSourceMap,
InvalidOriginalMapping,
OriginalMapping as Mapping,
OriginalMapping,
Expand Down Expand Up @@ -122,6 +124,8 @@ export let eachMapping: (map: TraceMap, cb: (mapping: EachMapping) => void) => v
*/
export let presortedDecodedMap: (map: DecodedSourceMap, mapUrl?: string) => TraceMap;

export { AnyMap } from './any-map';

export class TraceMap implements SourceMap {
declare version: SourceMapV3['version'];
declare file: SourceMapV3['file'];
Expand Down
15 changes: 15 additions & 0 deletions src/types.ts
Expand Up @@ -17,6 +17,20 @@ export interface DecodedSourceMap extends SourceMapV3 {
mappings: SourceMapSegment[][];
}

export interface Section {
offset: {
line: number;
column: number;
};
map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap;
}

export interface SectionedSourceMap {
file?: string | null;
sections: Section[];
version: 3;
}

export type OriginalMapping = {
source: string | null;
line: number;
Expand All @@ -41,6 +55,7 @@ export type InvalidGeneratedMapping = {
};

export type SourceMapInput = string | EncodedSourceMap | DecodedSourceMap;
export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMap;

export type Needle = { line: number; column: number; bias?: 1 | -1 };
export type SourceNeedle = { source: string; line: number; column: number; bias?: 1 | -1 };
Expand Down
102 changes: 102 additions & 0 deletions test/trace-mapping.test.ts
Expand Up @@ -5,6 +5,7 @@ import { encode, decode } from '@jridgewell/sourcemap-codec';
import { test, describe } from './setup';
import {
TraceMap,
AnyMap,
encodedMappings,
decodedMappings,
traceSegment,
Expand All @@ -22,6 +23,7 @@ import type {
EncodedSourceMap,
DecodedSourceMap,
EachMapping,
SectionedSourceMap,
} from '../src/trace-mapping';

describe('TraceMap', () => {
Expand Down Expand Up @@ -373,3 +375,103 @@ describe('TraceMap', () => {
});
});
});

describe('AnyMap', () => {
const map: SectionedSourceMap = {
version: 3,
file: 'sectioned.js',
sections: [
{
offset: { line: 1, column: 1 },
map: {
version: 3,
names: ['first'],
sources: ['first.js'],
sourcesContent: ['firstsource'],
mappings: 'AAAAA,CAAC',
},
},
{
offset: { line: 2, column: 0 },
map: {
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: {
version: 3,
names: ['second'],
sources: ['second.js'],
sourcesContent: ['secondsource'],
sourceRoot: 'nested',
mappings: 'AAAAA,CAAA;AAAA',
},
},
{
offset: { line: 0, column: 1 },
map: {
version: 3,
names: [],
sources: ['third.js'],
sourcesContent: ['thirdsource'],
mappings: 'AAAA',
},
},
],
},
},
],
};

describe('map properties', () => {
test('version', (t) => {
const tracer = new AnyMap(map);
t.is(tracer.version, map.version);
});

test('file', (t) => {
const tracer = new AnyMap(map);
t.is(tracer.file, map.file);
});

test('sourceRoot', (t) => {
const tracer = new AnyMap(map);
t.is(tracer.sourceRoot, undefined);
});

test('sources', (t) => {
const tracer = new AnyMap(map);
t.deepEqual(tracer.sources, ['first.js', 'nested/second.js', 'third.js']);
});

test('names', (t) => {
const tracer = new AnyMap(map);
t.deepEqual(tracer.names, ['first', 'second']);
});

test('encodedMappings', (t) => {
const tracer = new AnyMap(map);
t.is(encodedMappings(tracer), ';CAAAA,CAAC;ACADC,CCAA');
});

test('decodedMappings', (t) => {
const tracer = new AnyMap(map);
t.deepEqual(decodedMappings(tracer), [
[],
[
[1, 0, 0, 0, 0],
[2, 0, 0, 1],
],
[
[0, 1, 0, 0, 1],
[1, 2, 0, 0],
],
]);
});

test('sourcesContent', (t) => {
const tracer = new AnyMap(map);
t.deepEqual(tracer.sourcesContent, ['firstsource', 'secondsource', 'thirdsource']);
});
});
});