Browse Source

Merge pull request #391 from rollup/transform-bundle-hook-compose-sourcemaps

Compose sourcemaps from bundle transformers
gh-438-b
Rich Harris 9 years ago
parent
commit
b7994cab34
  1. 3
      package.json
  2. 23
      src/Bundle.js
  3. 134
      src/utils/collapseSourcemaps.js
  4. 19
      src/utils/transformBundle.js
  5. 17
      test/form/transform-bundle-plugin/_config.js
  6. 2
      test/form/transform-bundle-plugin/_expected/amd.js
  7. 2
      test/form/transform-bundle-plugin/_expected/cjs.js
  8. 2
      test/form/transform-bundle-plugin/_expected/es6.js
  9. 2
      test/form/transform-bundle-plugin/_expected/iife.js
  10. 2
      test/form/transform-bundle-plugin/_expected/umd.js
  11. 1
      test/form/transform-bundle-plugin/main.js
  12. 58
      test/sourcemaps/names-transformed/_config.js
  13. 4
      test/sourcemaps/names-transformed/a.js
  14. 4
      test/sourcemaps/names-transformed/b.js
  15. 5
      test/sourcemaps/names-transformed/main.js
  16. 38
      test/sourcemaps/transform-bundle/_config.js
  17. 1
      test/sourcemaps/transform-bundle/main.js

3
package.json

