Browse Source

Merge pull request #715 from Permutatrix/plugin-load-sourcemap

Make it possible for plugin loaders to provide sourcemaps.
ghi-672
Rich Harris 9 years ago
committed by GitHub
parent
commit
d3c2ee6e9c
  1. 24
      src/Bundle.js
  2. 3
      src/Module.js
  3. 75
      src/utils/collapseSourcemaps.js
  4. 4
      src/utils/transform.js
  5. 2
      src/utils/transformBundle.js
  6. 63
      test/sourcemaps/loaders/_config.js
  7. 1
      test/sourcemaps/loaders/bar.js
  8. 1
      test/sourcemaps/loaders/foo.js
  9. 4
      test/sourcemaps/loaders/main.js

24
src/Bundle.js

@ -46,12 +46,11 @@ export default class Bundle {
.concat( resolveId )
);
this.load = first(
this.plugins
.map( plugin => plugin.load )
.filter( Boolean )
.concat( load )
);
const loaders = this.plugins
.map( plugin => plugin.load )
.filter( Boolean );
this.hasLoaders = loaders.length !== 0;
this.load = first( loaders.concat( load ) );
this.transformers = this.plugins
.map( plugin => plugin.transform )
@ -204,9 +203,9 @@ export default class Bundle {
return transform( source, id, this.transformers );
})
.then( source => {
const { code, originalCode, ast, sourceMapChain } = source;
const { code, originalCode, originalSourceMap, ast, sourceMapChain } = source;
const module = new Module({ id, code, originalCode, ast, sourceMapChain, bundle: this });
const module = new Module({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, bundle: this });
this.modules.push( module );
this.moduleById.set( id, module );
@ -337,10 +336,11 @@ export default class Bundle {
let file = options.sourceMapFile || options.dest;
if ( file ) file = resolve( typeof process !== 'undefined' ? process.cwd() : '', file );
map = magicString.generateMap({ file, includeContent: true });
if ( this.transformers.length || this.bundleTransformers.length ) {
map = collapseSourcemaps( map, usedModules, bundleSourcemapChain );
if ( this.hasLoaders || this.transformers.length || this.bundleTransformers.length ) {
map = magicString.generateMap( {} );
map = collapseSourcemaps( file, map, usedModules, bundleSourcemapChain );
} else {
map = magicString.generateMap({ file, includeContent: true });
}
map.sources = map.sources.map( unixizePath );

3
src/Module.js

@ -17,9 +17,10 @@ import { emptyBlockStatement } from './ast/create.js';
import extractNames from './ast/extractNames.js';
export default class Module {
constructor ({ id, code, originalCode, ast, sourceMapChain, bundle }) {
constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, bundle }) {
this.code = code;
this.originalCode = originalCode;
this.originalSourceMap = originalSourceMap;
this.sourceMapChain = sourceMapChain;
this.bundle = bundle;

75
src/utils/collapseSourcemaps.js

@ -1,13 +1,15 @@
import { encode, decode } from 'sourcemap-codec';
import { dirname, relative, resolve } from './path.js';
class Source {
constructor ( index ) {
constructor ( filename, content ) {
this.isOriginal = true;
this.index = index;
this.filename = filename;
this.content = content;
}
traceSegment ( line, column, name ) {
return { line, column, name, index: this.index };
return { line, column, name, source: this };
}
}
@ -21,7 +23,7 @@ class Link {
}
traceMappings () {
let names = [];
let sources = [], sourcesContent = [], names = [];
const mappings = this.mappings.map( line => {
let tracedLine = [];
@ -31,14 +33,28 @@ class Link {
const traced = source.traceSegment( segment[2], segment[3], this.names[ segment[4] ] );
if ( traced ) {
let nameIndex = null;
let sourceIndex = null, nameIndex = null;
segment = [
segment[0],
traced.index,
null,
traced.line,
traced.column
];
// newer sources are more likely to be used, so search backwards.
sourceIndex = sources.lastIndexOf( traced.source.filename );
if ( sourceIndex === -1 ) {
sourceIndex = sources.length;
sources.push( traced.source.filename );
sourcesContent[ sourceIndex ] = traced.source.content;
} else if ( sourcesContent[ sourceIndex ] == null ) {
sourcesContent[ sourceIndex ] = traced.source.content;
} else if ( traced.source.content != null && sourcesContent[ sourceIndex ] !== traced.source.content ) {
throw new Error( `Multiple conflicting contents for sourcemap source ${source.filename}` );
}
segment[1] = sourceIndex;
if ( traced.name ) {
nameIndex = names.indexOf( traced.name );
if ( nameIndex === -1 ) {
@ -56,7 +72,7 @@ class Link {
return tracedLine;
});
return { names, mappings };
return { sources, sourcesContent, names, mappings };
}
traceSegment ( line, column, name ) {
@ -81,29 +97,58 @@ class Link {
}
}
export default function collapseSourcemaps ( map, modules, bundleSourcemapChain ) {
const sources = modules.map( ( module, i ) => {
let source = new Source( i );
export default function collapseSourcemaps ( file, map, modules, bundleSourcemapChain ) {
const moduleSources = modules.map( module => {
let sourceMapChain = module.sourceMapChain;
let source;
if ( module.originalSourceMap == null ) {
source = new Source( module.id, module.originalCode );
} else {
const sources = module.originalSourceMap.sources;
const sourcesContent = module.originalSourceMap.sourcesContent || [];
if ( sources == null || ( sources.length <= 1 && sources[0] == null ) ) {
source = new Source( module.id, sourcesContent[0] );
sourceMapChain = [ module.originalSourceMap ].concat( sourceMapChain );
} else {
// TODO indiscriminately treating IDs and sources as normal paths is probably bad.
const directory = dirname( module.id ) || '.';
const sourceRoot = module.originalSourceMap.sourceRoot || '.';
const baseSources = sources.map( (source, i) => {
return new Source( resolve( directory, sourceRoot, source ), sourcesContent[i] );
});
source = new Link( module.originalSourceMap, baseSources );
}
}
module.sourceMapChain.forEach( map => {
sourceMapChain.forEach( map => {
source = new Link( map, [ source ]);
});
return source;
});
let source = new Link( map, sources );
let source = new Link( map, moduleSources );
bundleSourcemapChain.forEach( map => {
source = new Link( map, [ source ] );
});
const { names, mappings } = source.traceMappings();
let { sources, sourcesContent, names, mappings } = source.traceMappings();
if ( file ) {
const directory = dirname( file );
sources = sources.map( source => relative( directory, source ) );
}
// we re-use the `map` object because it has convenient toString/toURL methods
map.sourcesContent = modules.map( module => module.originalCode );
map.mappings = encode( mappings );
map.sources = sources;
map.sourcesContent = sourcesContent;
map.names = names;
map.mappings = encode( mappings );
return map;
}

4
src/utils/transform.js

@ -1,6 +1,8 @@
export default function transform ( source, id, transformers ) {
let sourceMapChain = [];
const originalSourceMap = typeof source.map === 'string' ? JSON.parse( source.map ) : source.map;
let originalCode = source.code;
let ast = source.ast;
@ -30,7 +32,7 @@ export default function transform ( source, id, transformers ) {
}, Promise.resolve( source.code ) )
.then( code => ({ code, originalCode, ast, sourceMapChain }) )
.then( code => ({ code, originalCode, originalSourceMap, ast, sourceMapChain }) )
.catch( err => {
err.id = id;
err.message = `Error loading ${id}: ${err.message}`;

2
src/utils/transformBundle.js

@ -11,7 +11,7 @@ export default function transformBundle ( code, transformers, sourceMapChain ) {
};
}
const map = typeof result.map === 'string' ? JSON.parse( result.map ) : map;
const map = typeof result.map === 'string' ? JSON.parse( result.map ) : result.map;
sourceMapChain.push( map );
return result.code;

63
test/sourcemaps/loaders/_config.js

@ -0,0 +1,63 @@
var babel = require( 'babel-core' );
var fs = require( 'fs' );
var assert = require( 'assert' );
var getLocation = require( '../../utils/getLocation' );
var SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;
module.exports = {
description: 'preserves sourcemap chains when transforming',
options: {
plugins: [
{
load: function ( id ) {
if ( /foo.js$/.test( id ) ) {
id = id.replace( /foo.js$/, 'bar.js' );
} else if ( /bar.js$/.test( id ) ) {
id = id.replace( /bar.js$/, 'foo.js' );
}
var out = babel.transformFileSync( id, {
blacklist: [ 'es6.modules' ],
sourceMap: true,
comments: false // misalign the columns
});
if ( /main.js$/.test( id ) ) {
delete out.map.sources;
} else {
const slash = out.map.sources[0].lastIndexOf( '/' ) + 1;
out.map.sources = out.map.sources.map( source => '../' + source.slice( slash ) );
out.map.sourceRoot = 'fake';
}
return { code: out.code, map: out.map };
}
}
]
},
test: function ( code, map ) {
var smc = new SourceMapConsumer( map );
var generatedLoc = getLocation( code, code.indexOf( '22' ) );
var originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.source, '../foo.js' );
assert.equal( originalLoc.line, 1 );
assert.equal( originalLoc.column, 32 );
var generatedLoc = getLocation( code, code.indexOf( '20' ) );
var originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.source, '../bar.js' );
assert.equal( originalLoc.line, 1 );
assert.equal( originalLoc.column, 37 );
generatedLoc = getLocation( code, code.indexOf( 'log' ) );
originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.source, '../main.js' );
assert.ok( /columns/.test( smc.sourceContentFor( '../main.js' ) ) );
assert.equal( originalLoc.line, 4 );
assert.equal( originalLoc.column, 19 );
}
};

1
test/sourcemaps/loaders/bar.js

@ -0,0 +1 @@
/*misalign*/export const foo = () => 20;

1
test/sourcemaps/loaders/foo.js

@ -0,0 +1 @@
/*the*/export const bar = () => 22;

4
test/sourcemaps/loaders/main.js

@ -0,0 +1,4 @@
import { foo } from './foo';
import { bar } from './bar';
/*columns*/console.log( `the answer is ${foo() + bar()}` );
Loading…
Cancel
Save