@jsenv/sourcemap 1.4.4 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/magic_source.js +15 -0
- package/src/main.js +5 -1
- package/src/sourcemap_composition_v3.js +63 -33
- package/src/sourcemap_edits.js +255 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/sourcemap",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@jridgewell/gen-mapping": "0.3.13",
|
|
17
|
+
"@jridgewell/sourcemap-codec": "1.6.0",
|
|
17
18
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
18
19
|
"@jsenv/urls": "2.9.10",
|
|
19
20
|
"magic-string": "1.2.3",
|
package/src/magic_source.js
CHANGED
|
@@ -6,22 +6,36 @@ export const createMagicSource = (content) => {
|
|
|
6
6
|
}
|
|
7
7
|
const magicString = new MagicString(content);
|
|
8
8
|
let touched = false;
|
|
9
|
+
// the edit list mirrors what magicString receives, in the coordinate
|
|
10
|
+
// space of the initial content: it lets a consumer holding a sourcemap
|
|
11
|
+
// for that initial content apply the edits as position shifts instead of
|
|
12
|
+
// composing with a generated map (see sourcemap_edits.js)
|
|
13
|
+
const edits = [];
|
|
9
14
|
|
|
10
15
|
return {
|
|
11
16
|
prepend: (string) => {
|
|
12
17
|
touched = true;
|
|
18
|
+
edits.push({ type: "prepend", text: string });
|
|
13
19
|
magicString.prepend(string);
|
|
14
20
|
},
|
|
15
21
|
append: (string) => {
|
|
16
22
|
touched = true;
|
|
23
|
+
edits.push({ type: "append" });
|
|
17
24
|
magicString.append(string);
|
|
18
25
|
},
|
|
19
26
|
replace: ({ start, end, replacement }) => {
|
|
20
27
|
touched = true;
|
|
28
|
+
edits.push({ type: "replace", start, end, replacement });
|
|
21
29
|
magicString.overwrite(start, end, replacement);
|
|
22
30
|
},
|
|
31
|
+
insert: ({ position, text }) => {
|
|
32
|
+
touched = true;
|
|
33
|
+
edits.push({ type: "insert", position, text });
|
|
34
|
+
magicString.appendLeft(position, text);
|
|
35
|
+
},
|
|
23
36
|
remove: ({ start, end }) => {
|
|
24
37
|
touched = true;
|
|
38
|
+
edits.push({ type: "remove", start, end });
|
|
25
39
|
magicString.remove(start, end);
|
|
26
40
|
},
|
|
27
41
|
toContentAndSourcemap: ({ source } = {}) => {
|
|
@@ -38,6 +52,7 @@ export const createMagicSource = (content) => {
|
|
|
38
52
|
let map;
|
|
39
53
|
return {
|
|
40
54
|
content: code,
|
|
55
|
+
sourcemapEdits: { content, edits },
|
|
41
56
|
get sourcemap() {
|
|
42
57
|
if (map === undefined) {
|
|
43
58
|
// "boundary" = a mapping per word boundary. Per-character maps
|
package/src/main.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// tslint:disable:ordered-imports
|
|
2
2
|
|
|
3
3
|
export { createMagicSource } from "./magic_source.js";
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
composeSourcemaps,
|
|
6
|
+
composeTwoSourcemaps,
|
|
7
|
+
} from "./sourcemap_composition_v3.js";
|
|
8
|
+
export { applyContentEditsOnSourcemap } from "./sourcemap_edits.js";
|
|
5
9
|
|
|
6
10
|
export { getOriginalPosition } from "./original_position.js";
|
|
7
11
|
export { sourcemapConverter } from "./sourcemap_converter.js";
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import {
|
|
14
14
|
addMapping,
|
|
15
15
|
GenMapping,
|
|
16
|
+
toDecodedMap,
|
|
16
17
|
toEncodedMap,
|
|
17
18
|
} from "@jridgewell/gen-mapping";
|
|
18
19
|
import {
|
|
@@ -21,6 +22,66 @@ import {
|
|
|
21
22
|
TraceMap,
|
|
22
23
|
} from "@jridgewell/trace-mapping";
|
|
23
24
|
|
|
25
|
+
export const composeTwoSourcemaps = (firstSourcemap, secondSourcemap) => {
|
|
26
|
+
return composeSourcemaps([firstSourcemap, secondSourcemap]);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// A chain [A, B, C] where each map describes the content the next one was
|
|
30
|
+
// generated from: A maps the oldest content back to the true source(s),
|
|
31
|
+
// C maps the final content back to B's output. Composing pairwise left to
|
|
32
|
+
// right is the correct semantic, but a standalone pairwise composition
|
|
33
|
+
// encodes its result to VLQ mappings only for the next pair to decode them
|
|
34
|
+
// again; here intermediate results stay decoded and encoding happens once,
|
|
35
|
+
// on the last pair. Falsy entries are skipped (a step that produced no map).
|
|
36
|
+
export const composeSourcemaps = (sourcemaps) => {
|
|
37
|
+
const maps = [];
|
|
38
|
+
for (const sourcemap of sourcemaps) {
|
|
39
|
+
if (sourcemap) {
|
|
40
|
+
maps.push(sourcemap);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (maps.length === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (maps.length === 1) {
|
|
47
|
+
return maps[0];
|
|
48
|
+
}
|
|
49
|
+
const headSourcemap = maps[0];
|
|
50
|
+
let traceMap = new TraceMap(headSourcemap);
|
|
51
|
+
let genMapping;
|
|
52
|
+
for (let i = 1; i < maps.length; i++) {
|
|
53
|
+
if (genMapping) {
|
|
54
|
+
// TraceMap accepts decoded mappings: no VLQ roundtrip between pairs
|
|
55
|
+
traceMap = new TraceMap(toDecodedMap(genMapping));
|
|
56
|
+
}
|
|
57
|
+
genMapping = composePairIntoGenMapping(traceMap, new TraceMap(maps[i]));
|
|
58
|
+
}
|
|
59
|
+
const encodedMap = toEncodedMap(genMapping);
|
|
60
|
+
const sourcemap = {
|
|
61
|
+
version: 3,
|
|
62
|
+
sources: [...encodedMap.sources],
|
|
63
|
+
names: [...encodedMap.names],
|
|
64
|
+
mappings: encodedMap.mappings,
|
|
65
|
+
};
|
|
66
|
+
// sourcesContent is taken from the head map: every source surviving the
|
|
67
|
+
// composition originates from it (a later map's "sources" describe
|
|
68
|
+
// intermediate contents, not true sources).
|
|
69
|
+
const sourcesContent = [];
|
|
70
|
+
const headSourcesContent = headSourcemap.sourcesContent;
|
|
71
|
+
sourcemap.sources.forEach((source) => {
|
|
72
|
+
if (headSourcesContent) {
|
|
73
|
+
const headSourceIndex = headSourcemap.sources.indexOf(source);
|
|
74
|
+
if (headSourceIndex > -1) {
|
|
75
|
+
sourcesContent.push(headSourcesContent[headSourceIndex]);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
sourcesContent.push(null);
|
|
80
|
+
});
|
|
81
|
+
sourcemap.sourcesContent = sourcesContent;
|
|
82
|
+
return sourcemap;
|
|
83
|
+
};
|
|
84
|
+
|
|
24
85
|
// "first" maps an intermediate content back to the true original source(s);
|
|
25
86
|
// "second" maps the final content back to that same intermediate content
|
|
26
87
|
// (its "original" positions live in the coordinate space "first" was
|
|
@@ -29,19 +90,8 @@ import {
|
|
|
29
90
|
// don't share a single coordinate space, so naively adding both to the same
|
|
30
91
|
// generator produces mappings that silently collide/override each other
|
|
31
92
|
// wherever "second" happens to cover a position "first" also maps.
|
|
32
|
-
|
|
33
|
-
if (!firstSourcemap && !secondSourcemap) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
if (!firstSourcemap) {
|
|
37
|
-
return secondSourcemap;
|
|
38
|
-
}
|
|
39
|
-
if (!secondSourcemap) {
|
|
40
|
-
return firstSourcemap;
|
|
41
|
-
}
|
|
93
|
+
const composePairIntoGenMapping = (firstTraceMap, secondTraceMap) => {
|
|
42
94
|
const genMapping = new GenMapping();
|
|
43
|
-
const firstTraceMap = new TraceMap(firstSourcemap);
|
|
44
|
-
const secondTraceMap = new TraceMap(secondSourcemap);
|
|
45
95
|
eachMapping(
|
|
46
96
|
secondTraceMap,
|
|
47
97
|
({
|
|
@@ -76,25 +126,5 @@ export const composeTwoSourcemaps = (firstSourcemap, secondSourcemap) => {
|
|
|
76
126
|
});
|
|
77
127
|
},
|
|
78
128
|
);
|
|
79
|
-
|
|
80
|
-
const sourcemap = {
|
|
81
|
-
version: 3,
|
|
82
|
-
sources: [...encodedMap.sources],
|
|
83
|
-
names: [...encodedMap.names],
|
|
84
|
-
mappings: encodedMap.mappings,
|
|
85
|
-
};
|
|
86
|
-
const sourcesContent = [];
|
|
87
|
-
const firstSourcesContent = firstSourcemap.sourcesContent;
|
|
88
|
-
sourcemap.sources.forEach((source) => {
|
|
89
|
-
if (firstSourcesContent) {
|
|
90
|
-
const firstSourceIndex = firstSourcemap.sources.indexOf(source);
|
|
91
|
-
if (firstSourceIndex > -1) {
|
|
92
|
-
sourcesContent.push(firstSourcesContent[firstSourceIndex]);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
sourcesContent.push(null);
|
|
97
|
-
});
|
|
98
|
-
sourcemap.sourcesContent = sourcesContent;
|
|
99
|
-
return sourcemap;
|
|
129
|
+
return genMapping;
|
|
100
130
|
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Compose a sourcemap with a set of content edits WITHOUT generating a map
|
|
3
|
+
* for the edited content: the edits are applied to the existing map as
|
|
4
|
+
* position shifts. Composing map A with the generated map of the edits
|
|
5
|
+
* walks every segment of a full-file map (boundary density) and traces
|
|
6
|
+
* each one through A; shifting walks A's own segments once with pure
|
|
7
|
+
* arithmetic, and the result keeps exactly A's density and information (a
|
|
8
|
+
* generated edit map adds no knowledge about the unchanged content, it
|
|
9
|
+
* only densifies around it).
|
|
10
|
+
*
|
|
11
|
+
* Edit offsets refer to the content BEFORE any edit (magic-string
|
|
12
|
+
* semantics): every edit lives in that single coordinate space, and the
|
|
13
|
+
* shifts accumulate while walking segments and edits together in document
|
|
14
|
+
* order. Segments strictly inside a replaced span are dropped (that
|
|
15
|
+
* position does not exist anymore — magic-string drops interior mappings
|
|
16
|
+
* the same way).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { decode, encode } from "@jridgewell/sourcemap-codec";
|
|
20
|
+
|
|
21
|
+
export const applyContentEditsOnSourcemap = (sourcemap, { content, edits }) => {
|
|
22
|
+
if (typeof sourcemap.mappings !== "string") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Flatten the edit list into sorted, non-overlapping spans.
|
|
27
|
+
// - append never moves anything before it: ignored
|
|
28
|
+
// - prepend accumulates into a single insertion at (0, 0); successive
|
|
29
|
+
// magic-string prepends each land BEFORE the previous one
|
|
30
|
+
const spans = [];
|
|
31
|
+
let prependText = "";
|
|
32
|
+
for (const edit of edits) {
|
|
33
|
+
if (edit.type === "append") {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (edit.type === "prepend") {
|
|
37
|
+
prependText = edit.text + prependText;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (edit.type === "insert") {
|
|
41
|
+
spans.push({
|
|
42
|
+
start: edit.position,
|
|
43
|
+
end: edit.position,
|
|
44
|
+
replacement: edit.text,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
spans.push({
|
|
49
|
+
start: edit.start,
|
|
50
|
+
end: edit.end,
|
|
51
|
+
replacement: edit.type === "remove" ? "" : edit.replacement,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (prependText) {
|
|
55
|
+
spans.push({ start: 0, end: 0, replacement: prependText });
|
|
56
|
+
}
|
|
57
|
+
if (spans.length === 0) {
|
|
58
|
+
return sourcemap;
|
|
59
|
+
}
|
|
60
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
61
|
+
let spanIndex = 1;
|
|
62
|
+
while (spanIndex < spans.length) {
|
|
63
|
+
const previousSpan = spans[spanIndex - 1];
|
|
64
|
+
const span = spans[spanIndex];
|
|
65
|
+
if (
|
|
66
|
+
span.start === previousSpan.start &&
|
|
67
|
+
span.end === previousSpan.end &&
|
|
68
|
+
span.replacement === previousSpan.replacement
|
|
69
|
+
) {
|
|
70
|
+
// the same edit applied twice: magic-string re-overwrites the range
|
|
71
|
+
// with the same content, one application yields the same result
|
|
72
|
+
// (happens with import.meta.css template replacements)
|
|
73
|
+
spans.splice(spanIndex, 1);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (span.start < previousSpan.end) {
|
|
77
|
+
// genuinely overlapping distinct edits: their combined effect cannot
|
|
78
|
+
// be expressed as independent shifts
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
spanIndex++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let lineStarts;
|
|
85
|
+
const offsetToLineColumn = (offset) => {
|
|
86
|
+
if (!lineStarts) {
|
|
87
|
+
lineStarts = [0];
|
|
88
|
+
let index = content.indexOf("\n");
|
|
89
|
+
while (index !== -1) {
|
|
90
|
+
lineStarts.push(index + 1);
|
|
91
|
+
index = content.indexOf("\n", index + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let low = 0;
|
|
95
|
+
let high = lineStarts.length - 1;
|
|
96
|
+
while (low < high) {
|
|
97
|
+
const mid = (low + high + 1) >> 1;
|
|
98
|
+
if (lineStarts[mid] <= offset) {
|
|
99
|
+
low = mid;
|
|
100
|
+
} else {
|
|
101
|
+
high = mid - 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { line: low, column: offset - lineStarts[low] };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const records = spans.map(({ start, end, replacement }) => {
|
|
108
|
+
const startLocation = offsetToLineColumn(start);
|
|
109
|
+
const endLocation = offsetToLineColumn(end);
|
|
110
|
+
let insertedLineCount = 0;
|
|
111
|
+
let lastNewlineIndex = -1;
|
|
112
|
+
let newlineIndex = replacement.indexOf("\n");
|
|
113
|
+
while (newlineIndex !== -1) {
|
|
114
|
+
insertedLineCount++;
|
|
115
|
+
lastNewlineIndex = newlineIndex;
|
|
116
|
+
newlineIndex = replacement.indexOf("\n", newlineIndex + 1);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
startLine: startLocation.line,
|
|
120
|
+
startColumn: startLocation.column,
|
|
121
|
+
endLine: endLocation.line,
|
|
122
|
+
endColumn: endLocation.column,
|
|
123
|
+
insertedLineCount,
|
|
124
|
+
insertedLastLineLength:
|
|
125
|
+
insertedLineCount === 0
|
|
126
|
+
? replacement.length
|
|
127
|
+
: replacement.length - lastNewlineIndex - 1,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Walk segments and edits together in document order. The transform state
|
|
132
|
+
// says how a position at or after the last applied edit end moves:
|
|
133
|
+
// - on the same old line as that end: rebased on the end's new position
|
|
134
|
+
// - on a later line: shifted by the accumulated line delta
|
|
135
|
+
let recordIndex = 0;
|
|
136
|
+
let lineDelta = 0;
|
|
137
|
+
let tailOldLine = -1;
|
|
138
|
+
let tailOldColumn = 0;
|
|
139
|
+
let tailNewLine = 0;
|
|
140
|
+
let tailNewColumn = 0;
|
|
141
|
+
const transformPosition = (line, column) => {
|
|
142
|
+
if (line === tailOldLine && column >= tailOldColumn) {
|
|
143
|
+
return [tailNewLine, tailNewColumn + (column - tailOldColumn)];
|
|
144
|
+
}
|
|
145
|
+
return [line + lineDelta, column];
|
|
146
|
+
};
|
|
147
|
+
const applyRecord = (record) => {
|
|
148
|
+
const [newStartLine, newStartColumn] = transformPosition(
|
|
149
|
+
record.startLine,
|
|
150
|
+
record.startColumn,
|
|
151
|
+
);
|
|
152
|
+
let newEndLine;
|
|
153
|
+
let newEndColumn;
|
|
154
|
+
if (record.insertedLineCount === 0) {
|
|
155
|
+
newEndLine = newStartLine;
|
|
156
|
+
newEndColumn = newStartColumn + record.insertedLastLineLength;
|
|
157
|
+
} else {
|
|
158
|
+
newEndLine = newStartLine + record.insertedLineCount;
|
|
159
|
+
newEndColumn = record.insertedLastLineLength;
|
|
160
|
+
}
|
|
161
|
+
tailOldLine = record.endLine;
|
|
162
|
+
tailOldColumn = record.endColumn;
|
|
163
|
+
tailNewLine = newEndLine;
|
|
164
|
+
tailNewColumn = newEndColumn;
|
|
165
|
+
lineDelta = newEndLine - record.endLine;
|
|
166
|
+
};
|
|
167
|
+
const isAtOrAfterEnd = (line, column, record) => {
|
|
168
|
+
if (line > record.endLine) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return line === record.endLine && column >= record.endColumn;
|
|
172
|
+
};
|
|
173
|
+
const isStrictlyInside = (line, column, record) => {
|
|
174
|
+
const afterStart =
|
|
175
|
+
line > record.startLine ||
|
|
176
|
+
(line === record.startLine && column > record.startColumn);
|
|
177
|
+
if (!afterStart) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return (
|
|
181
|
+
line < record.endLine ||
|
|
182
|
+
(line === record.endLine && column < record.endColumn)
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// decode() output is owned here: segments are moved (and their column
|
|
187
|
+
// mutated) into the shifted structure without copies
|
|
188
|
+
const decoded = decode(sourcemap.mappings);
|
|
189
|
+
const decodedShifted = [];
|
|
190
|
+
const pushSegment = (newLine, segment) => {
|
|
191
|
+
while (decodedShifted.length <= newLine) {
|
|
192
|
+
decodedShifted.push([]);
|
|
193
|
+
}
|
|
194
|
+
decodedShifted[newLine].push(segment);
|
|
195
|
+
};
|
|
196
|
+
for (let lineIndex = 0; lineIndex < decoded.length; lineIndex++) {
|
|
197
|
+
const segments = decoded[lineIndex];
|
|
198
|
+
// whole-line fast path: no edit starts at or before this line anymore,
|
|
199
|
+
// and the tail rebase does not apply to it — the line moves as one block
|
|
200
|
+
if (
|
|
201
|
+
lineIndex !== tailOldLine &&
|
|
202
|
+
(recordIndex === records.length ||
|
|
203
|
+
records[recordIndex].startLine > lineIndex)
|
|
204
|
+
) {
|
|
205
|
+
if (segments.length > 0) {
|
|
206
|
+
const newLine = lineIndex + lineDelta;
|
|
207
|
+
while (decodedShifted.length <= newLine) {
|
|
208
|
+
decodedShifted.push([]);
|
|
209
|
+
}
|
|
210
|
+
if (decodedShifted[newLine].length === 0) {
|
|
211
|
+
decodedShifted[newLine] = segments;
|
|
212
|
+
} else {
|
|
213
|
+
for (const segment of segments) {
|
|
214
|
+
decodedShifted[newLine].push(segment);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
for (const segment of segments) {
|
|
221
|
+
const column = segment[0];
|
|
222
|
+
while (
|
|
223
|
+
recordIndex < records.length &&
|
|
224
|
+
isAtOrAfterEnd(lineIndex, column, records[recordIndex])
|
|
225
|
+
) {
|
|
226
|
+
applyRecord(records[recordIndex]);
|
|
227
|
+
recordIndex++;
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
recordIndex < records.length &&
|
|
231
|
+
isStrictlyInside(lineIndex, column, records[recordIndex])
|
|
232
|
+
) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
let newLine;
|
|
236
|
+
let newColumn;
|
|
237
|
+
if (lineIndex === tailOldLine && column >= tailOldColumn) {
|
|
238
|
+
newLine = tailNewLine;
|
|
239
|
+
newColumn = tailNewColumn + (column - tailOldColumn);
|
|
240
|
+
} else {
|
|
241
|
+
newLine = lineIndex + lineDelta;
|
|
242
|
+
newColumn = column;
|
|
243
|
+
}
|
|
244
|
+
if (newColumn !== column) {
|
|
245
|
+
segment[0] = newColumn;
|
|
246
|
+
}
|
|
247
|
+
pushSegment(newLine, segment);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...sourcemap,
|
|
253
|
+
mappings: encode(decodedShifted),
|
|
254
|
+
};
|
|
255
|
+
};
|