UNPKG

glov-build-sourcemap

Version:

Sourcemap handling helpers for glov-build tasks

282 lines (264 loc) 9.01 kB
const assert = require('assert'); const path = require('path'); const BASE64PRE1 = 'data:application/json;charset=utf8;base64,'; const BASE64PRE2 = 'data:application/json;charset=utf-8;base64,'; const SOURCEMAP_INLINE_PRE1 = `//# sourceMappingURL=${BASE64PRE1}`; // const SOURCEMAP_INLINE_PRE2 = `//# sourceMappingURL=${BASE64PRE2}`; const REGEX_SOURCEMAP_INLINE = /^\/\/# sourceMappingURL=data:application\/json;charset=utf-?8;base64,/m; const REGEX_SOURCEMAP_URL = /^\/\/# sourceMappingURL=(.*)$/m; function forwardSlashes(str) { return str.replace(/\\/g, '/'); } // returns [is_inline, path or map string] function extractSourcemap(code) { let m = code.match(REGEX_SOURCEMAP_URL); if (!m) { // no sourcemaps found, will probably error if expected? return [false, null]; } let pre = m[1].startsWith(BASE64PRE1) ? BASE64PRE1 : m[1].startsWith(BASE64PRE2) ? BASE64PRE2 : null; if (pre) { // Load inline let map_string = Buffer.from(m[1].slice(pre.length), 'base64').toString('utf8'); return [true, map_string]; } return [false, m[1]]; } // Calls next(err, map, raw_sourcemap_file (for pass-through), stripped source string) exports.init = function init(job, file, next) { let code = String(file.contents); let [is_inline, map_url] = extractSourcemap(code); if (!map_url) { // no sourcemaps found, will probably error if expected? job.warn(file, `No sourceMappingURL found in source file ${file.key}`); return void next(null, null, null, code); } let stripped = code.replace(REGEX_SOURCEMAP_URL, ''); if (is_inline) { // Loaded inline // TODO: delay the parsing until needed? let map = JSON.parse(map_url); if (map.file !== file.relative) { // sometimes seeing a dirname-less name for sourceMap.file map.file = file.relative; } return void next(null, map, null, stripped); } job.depAdd(`${file.bucket}:${file.relative}.map`, function (err, map_file) { let map = null; if (map_file) { // TODO: delay this parsing until needed? map = JSON.parse(map_file.contents.toString('utf8')); if (map.file !== file.relative) { // sometimes seeing a dirname-less name for sourceMap.file map.file = file.relative; } } next(err, map, map_file, stripped); }); }; /* // Derived from vinyl-sourcemaps-apply // However, this basically doesn't work because `source-map`::applySourceMap() doesn't // work for anything non-trivial, and the babel-generated sourcemaps are far from trivial const { SourceMapConsumer, SourceMapGenerator } = require('source-map'); exports.apply = function (map, new_map) { if (typeof new_map === 'string') { new_map = JSON.parse(new_map); } // check source map properties assert(new_map.file); assert(new_map.mappings); assert(new_map.sources); // normalize paths new_map.file = forwardSlashes(new_map.file); new_map.sources = new_map.sources.map(forwardSlashes); if (map && map.mappings !== '') { let generator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(new_map)); generator.applySourceMap(new SourceMapConsumer(map)); // TODO: leave as string for efficiency? map = JSON.parse(generator.toString()); } else { map = new_map; } return map; }; */ // From https://github.com/mishoo/UglifyJS/blob/master/lib/sourcemap.js let vlq_char = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); let vlq_bits = vlq_char.reduce(function (map, ch, bits) { map[ch] = bits; return map; }, Object.create(null)); function vlq_decode(indices, str) { let value = 0; let shift = 0; let j = 0; for (let i = 0; i < str.length; i++) { let bits = vlq_bits[str[i]]; value += (bits & 31) << shift; if (bits & 32) { shift += 5; } else { indices[j++] += value & 1 ? 0x80000000 | -(value >> 1) : value >> 1; value = 0; shift = 0; } } return j; } function vlq_encode(num) { let result = ''; num = Math.abs(num) << 1 | num >>> 31; do { let bits = num & 31; if ((num >>>= 5)) { bits |= 32; } result += vlq_char[bits]; } while (num); return result; } exports.decode = function (map) { let indices = [0, 0, 1, 0, 0]; assert(map); assert.equal(map.version, 3); return { names: map.names, // mappings format: // mappings[mapped linenum][idx] = [ mapped char offset, source file index, source line, char start, name index] // sometimes just [mapped char offset] with no mapping, often no name index mappings: map.mappings.split(/;/).map(function (line) { indices[0] = 0; return line.split(/,/).map(function (segment) { return indices.slice(0, vlq_decode(indices, segment)); }); }), sources: map.sources, sourcesContent: map.sourcesContent, }; }; exports.encode = function (filename, map) { let mappings = ''; let prev_src_idx; let generated_line = 1; let generated_column = 0; let source_index = 0; let original_line = 1; let original_column = 0; let name_index = 0; // Derived from https://github.com/mishoo/UglifyJS/blob/master/lib/sourcemap.js function add(src_idx, gen_line, gen_col, orig_line, orig_col, name_idx) { if (prev_src_idx === undefined && src_idx === undefined) { return; } prev_src_idx = src_idx; if (gen_line > generated_line) { generated_column = 0; do { mappings += ';'; } while (++generated_line < gen_line); } else if (mappings) { mappings += ','; } mappings += vlq_encode(gen_col - generated_column); generated_column = gen_col; if (src_idx === undefined) { return; } mappings += vlq_encode(src_idx - source_index); source_index = src_idx; mappings += vlq_encode(orig_line - original_line); original_line = orig_line; mappings += vlq_encode(orig_col - original_column); original_column = orig_col; if (name_idx !== undefined) { mappings += vlq_encode(name_idx - name_index); name_index = name_idx; } } for (let line_num = 0; line_num < map.mappings.length; ++line_num) { let line_map = map.mappings[line_num]; for (let ii = 0; ii < line_map.length; ++ii) { let map_elem = line_map[ii]; add(map_elem[1], line_num + 1, map_elem[0], map_elem[2], map_elem[3], map_elem[4]); } } return { version: 3, file: filename, names: map.names, mappings, sources: map.sources, sourcesContent: map.sourcesContent, }; }; exports.out = function (job, opts) { let { relative, contents, map, inline } = opts; if (!map) { if (contents.match(REGEX_SOURCEMAP_INLINE)) { // has inline sourcemap assert(!inline); // would be a no-op let is_inline; [is_inline, map] = extractSourcemap(contents); assert(map); assert(is_inline); // not a URL to an external file contents = contents.replace(REGEX_SOURCEMAP_URL, ''); } else { assert(false, 'Missing `map` parameter'); } } if (Buffer.isBuffer(map)) { map = map.toString(); } if (typeof map === 'string') { map = JSON.parse(map); } if (typeof map === 'object' && !Buffer.isBuffer(map)) { // should always be true // Fix up paths to be relative to where we're writing the map if (map.sources) { for (let ii = 0; ii < map.sources.length; ++ii) { if (path.dirname(map.sources[ii]) === path.dirname(relative)) { map.sources[ii] = path.basename(map.sources[ii]); } // Remove excessive relative paths to node_modules, they can vary based // on `npm link` situations and make for non-deterministic builds, as well // as (almost certainly) being the wrong levels off due to being calculated // relative to some intermediate file instead of the final output file. map.sources[ii] = map.sources[ii].replace(/(?:\.\.\/)+node_modules/g, 'node_modules'); } } // Don't "fixup" map.file, but just set it to what it should be // if (map.file === 'generated.js') { // Where is this coming from? Browserify, maybe? // // Is it correct to set this always? Seems like. // map.file = path.basename(relative); // } // if (path.dirname(map.file) === path.dirname(relative)) { // map.file = path.basename(map.file); // } // assert.equal(map.file, path.basename(relative)); map.file = path.basename(relative); // Encode back to string map = JSON.stringify(map); } if (!Buffer.isBuffer(map)) { map = Buffer.from(map); } if (inline) { contents = `${contents}\n${SOURCEMAP_INLINE_PRE1}${map.toString('base64')}`; job.out({ relative, contents, }); } else { let sourcemap_filename = `${relative}.map`; contents = `${contents}\n//# sourceMappingURL=${path.basename(sourcemap_filename)}\n`; job.out({ relative, contents, }); job.out({ relative: sourcemap_filename, contents: map, }); } };