@jsenv/sourcemap 1.4.3 → 1.4.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/sourcemap",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,9 +14,10 @@
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
- "magic-string": "1.0.0",
20
+ "magic-string": "1.2.3",
20
21
  "source-map-js": "1.2.1"
21
22
  },
22
23
  "publishConfig": {
@@ -6,22 +6,31 @@ 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
  },
23
31
  remove: ({ start, end }) => {
24
32
  touched = true;
33
+ edits.push({ type: "remove", start, end });
25
34
  magicString.remove(start, end);
26
35
  },
27
36
  toContentAndSourcemap: ({ source } = {}) => {
@@ -38,10 +47,17 @@ export const createMagicSource = (content) => {
38
47
  let map;
39
48
  return {
40
49
  content: code,
50
+ sourcemapEdits: { content, edits },
41
51
  get sourcemap() {
42
52
  if (map === undefined) {
53
+ // "boundary" = a mapping per word boundary. Per-character maps
54
+ // (hires: true) only add sub-word precision and cost ~25% of a
55
+ // package build in generation + composition + GC; line-level maps
56
+ // (hires: false) are cheaper still but collapse heavily rewritten
57
+ // lines (JSX) to their edit points. Word precision is what
58
+ // breakpoints, stack traces and devtools hovers actually consume.
43
59
  map = magicString.generateMap({
44
- hires: true,
60
+ hires: "boundary",
45
61
  includeContent: true,
46
62
  source,
47
63
  });
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 { composeTwoSourcemaps } from "./sourcemap_composition_v3.js";
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
- export const composeTwoSourcemaps = (firstSourcemap, secondSourcemap) => {
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
- const encodedMap = toEncodedMap(genMapping);
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,217 @@
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
+ spans.push({
41
+ start: edit.start,
42
+ end: edit.end,
43
+ replacement: edit.type === "remove" ? "" : edit.replacement,
44
+ });
45
+ }
46
+ if (prependText) {
47
+ spans.push({ start: 0, end: 0, replacement: prependText });
48
+ }
49
+ if (spans.length === 0) {
50
+ return sourcemap;
51
+ }
52
+ spans.sort((a, b) => a.start - b.start || a.end - b.end);
53
+ let spanIndex = 1;
54
+ while (spanIndex < spans.length) {
55
+ const previousSpan = spans[spanIndex - 1];
56
+ const span = spans[spanIndex];
57
+ if (
58
+ span.start === previousSpan.start &&
59
+ span.end === previousSpan.end &&
60
+ span.replacement === previousSpan.replacement
61
+ ) {
62
+ // the same edit applied twice: magic-string re-overwrites the range
63
+ // with the same content, one application yields the same result
64
+ // (happens with import.meta.css template replacements)
65
+ spans.splice(spanIndex, 1);
66
+ continue;
67
+ }
68
+ if (span.start < previousSpan.end) {
69
+ // genuinely overlapping distinct edits: their combined effect cannot
70
+ // be expressed as independent shifts
71
+ return null;
72
+ }
73
+ spanIndex++;
74
+ }
75
+
76
+ let lineStarts;
77
+ const offsetToLineColumn = (offset) => {
78
+ if (!lineStarts) {
79
+ lineStarts = [0];
80
+ let index = content.indexOf("\n");
81
+ while (index !== -1) {
82
+ lineStarts.push(index + 1);
83
+ index = content.indexOf("\n", index + 1);
84
+ }
85
+ }
86
+ let low = 0;
87
+ let high = lineStarts.length - 1;
88
+ while (low < high) {
89
+ const mid = (low + high + 1) >> 1;
90
+ if (lineStarts[mid] <= offset) {
91
+ low = mid;
92
+ } else {
93
+ high = mid - 1;
94
+ }
95
+ }
96
+ return { line: low, column: offset - lineStarts[low] };
97
+ };
98
+
99
+ const records = spans.map(({ start, end, replacement }) => {
100
+ const startLocation = offsetToLineColumn(start);
101
+ const endLocation = offsetToLineColumn(end);
102
+ let insertedLineCount = 0;
103
+ let lastNewlineIndex = -1;
104
+ let newlineIndex = replacement.indexOf("\n");
105
+ while (newlineIndex !== -1) {
106
+ insertedLineCount++;
107
+ lastNewlineIndex = newlineIndex;
108
+ newlineIndex = replacement.indexOf("\n", newlineIndex + 1);
109
+ }
110
+ return {
111
+ startLine: startLocation.line,
112
+ startColumn: startLocation.column,
113
+ endLine: endLocation.line,
114
+ endColumn: endLocation.column,
115
+ insertedLineCount,
116
+ insertedLastLineLength:
117
+ insertedLineCount === 0
118
+ ? replacement.length
119
+ : replacement.length - lastNewlineIndex - 1,
120
+ };
121
+ });
122
+
123
+ // Walk segments and edits together in document order. The transform state
124
+ // says how a position at or after the last applied edit end moves:
125
+ // - on the same old line as that end: rebased on the end's new position
126
+ // - on a later line: shifted by the accumulated line delta
127
+ let recordIndex = 0;
128
+ let lineDelta = 0;
129
+ let tailOldLine = -1;
130
+ let tailOldColumn = 0;
131
+ let tailNewLine = 0;
132
+ let tailNewColumn = 0;
133
+ const transformPosition = (line, column) => {
134
+ if (line === tailOldLine && column >= tailOldColumn) {
135
+ return [tailNewLine, tailNewColumn + (column - tailOldColumn)];
136
+ }
137
+ return [line + lineDelta, column];
138
+ };
139
+ const applyRecord = (record) => {
140
+ const [newStartLine, newStartColumn] = transformPosition(
141
+ record.startLine,
142
+ record.startColumn,
143
+ );
144
+ let newEndLine;
145
+ let newEndColumn;
146
+ if (record.insertedLineCount === 0) {
147
+ newEndLine = newStartLine;
148
+ newEndColumn = newStartColumn + record.insertedLastLineLength;
149
+ } else {
150
+ newEndLine = newStartLine + record.insertedLineCount;
151
+ newEndColumn = record.insertedLastLineLength;
152
+ }
153
+ tailOldLine = record.endLine;
154
+ tailOldColumn = record.endColumn;
155
+ tailNewLine = newEndLine;
156
+ tailNewColumn = newEndColumn;
157
+ lineDelta = newEndLine - record.endLine;
158
+ };
159
+ const isAtOrAfterEnd = (line, column, record) => {
160
+ if (line > record.endLine) {
161
+ return true;
162
+ }
163
+ return line === record.endLine && column >= record.endColumn;
164
+ };
165
+ const isStrictlyInside = (line, column, record) => {
166
+ const afterStart =
167
+ line > record.startLine ||
168
+ (line === record.startLine && column > record.startColumn);
169
+ if (!afterStart) {
170
+ return false;
171
+ }
172
+ return (
173
+ line < record.endLine ||
174
+ (line === record.endLine && column < record.endColumn)
175
+ );
176
+ };
177
+
178
+ const decoded = decode(sourcemap.mappings);
179
+ const decodedShifted = [];
180
+ const pushSegment = (newLine, segment) => {
181
+ while (decodedShifted.length <= newLine) {
182
+ decodedShifted.push([]);
183
+ }
184
+ decodedShifted[newLine].push(segment);
185
+ };
186
+ for (let lineIndex = 0; lineIndex < decoded.length; lineIndex++) {
187
+ for (const segment of decoded[lineIndex]) {
188
+ const column = segment[0];
189
+ while (
190
+ recordIndex < records.length &&
191
+ isAtOrAfterEnd(lineIndex, column, records[recordIndex])
192
+ ) {
193
+ applyRecord(records[recordIndex]);
194
+ recordIndex++;
195
+ }
196
+ if (
197
+ recordIndex < records.length &&
198
+ isStrictlyInside(lineIndex, column, records[recordIndex])
199
+ ) {
200
+ continue;
201
+ }
202
+ const [newLine, newColumn] = transformPosition(lineIndex, column);
203
+ if (newColumn === column && newLine === lineIndex) {
204
+ pushSegment(newLine, segment);
205
+ continue;
206
+ }
207
+ const segmentShifted = segment.slice();
208
+ segmentShifted[0] = newColumn;
209
+ pushSegment(newLine, segmentShifted);
210
+ }
211
+ }
212
+
213
+ return {
214
+ ...sourcemap,
215
+ mappings: encode(decodedShifted),
216
+ };
217
+ };