Skip to content

Commit ee53bfc

Browse files
committed
[js] Support source maps when writing code to file
Support reading them back in Support #line 67 foo in Perl 6 code.
1 parent 0b24131 commit ee53bfc

File tree

6 files changed

+116
-11
lines changed

6 files changed

+116
-11
lines changed

src/vm/js/Chunk.nqp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ class Chunk {
6868
}
6969

7070
method collect_with_source_map_info($offset, @strs, @mapping) {
71-
if nqp::defined($!node) && $!node.node {
71+
# HACK sometimes a QAST::Op sneaks into $!node.node, so call nqp::can
72+
if nqp::defined($!node) && $!node.node && nqp::can($!node.node, 'from') {
7273
nqp::push_i(@mapping, $!node.node.from());
7374
nqp::push_i(@mapping, $offset);
7475
}

src/vm/js/HLL/Backend.nqp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,12 @@ my class JSWithSourceMap {
7373
has $!mapping;
7474
has $!p6-source;
7575
has $!file;
76+
has $!comp_line_directives;
7677
method js() {$!js}
7778
method mapping() {$!mapping}
7879
method p6-source() {$!p6-source}
7980
method file() {$!file}
81+
method comp_line_directives() {$!comp_line_directives}
8082
}
8183

8284
class QASTWithMatch {
@@ -221,14 +223,35 @@ class JavaScriptBackend {
221223
my $file := nqp::ifnull(nqp::getlexdyn('$?FILES'), "<unknown file>");
222224
$backend.emit_with_source_map($parsed.ast, @js, @mapping, :$instant, :$shebang, :$nqp-runtime);
223225

224-
JSWithSourceMap.new(js => nqp::join('', @js), mapping => @mapping, p6-source => $parsed.match.orig, file => $file);
226+
my $sourcemap_and_js := JSWithSourceMap.new(js => nqp::join('', @js), mapping => @mapping, p6-source => $parsed.match.orig, file => $file, comp_line_directives => @*comp_line_directives);
227+
228+
%adverbs<output> ?? self.sourcemap_to_file($sourcemap_and_js, %adverbs<output>) !! $sourcemap_and_js;
225229
} else {
226230
my $code := $backend.emit(self.get_ast($parsed), :$instant, :$substagestats, :$shebang, :$nqp-runtime);
227231
$code := self.beautify($code) if %adverbs<beautify>;
228232
$code;
229233
}
230234
}
231235

236+
method sourcemap_to_file($sourcemap_and_js, $output) {
237+
my str $sourcemap_file := "$output.map";
238+
239+
my $sourcemap := nqp::getcomp('JavaScript').eval('nqp.buildSourceMap')(
240+
$sourcemap_and_js.js,
241+
$sourcemap_and_js.p6-source,
242+
$sourcemap_and_js.mapping,
243+
$output,
244+
$sourcemap_and_js.file,
245+
$sourcemap_and_js.comp_line_directives,
246+
$sourcemap_file
247+
);
248+
249+
spurt($sourcemap_file, $sourcemap<content>);
250+
251+
252+
$sourcemap_and_js.js ~ "//# sourceMappingURL={$sourcemap<url>}";
253+
}
254+
232255
method beautify($code) {
233256
my $tmp_file := self.tmp_file();
234257

@@ -252,11 +275,10 @@ class JavaScriptBackend {
252275
}
253276

254277
method run($js, *%adverbs) {
255-
# TODO source map support
256278

257279
if !self.spawn_new_node {
258280
if nqp::istype($js, JSWithSourceMap) {
259-
return nqp::getcomp('JavaScript').eval($js.js, :mapping($js.mapping), :p6-source($js.p6-source), :file($js.file));
281+
return nqp::getcomp('JavaScript').eval($js.js, :mapping($js.mapping), :p6-source($js.p6-source), :file($js.file), :comp_line_directives($js.comp_line_directives));
260282
} else {
261283
return nqp::getcomp('JavaScript').eval($js);
262284
}
@@ -269,6 +291,7 @@ class JavaScriptBackend {
269291
close($code);
270292

271293
sub (*@args) {
294+
# TODO source map support
272295
my @cmd := ["node",$tmp_file];
273296

274297
my $i := 1;

src/vm/js/nqp-runtime/core.js

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const unicodeCollationAlgorithm = require('unicode-collation-algorithm');
5757

5858
const unicodeData = require('nqp-unicode-data');
5959

60+
const resolveSourceMap = require('./resolve-sourcemap.js');
61+
62+
const path = require('path');
63+
6064
exports.CodeRef = CodeRef;
6165

6266
op.isinvokable = function(obj) {
@@ -646,30 +650,74 @@ const SourceNode = require('source-map').SourceNode;
646650

647651
const charProps = require('char-props');
648652

649-
function createSourceMap(js, p6, mapping, jsFile, p6File) {
653+
function applyLineDirectives(file, line, directives) {
654+
if (directives) {
655+
for (let i = directives.length - 1; i >= 0; i--) {
656+
if (line > directives[i].originalLine) {
657+
return {
658+
line: line - directives[i].originalLine + directives[i].line - 1,
659+
file: directives[i].file
660+
};
661+
}
662+
}
663+
}
664+
return {file: file, line: line};
665+
}
666+
667+
function processCompLineDirectives(ctx, compLineDirectives) {
668+
if (compLineDirectives === undefined) return undefined;
669+
return compLineDirectives.array.map(directive => ({
670+
originalLine: nqp.toInt(directive.array[0], ctx),
671+
file: nqp.toStr(directive.array[2], ctx),
672+
line: nqp.toInt(directive.array[1], ctx)
673+
}));
674+
}
675+
676+
function createSourceMap(js, p6, mapping, jsFile, p6File, lineDirectives) {
650677
const generator = new SourceMapGenerator({file: jsFile});
651678

652679
const jsProps = charProps(js);
653680
const p6Props = charProps(p6);
654681

655-
656682
for (let i=0; i < mapping.length; i += 2) {
683+
const {file: generatedFile, line: generatedLine} = applyLineDirectives(p6File, p6Props.lineAt(mapping[i])+1, lineDirectives);
657684
generator.addMapping({
658685
generated: {
659686
line: jsProps.lineAt(mapping[i+1])+1,
660687
column: jsProps.columnAt(mapping[i+1])+1,
661688
},
662689
original: {
663-
line: p6Props.lineAt(mapping[i])+1,
690+
line: generatedLine,
664691
column: p6Props.columnAt(mapping[i])+1,
665692
},
666-
source: p6File,
693+
source: generatedFile,
667694
});
668695
}
669696

670-
return new SourceMapConsumer(generator.toString());
697+
return generator.toString();
671698
}
672699

700+
701+
class BuildSourceMap extends NQPObject {
702+
$$call(ctx, NAMED, js, p6, mapping, jsFile, p6File, compLineDirectives, sourcemapFile) {
703+
js = nqp.arg_s(ctx, js);
704+
p6 = nqp.arg_s(ctx, p6);
705+
jsFile = nqp.arg_s(ctx, jsFile);
706+
mapping = mapping.array;
707+
p6File = nqp.arg_s(ctx, p6File)
708+
compLineDirectives = processCompLineDirectives(ctx, compLineDirectives);
709+
sourcemapFile = nqp.arg_s(ctx, sourcemapFile);
710+
711+
const ret = new Hash();
712+
ret.content.set('content', new NQPStr(createSourceMap(js, p6, mapping, path.relative(path.dirname(sourcemapFile), jsFile), p6File, compLineDirectives)));
713+
ret.content.set('url', new NQPStr(path.relative(path.dirname(jsFile), sourcemapFile)));
714+
return ret;
715+
}
716+
}
717+
718+
exports.buildSourceMap = new BuildSourceMap();
719+
720+
673721
class JavaScriptCompiler extends NQPObject {
674722
eval(ctx, _NAMED, self, code) {
675723
if (!(_NAMED !== null && _NAMED.hasOwnProperty('mapping'))) {
@@ -686,7 +734,15 @@ class JavaScriptCompiler extends NQPObject {
686734
global.nqp = nqp;
687735

688736
if (_NAMED !== null && _NAMED.hasOwnProperty('mapping')) {
689-
sourceMaps[fakeFilename] = createSourceMap(codeStr, nqp.toStr(_NAMED['p6-source'], ctx), _NAMED.mapping.array, fakeFilename, nqp.toStr(_NAMED.file, ctx));
737+
sourceMaps[fakeFilename] = new SourceMapConsumer(
738+
createSourceMap(
739+
codeStr,
740+
nqp.toStr(_NAMED['p6-source'], ctx),
741+
_NAMED.mapping.array,
742+
fakeFilename,
743+
nqp.toStr(_NAMED.file, ctx),
744+
processCompLineDirectives(ctx, _NAMED.comp_line_directives)));
745+
690746
const node = SourceNode.fromStringWithSourceMap(codeStr, sourceMaps[fakeFilename]);
691747

692748
// HACK
@@ -1366,6 +1422,18 @@ function backtrace(exception) {
13661422
line = original.line;
13671423
column = original.column;
13681424
}
1425+
} else {
1426+
if (file) {
1427+
const resolved = resolveSourceMap(file);
1428+
if (resolved !== null) {
1429+
const original = resolved.originalPositionFor({line: line, column: column});
1430+
if (original.source) {
1431+
file = original.source;
1432+
line = original.line;
1433+
column = original.column;
1434+
}
1435+
}
1436+
}
13691437
}
13701438
if (file === undefined) file = '?';
13711439
break;

src/vm/js/nqp-runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"shortid": "2.2.8",
3131
"sleep": "^5.1.1",
3232
"source-map": "0.5.7",
33+
"source-map-resolve": "^0.5.1",
3334
"stack-trace": "0.0.10",
3435
"unicharadata": "*",
3536
"unicode-collation-algorithm": "0.0.2",
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1-
21
const fs = require('fs');
32
const sourceMapResolve = require('source-map-resolve');
3+
4+
const SourceMapConsumer = require('source-map').SourceMapConsumer;
5+
6+
const cache = new Map();
7+
module.exports = function(filename) {
8+
if (cache.get(filename) !== undefined) return cache.get(filename);
9+
const got = sourceMapResolve.resolveSourceMapSync(fs.readFileSync(filename).toString('utf8'), filename, fs.readFileSync);
10+
const ret = got !== null ? new SourceMapConsumer(got.map) : got;
11+
cache.set(filename, ret);
12+
return ret;
13+
};

src/vm/js/nqp-runtime/runtime.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,5 @@ const props = require('./unicode-props.js');
672672
for (const prop in props) {
673673
exports[prop] = props[prop];
674674
}
675+
676+
exports.buildSourceMap = core.buildSourceMap;

0 commit comments

Comments
 (0)