180 lines
7.0 KiB
Python
180 lines
7.0 KiB
Python
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()
|