Skip to content

Commit

Permalink
Merge pull request #3 from jridgewell/sections
Browse files Browse the repository at this point in the history
Support sectioned sourcemaps with AnyMap
  • Loading branch information
jridgewell committed Apr 20, 2022
2 parents d4ead19 + 21ca07c commit 45f8062
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 0 deletions.
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']);
});
});
});

0 comments on commit 45f8062

Please sign in to comment.