diff --git a/.gitignore b/.gitignore index b0081af1da..e3eea17ef7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ dist .nyc_output coverage coverage.lcov +test/sourcemaps/*/output.js +test/sourcemaps/*/output.js.map diff --git a/compiler/generate/index.js b/compiler/generate/index.js index f73b4a90b3..347d7062cf 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -86,7 +86,7 @@ export default function generate ( parsed, source, options ) { const { name } = flattenReference( node ); if ( parent && parent.type === 'CallExpression' && node === parent.callee ) { - if ( generator.helpers[ name ] ) generator.code.insertRight( node.start, `template.helpers.` ); + if ( generator.helpers[ name ] ) generator.code.prependRight( node.start, `template.helpers.` ); return; } @@ -102,7 +102,7 @@ export default function generate ( parsed, source, options ) { if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); } else { dependencies.push( node.name ); - generator.code.insertRight( node.start, `root.` ); + generator.code.prependRight( node.start, `root.` ); if ( !~usedContexts.indexOf( 'root' ) ) usedContexts.push( 'root' ); } @@ -204,19 +204,19 @@ export default function generate ( parsed, source, options ) { while ( /\s/.test( source[ i - 1 ] ) ) i--; const indentation = source.slice( i, defaultExport.start ); - generator.code.insertLeft( finalNode.end, `\n\n${indentation}return template;` ); + generator.code.appendLeft( finalNode.end, `\n\n${indentation}return template;` ); } defaultExport.declaration.properties.forEach( prop => { templateProperties[ prop.key.name ] = prop.value; }); - generator.code.insertRight( parsed.js.content.start, 'var template = (function () {' ); + generator.code.prependRight( parsed.js.content.start, 'var template = (function () {' ); } else { - generator.code.insertRight( parsed.js.content.start, '(function () {' ); + generator.code.prependRight( parsed.js.content.start, '(function () {' ); } - generator.code.insertLeft( parsed.js.content.end, '}());' ); + generator.code.appendLeft( parsed.js.content.end, '}());' ); [ 'helpers', 'events', 'components' ].forEach( key => { if ( templateProperties[ key ] ) { @@ -508,30 +508,35 @@ export default function generate ( parsed, source, options ) { function addString ( str ) { compiled.addSource({ - filename: options.filename, content: new MagicString( str ) }); } - addString( getIntro( format, options, imports ) ); + const intro = getIntro( format, options, imports ); + if ( intro ) addString( intro ); + + // a filename is necessary for sourcemap generation + const filename = options.filename || 'SvelteComponent.html'; parts.forEach( str => { + const chunk = str.replace( pattern, '' ); + if ( chunk ) addString( chunk ); + const match = pattern.exec( str ); - addString( str.replace( pattern, '' ) ); + const snippet = generator.code.snip( +match[1], +match[2] ); compiled.addSource({ - filename: options.filename, - content: generator.code.snip( +match[1], +match[2] ) + filename, + content: snippet }); }); - compiled.append( finalChunk ); - + addString( finalChunk ); addString( '\n\n' + getOutro( format, constructorName, options, imports ) ); return { code: compiled.toString(), - map: compiled.generateMap() + map: compiled.generateMap({ includeContent: true }) }; } diff --git a/compiler/generate/visitors/attributes/addComponentAttributes.js b/compiler/generate/visitors/attributes/addComponentAttributes.js index 9995352dc8..47609e9c81 100644 --- a/compiler/generate/visitors/attributes/addComponentAttributes.js +++ b/compiler/generate/visitors/attributes/addComponentAttributes.js @@ -80,7 +80,7 @@ export default function addComponentAttributes ( generator, node, local ) { else if ( attribute.type === 'EventHandler' ) { // TODO verify that it's a valid callee (i.e. built-in or declared method) generator.addSourcemapLocations( attribute.expression ); - generator.code.insertRight( attribute.expression.start, 'component.' ); + generator.code.prependRight( attribute.expression.start, 'component.' ); const usedContexts = new Set(); attribute.expression.arguments.forEach( arg => { diff --git a/compiler/generate/visitors/attributes/addElementAttributes.js b/compiler/generate/visitors/attributes/addElementAttributes.js index 8637266ffd..6f372b24ee 100644 --- a/compiler/generate/visitors/attributes/addElementAttributes.js +++ b/compiler/generate/visitors/attributes/addElementAttributes.js @@ -116,7 +116,7 @@ export default function addElementAttributes ( generator, node, local ) { else if ( attribute.type === 'EventHandler' ) { // TODO verify that it's a valid callee (i.e. built-in or declared method) generator.addSourcemapLocations( attribute.expression ); - generator.code.insertRight( attribute.expression.start, 'component.' ); + generator.code.prependRight( attribute.expression.start, 'component.' ); const usedContexts = new Set(); attribute.expression.arguments.forEach( arg => { diff --git a/package.json b/package.json index 96bcdcf10b..fdd79c6f04 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "fuzzyset.js": "0.0.1", "jsdom": "^9.8.3", "locate-character": "^2.0.0", - "magic-string": "^0.16.0", + "magic-string": "^0.19.0", "mocha": "^3.1.2", "node-resolve": "^1.3.3", "nyc": "^9.0.1", @@ -56,6 +56,7 @@ "rollup-plugin-buble": "^0.14.0", "rollup-plugin-commonjs": "^5.0.5", "rollup-plugin-node-resolve": "^2.0.0", + "source-map": "^0.5.6", "source-map-support": "^0.4.6" }, "nyc": { diff --git a/test/sourcemaps/basic/input.html b/test/sourcemaps/basic/input.html new file mode 100644 index 0000000000..ed83093fa9 --- /dev/null +++ b/test/sourcemaps/basic/input.html @@ -0,0 +1 @@ +{{foo.bar.baz}} diff --git a/test/sourcemaps/basic/test.js b/test/sourcemaps/basic/test.js new file mode 100644 index 0000000000..b169b04f7a --- /dev/null +++ b/test/sourcemaps/basic/test.js @@ -0,0 +1,34 @@ +export function test ({ assert, smc, locateInSource, locateInGenerated }) { + const expected = locateInSource( 'foo.bar.baz' ); + + let loc; + let actual; + + loc = locateInGenerated( 'foo.bar.baz' ); + + actual = smc.originalPositionFor({ + line: loc.line + 1, + column: loc.column + }); + + assert.deepEqual( actual, { + source: 'SvelteComponent.html', + name: null, + line: expected.line + 1, + column: expected.column + }); + + loc = locateInGenerated( 'foo.bar.baz', loc.character + 1 ); + + actual = smc.originalPositionFor({ + line: loc.line + 1, + column: loc.column + }); + + assert.deepEqual( actual, { + source: 'SvelteComponent.html', + name: null, + line: expected.line + 1, + column: expected.column + }); +} diff --git a/test/sourcemaps/script/input.html b/test/sourcemaps/script/input.html new file mode 100644 index 0000000000..c4dc11d3a0 --- /dev/null +++ b/test/sourcemaps/script/input.html @@ -0,0 +1,9 @@ +
+ + diff --git a/test/sourcemaps/script/test.js b/test/sourcemaps/script/test.js new file mode 100644 index 0000000000..6b14b91a9d --- /dev/null +++ b/test/sourcemaps/script/test.js @@ -0,0 +1,16 @@ +export function test ({ assert, smc, locateInSource, locateInGenerated }) { + const expected = locateInSource( '42' ); + const loc = locateInGenerated( '42' ); + + const actual = smc.originalPositionFor({ + line: loc.line + 1, + column: loc.column + }); + + assert.deepEqual( actual, { + source: 'SvelteComponent.html', + name: null, + line: expected.line + 1, + column: expected.column + }); +} diff --git a/test/test.js b/test/test.js index 9d132849e3..1738c5e5ad 100644 --- a/test/test.js +++ b/test/test.js @@ -5,6 +5,8 @@ import * as path from 'path'; import * as fs from 'fs'; import jsdom from 'jsdom'; import * as acorn from 'acorn'; +import { SourceMapConsumer } from 'source-map'; +import { getLocator } from 'locate-character'; import * as consoleGroup from 'console-group'; consoleGroup.install(); @@ -468,4 +470,29 @@ describe( 'svelte', () => { }); }); }); + + describe( 'sourcemaps', () => { + fs.readdirSync( 'test/sourcemaps' ).forEach( dir => { + if ( dir[0] === '.' ) return; + + const solo = exists( `test/sourcemaps/${dir}/solo` ); + + ( solo ? it.only : it )( dir, () => { + const input = fs.readFileSync( `test/sourcemaps/${dir}/input.html`, 'utf-8' ).replace( /\s+$/, '' ); + const { code, map } = svelte.compile( input ); + + fs.writeFileSync( `test/sourcemaps/${dir}/output.js`, `${code}\n//# sourceMappingURL=output.js.map` ); + fs.writeFileSync( `test/sourcemaps/${dir}/output.js.map`, JSON.stringify( map, null, ' ' ) ); + + const { test } = require( `./sourcemaps/${dir}/test.js` ); + + const smc = new SourceMapConsumer( map ); + + const locateInSource = getLocator( input ); + const locateInGenerated = getLocator( code ); + + test({ assert, code, map, smc, locateInSource, locateInGenerated }); + }); + }); + }); });