@ -57,7 +57,8 @@
"rollup-plugin-replace": "^1.0.1",
"sander": "^0.4.0",
"source-map": "^0.5.3",
"sourcemap-codec": "^1.2.1"
"sourcemap-codec": "^1.2.1",
"uglify-js": "^2.6.1"
},
"dependencies": {
"chalk": "^1.1.1",

23
src/Bundle.js

@ -11,7 +11,9 @@ import getExportMode from './utils/getExportMode.js';
import getIndentString from './utils/getIndentString.js';
import { unixizePath } from './utils/normalizePlatform.js';
import transform from './utils/transform.js';
import transformBundle from './utils/transformBundle.js';
import collapseSourcemaps from './utils/collapseSourcemaps.js';
import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
import callIfFunction from './utils/callIfFunction.js';
import { isRelative } from './utils/path.js';
@ -46,6 +48,10 @@ export default class Bundle {
.map( plugin => plugin.transform )
.filter( Boolean );
this.bundleTransformers = this.plugins
.map( plugin => plugin.transformBundle )
.filter( Boolean );
this.moduleById = blank();
this.modules = [];
@ -242,18 +248,21 @@ export default class Bundle {
if ( banner ) magicString.prepend( banner + '\n' );
if ( footer ) magicString.append( '\n' + footer );
const code = magicString.toString();
let code = magicString.toString();
let map = null;
let bundleSourcemapChain = [];
code = transformBundle( code, this.bundleTransformers, bundleSourcemapChain )
.replace( new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' ), '' );
if ( options.sourceMap ) {
const file = options.sourceMapFile || options.dest;
map = magicString.generateMap({
includeContent: true,
file
// TODO
});
map = magicString.generateMap({ file, includeContent: true });
if ( this.transformers.length || this.bundleTransformers.length ) {
map = collapseSourcemaps( map, usedModules, bundleSourcemapChain );
}
if ( this.transformers.length ) map = collapseSourcemaps( map, usedModules );
map.sources = map.sources.map( unixizePath );
}

134
src/utils/collapseSourcemaps.js

@ -1,72 +1,108 @@
import { encode, decode } from 'sourcemap-codec';
function traceSegment ( loc, mappings ) {
const line = loc[0];
const column = loc[1];
function Source ( map, sources ) {
if ( !map ) throw new Error( 'Cannot generate a sourcemap if non-sourcemap-generating transformers are used' );
const segments = mappings[ line ];
this.sources = sources;
this.names = map.names;
this.mappings = decode( map.mappings );
}
if ( !segments ) return null;
Source.prototype = { // TODO bring into line with others post-https://github.com/rollup/rollup/pull/386
traceMappings () {
let names = [];
const mappings = this.mappings.map( line => {
let tracedLine = [];
line.forEach( segment => {
const source = this.sources[ segment[1] ];
const traced = source.traceSegment( segment[2], segment[3], this.names[ segment[4] ] );
if ( traced ) {
let nameIndex = null;
segment = [
segment[0],
traced.index,
traced.line,
traced.column
];
if ( traced.name ) {
nameIndex = names.indexOf( traced.name );
if ( nameIndex === -1 ) {
nameIndex = names.length;
names.push( traced.name );
}
segment[4] = nameIndex;
}
tracedLine.push( segment );
}
});
return tracedLine;
});
for ( let i = 0; i < segments.length; i += 1 ) {
const segment = segments[i];
return { names, mappings };
},
if ( segment[0] > column ) return null;
traceSegment ( line, column, name ) {
const segments = this.mappings[ line ];
if ( segment[0] === column ) {
if ( segment[1] !== 0 ) {
throw new Error( 'Bad sourcemap' );
}
return [ segment[2], segment[3] ];
}
}
if ( !segments ) return null;
return null;
}
for ( let i = 0; i < segments.length; i += 1 ) {
const segment = segments[i];
export default function collapseSourcemaps ( map, modules ) {
const chains = modules.map( module => {
return module.sourceMapChain.map( map => {
if ( !map ) throw new Error( 'Cannot generate a sourcemap if non-sourcemap-generating transformers are used' );
return decode( map.mappings );
});
});
if ( segment[0] > column ) return null;
const decodedMappings = decode( map.mappings );
if ( segment[0] === column ) {
const source = this.sources[ segment[1] ];
const tracedMappings = decodedMappings.map( line => {
let tracedLine = [];
if ( !source ) throw new Error( 'Bad sourcemap' );
line.forEach( segment => {
const sourceIndex = segment[1];
const sourceCodeLine = segment[2];
const sourceCodeColumn = segment[3];
if ( source.isOriginal ) {
return {
index: source.index,
line: segment[2],
column: segment[3],
name: this.names[ segment[4] ] || name
};
}
const chain = chains[ sourceIndex ];
return source.traceSegment( segment[2], segment[3], name );
}
}
let i = chain.length;
let traced = [ sourceCodeLine, sourceCodeColumn ];
return null;
}
};
while ( i-- && traced ) {
traced = traceSegment( traced, chain[i] );
}
export default function collapseSourcemaps ( map, modules, bundleSourcemapChain ) {
const sources = modules.map( ( module, i ) => {
let source = { isOriginal: true, index: i };
if ( traced ) {
tracedLine.push([
segment[0],
segment[1],
traced[0],
traced[1]
// TODO name?
]);
}
module.sourceMapChain.forEach( map => {
source = new Source( map, [ source ]);
});
return tracedLine;
return source;
});
let source = new Source( map, sources );
bundleSourcemapChain.forEach( map => {
source = new Source( map, [ source ] );
});
const { names, mappings } = source.traceMappings();
// we re-use the `map` object because it has convenient toString/toURL methods
map.sourcesContent = modules.map( module => module.originalCode );
map.mappings = encode( tracedMappings );
map.mappings = encode( mappings );
map.names = names;
return map;
}

19
src/utils/transformBundle.js

@ -0,0 +1,19 @@
export default function transformBundle ( code, transformers, sourceMapChain ) {
return transformers.reduce( ( code, transformer ) => {
let result = transformer( code );
if ( result == null ) return code;
if ( typeof result === 'string' ) {
result = {
code: result,
map: null
};
}
const map = typeof result.map === 'string' ? JSON.parse( result.map ) : map;
sourceMapChain.push( map );
return result.code;
}, code );
}

17
test/form/transform-bundle-plugin/_config.js

@ -0,0 +1,17 @@
module.exports = {
description: 'allows plugins to transform bundle',
options: {
plugins: [
{
transformBundle: function (code) {
return '/* first plugin */';
}
},
{
transformBundle: function (code) {
return code + '\n/* second plugin */';
}
}
]
}
}

2
test/form/transform-bundle-plugin/_expected/amd.js

@ -0,0 +1,2 @@
/* first plugin */
/* second plugin */

2
test/form/transform-bundle-plugin/_expected/cjs.js

@ -0,0 +1,2 @@
/* first plugin */
/* second plugin */

2
test/form/transform-bundle-plugin/_expected/es6.js

@ -0,0 +1,2 @@
/* first plugin */
/* second plugin */

2
test/form/transform-bundle-plugin/_expected/iife.js

@ -0,0 +1,2 @@
/* first plugin */
/* second plugin */

2
test/form/transform-bundle-plugin/_expected/umd.js

@ -0,0 +1,2 @@
/* first plugin */
/* second plugin */

1
test/form/transform-bundle-plugin/main.js

@ -0,0 +1 @@
console.log( 1 + 1 );

58
test/sourcemaps/names-transformed/_config.js

@ -0,0 +1,58 @@
var assert = require( 'assert' );
var uglify = require( 'uglify-js' );
var MagicString = require( 'magic-string' );
var getLocation = require( '../../utils/getLocation' );
var SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;
module.exports = {
description: 'names are recovered if transforms are used',
options: {
plugins: [
{
transform: function ( code ) {
var s = new MagicString( code );
var pattern = /mangleMe/g;
var match;
while ( match = pattern.exec( code ) ) {
s.overwrite( match.index, match.index + match[0].length, 'mangleMePlease', true );
}
return {
code: s.toString(),
map: s.generateMap({ hires: true })
};
},
transformBundle: function ( code ) {
return uglify.minify( code, {
fromString: true,
outSourceMap: 'x'
});
}
}
]
},
test: function ( code, map ) {
var smc = new SourceMapConsumer( map );
var generatedLoc = getLocation( code, /\w+=1/.exec( code ).index );
var originalLoc = smc.originalPositionFor( generatedLoc );
assert.deepEqual( originalLoc, {
source: '../a.js',
line: 1,
column: 4,
name: 'mangleMe'
});
generatedLoc = getLocation( code, /\w+=2/.exec( code ).index );
originalLoc = smc.originalPositionFor( generatedLoc );
assert.deepEqual( originalLoc, {
source: '../b.js',
line: 1,
column: 4,
name: 'mangleMe'
});
}
};

4
test/sourcemaps/names-transformed/a.js

@ -0,0 +1,4 @@
var mangleMe = 1;
export default function () {
assert.equal( mangleMe, 1 );
}

4
test/sourcemaps/names-transformed/b.js

@ -0,0 +1,4 @@
var mangleMe = 2;
export default function () {
assert.equal( mangleMe, 2 );
}

5
test/sourcemaps/names-transformed/main.js

@ -0,0 +1,5 @@
import a from './a.js';
import b from './b.js';
a();
b();

38
test/sourcemaps/transform-bundle/_config.js

@ -0,0 +1,38 @@
var uglify = require( 'uglify-js' );
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: [
{
transformBundle: function ( code ) {
var options = {
fromString: true,
outSourceMap: 'x' // trigger sourcemap generation
};
return uglify.minify( code, options );
}
}
]
},
test: function ( code, map ) {
var smc = new SourceMapConsumer( map );
var generatedLoc = getLocation( code, code.indexOf( '42' ) );
var originalLoc = smc.originalPositionFor( generatedLoc );
assert.ok( /main/.test( originalLoc.source ) );
assert.equal( originalLoc.line, 1 );
assert.equal( originalLoc.column, 13 );
generatedLoc = getLocation( code, code.indexOf( 'log' ) );
originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.line, 1 );
assert.equal( originalLoc.column, 8 );
}
};

1
test/sourcemaps/transform-bundle/main.js

@ -0,0 +1 @@
console.log( 42 );
Loading…
Cancel
Save