diff --git a/src/Bundle.js b/src/Bundle.js index 0f3786a..8a11683 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -1,9 +1,10 @@ import MagicString from './MagicString.js'; import SourceMap from './utils/SourceMap.js'; -import getSemis from './utils/getSemis.js'; import getRelativePath from './utils/getRelativePath.js'; import hasOwnProp from './utils/hasOwnProp.js'; import isObject from './utils/isObject.js'; +import getLocator from './utils/getLocator.js'; +import Mappings from './utils/Mappings.js'; export default function Bundle ( options = {} ) { this.intro = options.intro || ''; @@ -87,6 +88,50 @@ Bundle.prototype = { }); }); + const mappings = new Mappings( options.hires ); + + if ( this.intro ) { + mappings.advance( this.intro ); + } + + this.sources.forEach( ( source, i ) => { + if ( i > 0 ) { + mappings.advance( this.separator ); + } + + const sourceIndex = source.filename ? this.uniqueSourceIndexByFilename[ source.filename ] : -1; + const magicString = source.content; + const locate = getLocator( magicString.original ); + + if ( magicString.intro ) { + mappings.advance( magicString.intro ); + } + + magicString.firstChunk.eachNext( chunk => { + const loc = locate( chunk.start ); + + if ( chunk.intro.length ) mappings.advance( chunk.intro ); + + if ( source.filename ) { + if ( chunk.edited ) { + mappings.addEdit( sourceIndex, chunk.content, chunk.original, loc, chunk.storeName ? names.indexOf( chunk.original ) : -1 ); + } else { + mappings.addUneditedChunk( sourceIndex, chunk, magicString.original, loc, magicString.sourcemapLocations ); + } + } + + else { + mappings.advance( chunk.content ); + } + + if ( chunk.outro.length ) mappings.advance( chunk.outro ); + }); + + if ( magicString.outro ) { + mappings.advance( magicString.outro ); + } + }); + return new SourceMap({ file: ( options.file ? options.file.split( /[\/\\]/ ).pop() : null ), sources: this.uniqueSources.map( source => { @@ -96,32 +141,10 @@ Bundle.prototype = { return options.includeContent ? source.content : null; }), names, - mappings: this.getMappings( options, names ) + mappings: mappings.encode() }); }, - getMappings ( options, names ) { - const offsets = {}; - - return ( - getSemis( this.intro ) + - this.sources.map( ( source, i ) => { - const prefix = ( i > 0 ) ? ( getSemis( source.separator ) || ',' ) : ''; - let mappings; - - // we don't bother encoding sources without a filename - if ( !source.filename ) { - mappings = getSemis( source.content.toString() ); - } else { - const sourceIndex = this.uniqueSourceIndexByFilename[ source.filename ]; - mappings = source.content.getMappings( options, sourceIndex, offsets, names ); - } - - return prefix + mappings; - }).join( '' ) - ); - }, - getIndentString () { const indentStringCounts = {}; diff --git a/src/MagicString.js b/src/MagicString.js index 7fc93d1..af851f2 100644 --- a/src/MagicString.js +++ b/src/MagicString.js @@ -1,10 +1,10 @@ import Chunk from './Chunk.js'; import SourceMap from './utils/SourceMap.js'; import guessIndent from './utils/guessIndent.js'; -import encodeMappings from './utils/encodeMappings.js'; import getRelativePath from './utils/getRelativePath.js'; import isObject from './utils/isObject.js'; import getLocator from './utils/getLocator.js'; +import Mappings from './utils/Mappings.js'; import Stats from './utils/Stats.js'; const warned = { @@ -130,7 +130,29 @@ MagicString.prototype = { generateMap ( options ) { options = options || {}; + const sourceIndex = 0; const names = Object.keys( this.storedNames ); + const mappings = new Mappings( options.hires ); + + const locate = getLocator( this.original ); + + if ( this.intro ) { + mappings.advance( this.intro ); + } + + this.firstChunk.eachNext( chunk => { + const loc = locate( chunk.start ); + + if ( chunk.intro.length ) mappings.advance( chunk.intro ); + + if ( chunk.edited ) { + mappings.addEdit( sourceIndex, chunk.content, chunk.original, loc, chunk.storeName ? names.indexOf( chunk.original ) : -1 ); + } else { + mappings.addUneditedChunk( sourceIndex, chunk, this.original, loc, this.sourcemapLocations ); + } + + if ( chunk.outro.length ) mappings.advance( chunk.outro ); + }); if ( DEBUG ) this.stats.time( 'generateMap' ); const map = new SourceMap({ @@ -138,7 +160,7 @@ MagicString.prototype = { sources: [ options.source ? getRelativePath( options.file || '', options.source ) : null ], sourcesContent: options.includeContent ? [ this.original ] : [ null ], names, - mappings: this.getMappings( options, 0, {}, names ) + mappings: mappings.encode() }); if ( DEBUG ) this.stats.timeEnd( 'generateMap' ); @@ -149,10 +171,6 @@ MagicString.prototype = { return this.indentStr === null ? '\t' : this.indentStr; }, - getMappings ( options, sourceIndex, offsets, names ) { - return encodeMappings( this.original, this.intro, this.outro, this.firstChunk, options.hires, this.sourcemapLocations, sourceIndex, offsets, names ); - }, - indent ( indentStr, options ) { const pattern = /^[^\r\n]/gm; diff --git a/src/utils/Mappings.js b/src/utils/Mappings.js new file mode 100644 index 0000000..b32cf0e --- /dev/null +++ b/src/utils/Mappings.js @@ -0,0 +1,117 @@ +import { encode } from 'vlq'; + +export default function Mappings ( hires ) { + const offsets = { + generatedCodeColumn: 0, + sourceIndex: 0, + sourceCodeLine: 0, + sourceCodeColumn: 0, + sourceCodeName: 0 + }; + + let generatedCodeLine = 0; + let generatedCodeColumn = 0; + + this.raw = []; + let rawSegments = this.raw[ generatedCodeLine ] = []; + + let pending = null; + + this.addEdit = ( sourceIndex, content, original, loc, nameIndex ) => { + if ( content.length ) { + rawSegments.push([ + generatedCodeColumn, + sourceIndex, + loc.line, + loc.column, + nameIndex, + ]); + } else if ( pending ) { + rawSegments.push( pending ); + } + + this.advance( content ); + pending = null; + }; + + this.addUneditedChunk = ( sourceIndex, chunk, original, loc, sourcemapLocations ) => { + let originalCharIndex = chunk.start; + let first = true; + + while ( originalCharIndex < chunk.end ) { + if ( hires || first || sourcemapLocations[ originalCharIndex ] ) { + rawSegments.push([ + generatedCodeColumn, + sourceIndex, + loc.line, + loc.column, + -1 + ]); + } + + if ( original[ originalCharIndex ] === '\n' ) { + loc.line += 1; + loc.column = 0; + generatedCodeLine += 1; + this.raw[ generatedCodeLine ] = rawSegments = []; + generatedCodeColumn = 0; + } else { + loc.column += 1; + generatedCodeColumn += 1; + } + + originalCharIndex += 1; + first = false; + } + + pending = [ + generatedCodeColumn, + sourceIndex, + loc.line, + loc.column, + -1, + ]; + }; + + this.advance = str => { + if ( !str ) return; + + const lines = str.split( '\n' ); + const lastLine = lines.pop(); + + if ( lines.length ) { + generatedCodeLine += lines.length; + this.raw[ generatedCodeLine ] = rawSegments = []; + generatedCodeColumn = lastLine.length; + } else { + generatedCodeColumn += lastLine.length; + } + }; + + this.encode = () => { + return this.raw.map( segments => { + let generatedCodeColumn = 0; + + return segments.map( segment => { + const arr = [ + segment[0] - generatedCodeColumn, + segment[1] - offsets.sourceIndex, + segment[2] - offsets.sourceCodeLine, + segment[3] - offsets.sourceCodeColumn + ]; + + generatedCodeColumn = segment[0]; + offsets.sourceIndex = segment[1]; + offsets.sourceCodeLine = segment[2]; + offsets.sourceCodeColumn = segment[3]; + + if ( ~segment[4] ) { + arr.push( segment[4] - offsets.sourceCodeName ); + offsets.sourceCodeName = segment[4]; + } + + return encode( arr ); + }).join( ',' ); + }).join( ';' ); + }; +} diff --git a/src/utils/encodeMappings.js b/src/utils/encodeMappings.js deleted file mode 100644 index de210e0..0000000 --- a/src/utils/encodeMappings.js +++ /dev/null @@ -1,137 +0,0 @@ -import { encode } from 'vlq'; -import getSemis from './getSemis.js'; -import getLocator from './getLocator.js'; - -const nonWhitespace = /\S/; - -export default function encodeMappings ( original, intro, outro, chunk, hires, sourcemapLocations, sourceIndex, offsets, names ) { - const rawLines = []; - - let generatedCodeLine = intro.split( '\n' ).length - 1; - let rawSegments = rawLines[ generatedCodeLine ] = []; - - let generatedCodeColumn = 0; - - const locate = getLocator( original ); - - function addEdit ( content, original, loc, nameIndex, i ) { - if ( i || ( content.length && nonWhitespace.test( content ) ) ) { - rawSegments.push({ - generatedCodeLine, - generatedCodeColumn, - sourceCodeLine: loc.line, - sourceCodeColumn: loc.column, - sourceCodeName: nameIndex, - sourceIndex - }); - } - - let lines = content.split( '\n' ); - let lastLine = lines.pop(); - - if ( lines.length ) { - generatedCodeLine += lines.length; - rawLines[ generatedCodeLine ] = rawSegments = []; - generatedCodeColumn = lastLine.length; - } else { - generatedCodeColumn += lastLine.length; - } - - lines = original.split( '\n' ); - lastLine = lines.pop(); - - if ( lines.length ) { - loc.line += lines.length; - loc.column = lastLine.length; - } else { - loc.column += lastLine.length; - } - } - - function addUneditedChunk ( chunk, loc ) { - let originalCharIndex = chunk.start; - let first = true; - - while ( originalCharIndex < chunk.end ) { - if ( hires || first || sourcemapLocations[ originalCharIndex ] ) { - rawSegments.push({ - generatedCodeLine, - generatedCodeColumn, - sourceCodeLine: loc.line, - sourceCodeColumn: loc.column, - sourceCodeName: -1, - sourceIndex - }); - } - - if ( original[ originalCharIndex ] === '\n' ) { - loc.line += 1; - loc.column = 0; - generatedCodeLine += 1; - rawLines[ generatedCodeLine ] = rawSegments = []; - generatedCodeColumn = 0; - } else { - loc.column += 1; - generatedCodeColumn += 1; - } - - originalCharIndex += 1; - first = false; - } - } - - let hasContent = false; - - while ( chunk ) { - const loc = locate( chunk.start ); - - if ( chunk.intro.length ) { - addEdit( chunk.intro, '', loc, -1, hasContent ); - } - - if ( chunk.edited ) { - addEdit( chunk.content, chunk.original, loc, chunk.storeName ? names.indexOf( chunk.original ) : -1, hasContent ); - } else { - addUneditedChunk( chunk, loc ); - } - - if ( chunk.outro.length ) { - addEdit( chunk.outro, '', loc, -1, hasContent ); - } - - if ( chunk.content || chunk.intro || chunk.outro ) hasContent = true; - - const nextChunk = chunk.next; - chunk = nextChunk; - } - - offsets.sourceIndex = offsets.sourceIndex || 0; - offsets.sourceCodeLine = offsets.sourceCodeLine || 0; - offsets.sourceCodeColumn = offsets.sourceCodeColumn || 0; - offsets.sourceCodeName = offsets.sourceCodeName || 0; - - return rawLines.map( segments => { - let generatedCodeColumn = 0; - - return segments.map( segment => { - const arr = [ - segment.generatedCodeColumn - generatedCodeColumn, - segment.sourceIndex - offsets.sourceIndex, - segment.sourceCodeLine - offsets.sourceCodeLine, - segment.sourceCodeColumn - offsets.sourceCodeColumn - ]; - - generatedCodeColumn = segment.generatedCodeColumn; - offsets.sourceIndex = segment.sourceIndex; - offsets.sourceCodeLine = segment.sourceCodeLine; - offsets.sourceCodeColumn = segment.sourceCodeColumn; - - if ( ~segment.sourceCodeName ) { - arr.push( segment.sourceCodeName - offsets.sourceCodeName ); - offsets.sourceCodeName = segment.sourceCodeName; - } - - return encode( arr ); - }).join( ',' ); - }).join( ';' ) + getSemis(outro); -} diff --git a/src/utils/getSemis.js b/src/utils/getSemis.js deleted file mode 100644 index 4ead76e..0000000 --- a/src/utils/getSemis.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getSemis ( str ) { - return new Array( str.split( '\n' ).length ).join( ';' ); -} diff --git a/test/MagicString.Bundle.js b/test/MagicString.Bundle.js index 9033450..f0ce009 100644 --- a/test/MagicString.Bundle.js +++ b/test/MagicString.Bundle.js @@ -111,9 +111,6 @@ describe( 'MagicString.Bundle', () => { assert.deepEqual( map.sources, [ 'foo.js', 'bar.js' ]); assert.deepEqual( map.sourcesContent, [ 'var answer = 42;', 'console.log( answer );' ]); - assert.equal( map.toString(), '{"version":3,"file":"bundle.js","sources":["foo.js","bar.js"],"sourcesContent":["var answer = 42;","console.log( answer );"],"names":[],"mappings":"AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;ACAf,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}' ); - assert.equal( map.toUrl(), 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlcyI6WyJmb28uanMiLCJiYXIuanMiXSwic291cmNlc0NvbnRlbnQiOlsidmFyIGFuc3dlciA9IDQyOyIsImNvbnNvbGUubG9nKCBhbnN3ZXIgKTsiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUNBZixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyJ9' ); - const smc = new SourceMapConsumer( map ); let loc; @@ -400,6 +397,105 @@ describe( 'MagicString.Bundle', () => { loc = smc.originalPositionFor({ line: 3, column: 9 }); assert.equal( loc.source, 'two.js' ); }); + + it( 'should handle empty separator', () => { + const b = new MagicString.Bundle({ + separator: '' + }); + + b.addSource({ + content: new MagicString( 'if ( foo ) { ' ) + }); + + const s = new MagicString( 'console.log( 42 );' ); + s.addSourcemapLocation( 8 ); + s.addSourcemapLocation( 15 ); + + b.addSource({ + filename: 'input.js', + content: s + }); + + b.addSource({ + content: new MagicString( ' }' ) + }); + + assert.equal( b.toString(), 'if ( foo ) { console.log( 42 ); }' ); + + const map = b.generateMap({ + file: 'output.js', + source: 'input.js', + includeContent: true + }); + + const smc = new SourceMapConsumer( map ); + const loc = smc.originalPositionFor({ line: 1, column: 21 }); + + assert.deepEqual( loc, { + source: 'input.js', + name: null, + line: 1, + column: 8 + }); + }); + + // TODO tidy this up. is a recreation of a bug in Svelte + it( 'generates a correct sourcemap for a Svelte component', () => { + const b = new MagicString.Bundle({ + separator: '' + }); + + const s = new MagicString( ` +
+ +`.trim() ); + + [ 21, 23, 38, 42, 50, 51, 54, 59, 66, 67, 70, 72, 74, 76, 77, 81, 84, 85 ].forEach( pos => { + s.addSourcemapLocation( pos ); + }); + + s.remove( 0, 21 ); + s.overwrite( 23, 38, 'return ' ); + s.prependRight( 21, 'var template = (function () {' ); + s.appendLeft( 85, '}());' ); + s.overwrite( 85, 94, '' ); + + b.addSource({ + content: s, + filename: 'input.js' + }); + + assert.equal( b.toString(), ` +var template = (function () { + return { + onrender () { + console.log( 42 ); + } + } +}());`.trim() ); + + const map = b.generateMap({ + file: 'output.js', + source: 'input.js', + includeContent: true + }); + + const smc = new SourceMapConsumer( map ); + const loc = smc.originalPositionFor({ line: 4, column: 16 }); + + assert.deepEqual( loc, { + source: 'input.js', + name: null, + line: 6, + column: 16 + }); + }); }); describe( 'indent', () => {