Convert src/compiler/utils/mapped_code.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent e86fb16402
commit b684833126

@ -1,350 +1,373 @@
import type { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping';
import remapping from '@ampproject/remapping'; import remapping from '@ampproject/remapping';
import { SourceMap } from 'magic-string'; import { push_array } from './push_array.js';
import { Source, Processed } from '../preprocess/types';
import { push_array } from './push_array';
export type SourceLocation = { /**
line: number; * @param {string} s
column: number; */
}; function last_line_length(s) {
return s.length - s.lastIndexOf('\n') - 1;
function last_line_length(s: string) {
return s.length - s.lastIndexOf('\n') - 1;
} }
// mutate map in-place // mutate map in-place
export function sourcemap_add_offset(
map: DecodedSourceMap,
offset: SourceLocation,
source_index: number
) {
if (map.mappings.length == 0) return;
for (let line = 0; line < map.mappings.length; line++) {
const segment_list = map.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
// shift only segments that belong to component source file
if (seg[1] === source_index) {
// also ensures that seg.length >= 4
// shift column if it points at the first line
if (seg[2] === 0) {
seg[3] += offset.column;
}
// shift line
seg[2] += offset.line;
}
}
}
}
function merge_tables<T>(this_table: T[], other_table: T[]): [T[], number[], boolean, boolean] { /**
const new_table = this_table.slice(); * @param {DecodedSourceMap} map
const idx_map = []; * @param {SourceLocation} offset
other_table = other_table || []; * @param {number} source_index
let val_changed = false; */
for (const [other_idx, other_val] of other_table.entries()) { export function sourcemap_add_offset(map, offset, source_index) {
const this_idx = this_table.indexOf(other_val); if (map.mappings.length == 0)
if (this_idx >= 0) { return;
idx_map[other_idx] = this_idx; for (let line = 0; line < map.mappings.length; line++) {
} else { const segment_list = map.mappings[line];
const new_idx = new_table.length; for (let segment = 0; segment < segment_list.length; segment++) {
new_table[new_idx] = other_val; const seg = segment_list[segment];
idx_map[other_idx] = new_idx; // shift only segments that belong to component source file
val_changed = true; if (seg[1] === source_index) {
} // also ensures that seg.length >= 4
} // shift column if it points at the first line
let idx_changed = val_changed; if (seg[2] === 0) {
if (val_changed) { seg[3] += offset.column;
if (idx_map.find((val, idx) => val != idx) === undefined) { }
// idx_map is identity map [0, 1, 2, 3, 4, ....] // shift line
idx_changed = false; seg[2] += offset.line;
} }
} }
return [new_table, idx_map, val_changed, idx_changed]; }
} }
/**
* @param {T[]} this_table
* @param {T[]} other_table
* @returns {[T[], number[], boolean, boolean]}
*/
function merge_tables(this_table, other_table) {
const new_table = this_table.slice();
const idx_map = [];
other_table = other_table || [];
let val_changed = false;
for (const [other_idx, other_val] of other_table.entries()) {
const this_idx = this_table.indexOf(other_val);
if (this_idx >= 0) {
idx_map[other_idx] = this_idx;
}
else {
const new_idx = new_table.length;
new_table[new_idx] = other_val;
idx_map[other_idx] = new_idx;
val_changed = true;
}
}
let idx_changed = val_changed;
if (val_changed) {
if (idx_map.find((val, idx) => val != idx) === undefined) {
// idx_map is identity map [0, 1, 2, 3, 4, ....]
idx_changed = false;
}
}
return [new_table, idx_map, val_changed, idx_changed];
}
const regex_line_token = /([^\d\w\s]|\s+)/g; const regex_line_token = /([^\d\w\s]|\s+)/g;
/** */
export class MappedCode { export class MappedCode {
string: string;
map: DecodedSourceMap;
constructor(string = '', map: DecodedSourceMap = null) {
this.string = string;
if (map) {
this.map = map as DecodedSourceMap;
} else {
this.map = {
version: 3,
mappings: [],
sources: [],
names: []
};
}
}
/**
* concat in-place (mutable), return this (chainable)
* will also mutate the `other` object
*/
concat(other: MappedCode): MappedCode {
// noop: if one is empty, return the other
if (other.string == '') return this;
if (this.string == '') {
this.string = other.string;
this.map = other.map;
return this;
}
// compute last line length before mutating
const column_offset = last_line_length(this.string);
this.string += other.string;
const m1 = this.map;
const m2 = other.map;
if (m2.mappings.length == 0) return this;
// combine sources and names
const [sources, new_source_idx, sources_changed, sources_idx_changed] = merge_tables(
m1.sources,
m2.sources
);
const [names, new_name_idx, names_changed, names_idx_changed] = merge_tables(
m1.names,
m2.names
);
if (sources_changed) m1.sources = sources;
if (names_changed) m1.names = names;
// unswitched loops are faster
if (sources_idx_changed && names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
}
}
} else if (sources_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
}
}
} else if (names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
}
}
}
// combine the mappings
// combine
// 1. last line of first map
// 2. first line of second map
// columns of 2 must be shifted
if (m2.mappings.length > 0 && column_offset > 0) {
const first_line = m2.mappings[0];
for (let i = 0; i < first_line.length; i++) {
first_line[i][0] += column_offset;
}
}
// combine last line + first line
push_array(m1.mappings[m1.mappings.length - 1], m2.mappings.shift());
// append other lines
push_array(m1.mappings, m2.mappings);
return this;
}
static from_processed(string: string, map?: DecodedSourceMap): MappedCode {
const line_count = string.split('\n').length;
if (map) { /**
// ensure that count of source map mappings lines * @type {string}
// is equal to count of generated code lines */
// (some tools may produce less) string = undefined;
const missing_lines = line_count - map.mappings.length;
for (let i = 0; i < missing_lines; i++) { /**
map.mappings.push([]); * @type {DecodedSourceMap}
} */
return new MappedCode(string, map); map = undefined;
} constructor(string = '', map = null) {
this.string = string;
if (string == '') return new MappedCode(); if (map) {
map = { version: 3, names: [], sources: [], mappings: [] }; this.map = map;
}
// add empty SourceMapSegment[] for every line else {
for (let i = 0; i < line_count; i++) map.mappings.push([]); this.map = {
return new MappedCode(string, map); version: 3,
} mappings: [],
sources: [],
static from_source({ source, file_basename, get_location }: Source): MappedCode { names: []
let offset: SourceLocation = get_location(0); };
}
if (!offset) offset = { line: 0, column: 0 }; }
const map: DecodedSourceMap = { version: 3, names: [], sources: [file_basename], mappings: [] }; /**
if (source == '') return new MappedCode(source, map); * concat in-place (mutable), return this (chainable)
* will also mutate the `other` object
// we create a high resolution identity map here, * @param {MappedCode} other
// we know that it will eventually be merged with svelte's map, * @returns {import("C:/repos/svelte/svelte/mapped_code.ts-to-jsdoc").MappedCode}
// at which stage the resolution will decrease. */
const line_list = source.split('\n'); concat(other) {
for (let line = 0; line < line_list.length; line++) { // noop: if one is empty, return the other
map.mappings.push([]); if (other.string == '')
const token_list = line_list[line].split(regex_line_token); return this;
for (let token = 0, column = 0; token < token_list.length; token++) { if (this.string == '') {
if (token_list[token] == '') continue; this.string = other.string;
map.mappings[line].push([column, 0, offset.line + line, column]); this.map = other.map;
column += token_list[token].length; return this;
} }
} // compute last line length before mutating
// shift columns in first line const column_offset = last_line_length(this.string);
const segment_list = map.mappings[0]; this.string += other.string;
for (let segment = 0; segment < segment_list.length; segment++) { const m1 = this.map;
segment_list[segment][3] += offset.column; const m2 = other.map;
} if (m2.mappings.length == 0)
return new MappedCode(source, map); return this;
} // combine sources and names
const [sources, new_source_idx, sources_changed, sources_idx_changed] = merge_tables(m1.sources, m2.sources);
const [names, new_name_idx, names_changed, names_idx_changed] = merge_tables(m1.names, m2.names);
if (sources_changed)
m1.sources = sources;
if (names_changed)
m1.names = names;
// unswitched loops are faster
if (sources_idx_changed && names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1] >= 0)
seg[1] = new_source_idx[seg[1]];
if (seg[4] >= 0)
seg[4] = new_name_idx[seg[4]];
}
}
}
else if (sources_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1] >= 0)
seg[1] = new_source_idx[seg[1]];
}
}
}
else if (names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[4] >= 0)
seg[4] = new_name_idx[seg[4]];
}
}
}
// combine the mappings
// combine
// 1. last line of first map
// 2. first line of second map
// columns of 2 must be shifted
if (m2.mappings.length > 0 && column_offset > 0) {
const first_line = m2.mappings[0];
for (let i = 0; i < first_line.length; i++) {
first_line[i][0] += column_offset;
}
}
// combine last line + first line
push_array(m1.mappings[m1.mappings.length - 1], m2.mappings.shift());
// append other lines
push_array(m1.mappings, m2.mappings);
return this;
}
/**
* @static
* @param {string} string
* @param {DecodedSourceMap} [map]
* @returns {import("C:/repos/svelte/svelte/mapped_code.ts-to-jsdoc").MappedCode}
*/
static from_processed(string, map) {
const line_count = string.split('\n').length;
if (map) {
// ensure that count of source map mappings lines
// is equal to count of generated code lines
// (some tools may produce less)
const missing_lines = line_count - map.mappings.length;
for (let i = 0; i < missing_lines; i++) {
map.mappings.push([]);
}
return new MappedCode(string, map);
}
if (string == '')
return new MappedCode();
map = { version: 3, names: [], sources: [], mappings: [] };
// add empty SourceMapSegment[] for every line
for (let i = 0; i < line_count; i++)
map.mappings.push([]);
return new MappedCode(string, map);
}
/**
* @static
* @param {Source}
* @returns {import("C:/repos/svelte/svelte/mapped_code.ts-to-jsdoc").MappedCode}
*/
static from_source({ source, file_basename, get_location }) {
/**
* @type {SourceLocation}
*/
let offset = get_location(0);
if (!offset)
offset = { line: 0, column: 0 };
/**
* @type {DecodedSourceMap}
*/
const map = { version: 3, names: [], sources: [file_basename], mappings: [] };
if (source == '')
return new MappedCode(source, map);
// we create a high resolution identity map here,
// we know that it will eventually be merged with svelte's map,
// at which stage the resolution will decrease.
const line_list = source.split('\n');
for (let line = 0; line < line_list.length; line++) {
map.mappings.push([]);
const token_list = line_list[line].split(regex_line_token);
for (let token = 0, column = 0; token < token_list.length; token++) {
if (token_list[token] == '')
continue;
map.mappings[line].push([column, 0, offset.line + line, column]);
column += token_list[token].length;
}
}
// shift columns in first line
const segment_list = map.mappings[0];
for (let segment = 0; segment < segment_list.length; segment++) {
segment_list[segment][3] += offset.column;
}
return new MappedCode(source, map);
}
} }
export function combine_sourcemaps( /**
filename: string, * @param {string} filename
sourcemap_list: Array<DecodedSourceMap | RawSourceMap> * @param {Array<DecodedSourceMap | RawSourceMap>} sourcemap_list
) { */
if (sourcemap_list.length == 0) return null; export function combine_sourcemaps(filename, sourcemap_list) {
if (sourcemap_list.length == 0)
let map_idx = 1; return null;
const map = let map_idx = 1;
sourcemap_list.slice(0, -1).find((m) => m.sources.length !== 1) === undefined const map = sourcemap_list.slice(0, -1).find((m) => m.sources.length !== 1) === undefined
? remapping( ? remapping(
// use array interface // use array interface
// only the oldest sourcemap can have multiple sources // only the oldest sourcemap can have multiple sources
sourcemap_list, sourcemap_list, () => null, true // skip optional field `sourcesContent`
() => null, )
true // skip optional field `sourcesContent` : remapping(
) // use loader interface
: remapping( sourcemap_list[0], // last map
// use loader interface function loader(sourcefile) {
sourcemap_list[0], // last map if (sourcefile === filename && sourcemap_list[map_idx]) {
function loader(sourcefile) { return sourcemap_list[map_idx++]; // idx 1, 2, ...
if (sourcefile === filename && sourcemap_list[map_idx]) { // bundle file = branch node
return sourcemap_list[map_idx++]; // idx 1, 2, ... }
// bundle file = branch node else {
} else { return null; // source file = leaf node
return null; // source file = leaf node }
} }, true);
} as SourceMapLoader, if (!map.file)
true delete map.file; // skip optional field `file`
); // When source maps are combined and the leading map is empty, sources is not set.
// Add the filename to the empty array in this case.
if (!map.file) delete map.file; // skip optional field `file` // Further improvements to remapping may help address this as well https://github.com/ampproject/remapping/issues/116
if (!map.sources.length)
// When source maps are combined and the leading map is empty, sources is not set. map.sources = [filename];
// Add the filename to the empty array in this case. return map;
// Further improvements to remapping may help address this as well https://github.com/ampproject/remapping/issues/116
if (!map.sources.length) map.sources = [filename];
return map;
} }
// browser vs node.js // browser vs node.js
const b64enc = typeof btoa == 'function' ? btoa : (b) => Buffer.from(b).toString('base64'); const b64enc = typeof btoa == 'function' ? btoa : (b) => Buffer.from(b).toString('base64');
const b64dec = typeof atob == 'function' ? atob : (a) => Buffer.from(a, 'base64').toString(); const b64dec = typeof atob == 'function' ? atob : (a) => Buffer.from(a, 'base64').toString();
export function apply_preprocessor_sourcemap( /**
filename: string, * @param {string} filename
svelte_map: SourceMap, * @param {SourceMap} svelte_map
preprocessor_map_input: string | DecodedSourceMap | RawSourceMap * @param {string | DecodedSourceMap | RawSourceMap} preprocessor_map_input
): SourceMap { * @returns {SourceMap}
if (!svelte_map || !preprocessor_map_input) return svelte_map; */
export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) {
const preprocessor_map = if (!svelte_map || !preprocessor_map_input)
typeof preprocessor_map_input === 'string' return svelte_map;
? JSON.parse(preprocessor_map_input) const preprocessor_map = typeof preprocessor_map_input === 'string'
: preprocessor_map_input; ? JSON.parse(preprocessor_map_input)
: preprocessor_map_input;
const result_map = combine_sourcemaps(filename, [ const result_map = combine_sourcemaps(filename, [
svelte_map as RawSourceMap, svelte_map,
preprocessor_map preprocessor_map
]) as RawSourceMap; ]);
// Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class,
// Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class, // we just tack on the extra properties.
// we just tack on the extra properties. Object.defineProperties(result_map, {
Object.defineProperties(result_map, { toString: {
toString: { enumerable: false,
enumerable: false, value: function toString() {
value: function toString() { return JSON.stringify(this);
return JSON.stringify(this); }
} },
}, toUrl: {
toUrl: { enumerable: false,
enumerable: false, value: function toUrl() {
value: function toUrl() { return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString());
return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString()); }
} }
} });
}); return result_map;
return result_map as SourceMap;
} }
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/; const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
// parse attached sourcemap in processed.code // parse attached sourcemap in processed.code
export function parse_attached_sourcemap(processed: Processed, tag_name: 'script' | 'style'): void {
const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)'; /**
const regex = * @param {Processed} processed
tag_name == 'script' * @param {'script' | 'style'} tag_name
? new RegExp('(?://' + r_in + ')|(?:/\\*' + r_in + '\\s*\\*/)$') * @returns {void}
: new RegExp('/\\*' + r_in + '\\s*\\*/$'); */
function log_warning(message) { export function parse_attached_sourcemap(processed, tag_name) {
// code_start: help to find preprocessor const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)';
const code_start = const regex = tag_name == 'script'
processed.code.length < 100 ? processed.code : processed.code.slice(0, 100) + ' [...]'; ? new RegExp('(?://' + r_in + ')|(?:/\\*' + r_in + '\\s*\\*/)$')
console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`); : new RegExp('/\\*' + r_in + '\\s*\\*/$');
}
processed.code = processed.code.replace(regex, (_, match1, match2) => { /**
const map_url = tag_name == 'script' ? match1 || match2 : match1; * @param {any} message
const map_data = (map_url.match(regex_data_uri) || [])[1]; */
if (map_data) { function log_warning(message) {
// sourceMappingURL is data URL // code_start: help to find preprocessor
if (processed.map) { const code_start = processed.code.length < 100 ? processed.code : processed.code.slice(0, 100) + ' [...]';
log_warning( console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`);
'Not implemented. ' + }
'Found sourcemap in both processed.code and processed.map. ' + processed.code = processed.code.replace(regex, (_, match1, match2) => {
'Please update your preprocessor to return only one sourcemap.' const map_url = tag_name == 'script' ? match1 || match2 : match1;
); const map_data = (map_url.match(regex_data_uri) || [])[1];
// ignore attached sourcemap if (map_data) {
return ''; // sourceMappingURL is data URL
} if (processed.map) {
processed.map = b64dec(map_data); // use attached sourcemap log_warning('Not implemented. ' +
return ''; // remove from processed.code 'Found sourcemap in both processed.code and processed.map. ' +
} 'Please update your preprocessor to return only one sourcemap.');
// sourceMappingURL is path or URL // ignore attached sourcemap
if (!processed.map) { return '';
log_warning( }
`Found sourcemap path ${JSON.stringify( processed.map = b64dec(map_data); // use attached sourcemap
map_url return ''; // remove from processed.code
)} in processed.code, but no sourcemap data. ` + }
'Please update your preprocessor to return sourcemap data directly.' // sourceMappingURL is path or URL
); if (!processed.map) {
} log_warning(`Found sourcemap path ${JSON.stringify(map_url)} in processed.code, but no sourcemap data. ` +
// ignore sourcemap path 'Please update your preprocessor to return sourcemap data directly.');
return ''; // remove from processed.code }
}); // ignore sourcemap path
return ''; // remove from processed.code
});
} }
/**
* @typedef {{
* line: number;
* column: number;
* }} SourceLocation
*/

Loading…
Cancel
Save