diff --git a/src/Bundle.js b/src/Bundle.js index 3d4414a..d0709fa 100644 --- a/src/Bundle.js +++ b/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 ); diff --git a/src/Module.js b/src/Module.js index 7bc89d3..2cb9a61 100644 --- a/src/Module.js +++ b/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; diff --git a/src/utils/collapseSourcemaps.js b/src/utils/collapseSourcemaps.js index ff86eaf..68a97e0 100644 --- a/src/utils/collapseSourcemaps.js +++ b/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; } diff --git a/src/utils/transform.js b/src/utils/transform.js index 62b47e2..285eea0 100644 --- a/src/utils/transform.js +++ b/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}`; diff --git a/src/utils/transformBundle.js b/src/utils/transformBundle.js index f82bfb6..56a4359 100644 --- a/src/utils/transformBundle.js +++ b/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; diff --git a/test/sourcemaps/loaders/_config.js b/test/sourcemaps/loaders/_config.js new file mode 100644 index 0000000..8665133 --- /dev/null +++ b/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 ); + } +}; diff --git a/test/sourcemaps/loaders/bar.js b/test/sourcemaps/loaders/bar.js new file mode 100644 index 0000000..6c3e77f --- /dev/null +++ b/test/sourcemaps/loaders/bar.js @@ -0,0 +1 @@ +/*misalign*/export const foo = () => 20; diff --git a/test/sourcemaps/loaders/foo.js b/test/sourcemaps/loaders/foo.js new file mode 100644 index 0000000..9ce4283 --- /dev/null +++ b/test/sourcemaps/loaders/foo.js @@ -0,0 +1 @@ +/*the*/export const bar = () => 22; diff --git a/test/sourcemaps/loaders/main.js b/test/sourcemaps/loaders/main.js new file mode 100644 index 0000000..df7e27a --- /dev/null +++ b/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()}` );