1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
from functools import lru_cache
import json
class SourceMapGenerator:
"""
The SourceMapGenerator creates the sourcemap maps the asset bundle to the js/css files.
What is a sourcemap ? (https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map)
In brief: a source map is what makes possible to debug your processed/compiled/minified code as if you were
debugging the original, non-altered source code. It is a file that provides a mapping original <=> processed for
the browser to read.
This implementation of the SourceMapGenerator is a translation and adaptation of this implementation
in js https://github.com/mozilla/source-map. For performance purposes, we have removed all unnecessary
functions/steps for our use case. This simpler version does a line by line mapping, with the ability to
add offsets at the start and end of a file. (when we have to add comments on top a transpiled file by example).
"""
def __init__(self, source_root=None):
self._file = None
self._source_root = source_root
self._sources = {}
self._mappings = []
self._sources_contents = {}
self._version = 3
self._cache = {}
def _serialize_mappings(self):
"""
A source map mapping is encoded with the base 64 VLQ format.
This function encodes the readable source to the format.
:return the encoded content
"""
previous_generated_line = 1
previous_original_line = 0
previous_source = 0
encoded_column = base64vlq_encode(0)
result = ""
for mapping in self._mappings:
if mapping["generatedLine"] != previous_generated_line:
while mapping["generatedLine"] > previous_generated_line:
result += ";"
previous_generated_line += 1
if mapping["source"] is not None:
sourceIdx = self._sources[mapping["source"]]
source = sourceIdx - previous_source
previous_source = sourceIdx
# lines are stored 0-based in SourceMap spec version 3
line = mapping["originalLine"] - 1 - previous_original_line
previous_original_line = mapping["originalLine"] - 1
if (source, line) not in self._cache:
self._cache[(source, line)] = "".join([
encoded_column,
base64vlq_encode(source),
base64vlq_encode(line),
encoded_column,
])
result += self._cache[source, line]
return result
def to_json(self):
"""
Generates the json sourcemap.
It is the main function that assembles all the pieces.
:return {str} valid sourcemap in json format
"""
mapping = {
"version": self._version,
"sources": list(self._sources.keys()),
"mappings": self._serialize_mappings(),
"sourcesContent": [self._sources_contents[source] for source in self._sources]
}
if self._file:
mapping["file"] = self._file
if self._source_root:
mapping["sourceRoot"] = self._source_root
return mapping
def get_content(self):
"""Generates the content of the sourcemap.
:return the content of the sourcemap as a string encoded in UTF-8.
"""
# Store with XSSI-prevention prefix
return b")]}'\n" + json.dumps(self.to_json()).encode('utf8')
def add_source(self, source_name, source_content, last_index, start_offset=0):
"""Adds a new source file in the sourcemap. All the lines of the source file will be mapped line by line
to the generated file from the (last_index + start_offset). All lines between
last_index and (last_index + start_offset) will
be mapped to line 1 of the source file.
Example:
ls 1 = Line 1 from new source file
lg 1 = Line 1 from genereted file
ls 1 <=> lg 1 Line 1 from new source file is map to Line 1 from genereted file
nb_ls = number of lines in the new source file
Step 1:
ls 1 <=> lg last_index + 1
Step 2:
ls 1 <=> lg last_index + start_offset + 1
ls 2 <=> lg last_index + start_offset + 2
...
ls nb_ls <=> lg last_index + start_offset + nb_ls
:param source_name: name of the source to add
:param source_content: content of the source to add
:param last_index: Line where we start to map the new source
:param start_offset: Number of lines to pass in the generated file before starting mapping line by line
"""
source_line_count = len(source_content.split("\n"))
self._sources.setdefault(source_name, len(self._sources))
self._sources_contents[source_name] = source_content
if start_offset > 0:
# adds a mapping between the first line of the source
# and the first line of the corresponding code in the generated file.
self._mappings.append({
"generatedLine": last_index + 1,
"originalLine": 1,
"source": source_name,
})
for i in range(1, source_line_count + 1):
self._mappings.append({
"generatedLine": last_index + i + start_offset,
"originalLine": i,
"source": source_name,
})
B64CHARS = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
SHIFTSIZE, FLAG, MASK = 5, 1 << 5, (1 << 5) - 1
@lru_cache(maxsize=64)
def base64vlq_encode(*values):
"""
Encode Base64 VLQ encoded sequences
https://gist.github.com/mjpieters/86b0d152bb51d5f5979346d11005588b
Base64 VLQ is used in source maps.
VLQ values consist of 6 bits (matching the 64 characters of the Base64
alphabet), with the most significant bit a *continuation* flag. If the
flag is set, then the next character in the input is part of the same
integer value. Multiple VLQ character sequences so form an unbounded
integer value, in little-endian order.
The *first* VLQ value consists of a continuation flag, 4 bits for the
value, and the last bit the *sign* of the integer:
+-----+-----+-----+-----+-----+-----+
| c | b3 | b2 | b1 | b0 | s |
+-----+-----+-----+-----+-----+-----+
while subsequent VLQ characters contain 5 bits of value:
+-----+-----+-----+-----+-----+-----+
| c | b4 | b3 | b2 | b1 | b0 |
+-----+-----+-----+-----+-----+-----+
For source maps, Base64 VLQ sequences can contain 1, 4 or 5 elements.
"""
results = []
add = results.append
for v in values:
# add sign bit
v = (abs(v) << 1) | int(v < 0)
while True:
toencode, v = v & MASK, v >> SHIFTSIZE
add(toencode | (v and FLAG))
if not v:
break
return bytes(map(B64CHARS.__getitem__, results)).decode()
|