diff --git a/src/builtins/map.js b/src/builtins/map.js index a6ddea6..b7c1e1b 100644 --- a/src/builtins/map.js +++ b/src/builtins/map.js @@ -7,14 +7,7 @@ import config from '../config'; import extractLocationInfo from '../utils/extractLocationInfo'; import { isRegExp } from '../utils/is'; import { ABORTED } from '../utils/signals'; - -let SOURCEMAPPING_URL = 'sourceMa'; -SOURCEMAPPING_URL += 'ppingURL'; - -const SOURCEMAP_COMMENT = new RegExp( `\n*(?:` + - `\\/\\/[@#]\\s*${SOURCEMAPPING_URL}=([^'"]+)|` + // js - `\\/\\*#?\\s*${SOURCEMAPPING_URL}=([^'"]+)\\s\\+\\/)` + // css -`\\s*$`, 'g' ); +import { getSourcemapComment, SOURCEMAPPING_URL, SOURCEMAP_COMMENT } from '../utils/sourcemap'; export default function map ( inputdir, outputdir, options ) { let changed = {}; @@ -125,7 +118,7 @@ function processResult ( result, original, src, dest, codepath ) { // if a sourcemap was returned, use it if ( result.map ) { return { - code: result.code.replace( SOURCEMAP_COMMENT, '' ) + sourceMappingURLComment( codepath ), + code: result.code.replace( SOURCEMAP_COMMENT, '' ) + getSourcemapComment( encodeURI( codepath + '.map' ), extname( codepath ) ), map: processSourcemap( result.map, src, dest, original ) }; } @@ -158,7 +151,7 @@ function processInlineSourceMap ( code, src, dest, original, codepath ) { let json = atob( match[1] ); map = processSourcemap( json, src, dest, original ); - code = code.replace( SOURCEMAP_COMMENT, '' ) + sourceMappingURLComment( codepath ); + code = code.replace( SOURCEMAP_COMMENT, '' ) + getSourcemapComment( encodeURI( codepath + '.map' ), extname( codepath ) ); } return { code, map }; diff --git a/src/nodes/Node.js b/src/nodes/Node.js index 9f8f147..0297493 100644 --- a/src/nodes/Node.js +++ b/src/nodes/Node.js @@ -82,45 +82,14 @@ export default class Node extends EventEmitter2 { node.on( 'error', handleError ); function build () { - const buildStart = Date.now(); - buildScheduled = false; + watchTask.emit( 'build:start' ); + node.ready() .then( outputdir => { - watchTask.emit( 'built', { - dir: outputdir, - duration: Date.now() - buildStart - }); + watchTask.emit( 'build:end', outputdir ); }) - /*.then( inputdir => { - const sourcemapProcessStart = Date.now(); - - watchTask.emit( 'info', { - code: 'SOURCEMAP_PROCESS_START', - progressIndicator: true - }); - - // create new directory for sourcemaps... - const outputdir = join( session.config.gobbledir, '.final', '' + uid++ ); - - return copydir( inputdir ).to( outputdir ) - .then( () => flattenSourcemaps( inputdir, outputdir, dest, watchTask ) ) - .then( () => { - watchTask.emit( 'info', { - code: 'SOURCEMAP_PROCESS_COMPLETE', - duration: Date.now() - sourcemapProcessStart - }); - - watchTask.emit( 'info', { - code: 'BUILD_COMPLETE', - duration: Date.now() - buildStart, - watch: true - }); - - watchTask.emit( 'built', outputdir ); - }); - })*/ .catch( handleError ); } @@ -137,7 +106,7 @@ export default class Node extends EventEmitter2 { watchTask.close = () => node.stop(); this.start(); - build(); + process.nextTick( build ); return watchTask; } diff --git a/src/nodes/Transformer.js b/src/nodes/Transformer.js index 61f76d5..55dfaed 100644 --- a/src/nodes/Transformer.js +++ b/src/nodes/Transformer.js @@ -53,8 +53,6 @@ export default class Transformer extends Node { transformation.aborted = true; }; - this.sourcemaps = {}; - outputdir = resolve( session.config.gobbledir, this.id, '' + this.counter++ ); this._ready = this.input.ready().then( inputdir => { diff --git a/src/nodes/serve/handleRequest.js b/src/nodes/serve/handleRequest.js index 21b0f99..5adecee 100644 --- a/src/nodes/serve/handleRequest.js +++ b/src/nodes/serve/handleRequest.js @@ -1,11 +1,12 @@ -import { join } from 'path'; +import { extname, join } from 'path'; import { parse } from 'url'; import { stat, Promise } from 'sander'; import serveFile from './serveFile'; import serveDir from './serveDir'; +import serveSourcemap from './serveSourcemap'; import serveError from './serveError'; -export default function handleRequest ( srcDir, error, request, response ) { +export default function handleRequest ( srcDir, error, sourcemapPromises, request, response ) { const parsedUrl = parse( request.url ); const pathname = parsedUrl.pathname; @@ -29,6 +30,11 @@ export default function handleRequest ( srcDir, error, request, response ) { filepath = join( srcDir, pathname ); + if ( extname( filepath ) === '.map' ) { + return serveSourcemap( filepath, sourcemapPromises, request, response ) + .catch( err => serveError( err, request, response ) ); + } + return stat( filepath ).then( stats => { if ( stats.isDirectory() ) { // might need to redirect from `foo` to `foo/` diff --git a/src/nodes/serve/index.js b/src/nodes/serve/index.js index da8de1d..b833090 100644 --- a/src/nodes/serve/index.js +++ b/src/nodes/serve/index.js @@ -16,6 +16,7 @@ export default function serve ( node, options = {} ) { let buildStarted = Date.now(); let watchTask; let srcDir; + let sourcemapPromises; let server; let serverReady; let lrServer; @@ -35,14 +36,23 @@ export default function serve ( node, options = {} ) { task.emit( 'error', err ); }); - watchTask.on( 'built', d => { + let buildStart; + watchTask.on( 'build:start', () => buildStart = Date.now() ); + + watchTask.on( 'build:end', dir => { error = null; - srcDir = d; + sourcemapPromises = {}; + srcDir = dir; built = true; task.emit( 'built' ); + task.emit( 'info', { + code: 'BUILD_COMPLETE', + duration: Date.now() - buildStart + }); + if ( !firedReadyEvent && serverReady ) { task.emit( 'ready' ); firedReadyEvent = true; @@ -114,7 +124,7 @@ export default function serve ( node, options = {} ) { }); server.on( 'request', ( request, response ) => { - handleRequest( srcDir, error, request, response ) + handleRequest( srcDir, error, sourcemapPromises, request, response ) .catch( err => task.emit( 'error', err ) ); }); diff --git a/src/nodes/serve/serveFile.js b/src/nodes/serve/serveFile.js index b26279c..ee455e0 100644 --- a/src/nodes/serve/serveFile.js +++ b/src/nodes/serve/serveFile.js @@ -1,13 +1,32 @@ +import { basename, extname } from 'path'; import { lookup } from 'mime'; -import { readFile } from 'sander'; +import { readFile, stat, createReadStream } from 'sander'; +import { getSourcemapComment, SOURCEMAP_COMMENT } from '../../utils/sourcemap'; export default function serveFile ( filepath, request, response ) { - return readFile( filepath ).then( data => { + const ext = extname( filepath ); + + // this might be turn out to be a really bad idea. But let's try it and see + if ( ext === '.js' || ext === '.css' ) { + return readFile( filepath ).then( data => { + // this takes the auto-generated absolute sourcemap path, and turns + // it into what you'd get with `gobble build` or `gobble watch` + const sourcemapComment = getSourcemapComment( basename( filepath ) + '.map', ext ); + data = data.toString().replace( SOURCEMAP_COMMENT, sourcemapComment ); + + response.statusCode = 200; + response.setHeader( 'Content-Type', lookup( filepath ) ); + + response.write( data ); + response.end(); + }); + } + + return stat( filepath ).then( stats => { response.statusCode = 200; response.setHeader( 'Content-Type', lookup( filepath ) ); - response.setHeader( 'Content-Length', data.length ); + response.setHeader( 'Content-Length', stats.size ); - response.write( data ); - response.end(); + createReadStream( filepath ).pipe( response ); }); } diff --git a/src/nodes/serve/serveSourcemap.js b/src/nodes/serve/serveSourcemap.js new file mode 100644 index 0000000..01c8e0f --- /dev/null +++ b/src/nodes/serve/serveSourcemap.js @@ -0,0 +1,24 @@ +import { load } from 'sorcery'; + +export default function serveSourcemap ( filepath, sourcemapPromises, request, response ) { + const owner = filepath.slice( 0, -4 ); + + if ( !sourcemapPromises[ filepath ] ) { + sourcemapPromises[ filepath ] = load( owner ) + .then( chain => { + if ( !chain ) { + throw new Error( 'Could not resolve sourcemap for ' + owner ); + } + + return chain.apply().toString(); + }); + } + + return sourcemapPromises[ filepath ].then( map => { + response.statusCode = 200; + response.setHeader( 'Content-Type', 'application/json' ); + + response.write( map ); + response.end(); + }); +} \ No newline at end of file diff --git a/src/nodes/watch/index.js b/src/nodes/watch/index.js index 2f88f30..17815fe 100644 --- a/src/nodes/watch/index.js +++ b/src/nodes/watch/index.js @@ -2,6 +2,7 @@ import { copydir, rimraf, Promise } from 'sander'; import cleanup from '../../utils/cleanup'; import session from '../../session'; import GobbleError from '../../utils/GobbleError'; +import flattenSourcemaps from '../../utils/flattenSourcemaps'; export default function watch ( node, options ) { if ( !options || !options.dest ) { @@ -23,11 +24,35 @@ export default function watch ( node, options ) { watchTask.on( 'info', details => task.emit( 'info', details ) ); watchTask.on( 'error', err => task.emit( 'error', err ) ); - watchTask.on( 'built', { dir, duration } => { + let buildStart; + watchTask.on( 'build:start', () => buildStart = Date.now() ); + + watchTask.on( 'build:end', dir => { const dest = options.dest; rimraf( dest ) - .then( () => copydir( outputdir ).to( dest ) ) + .then( () => copydir( dir ).to( dest ) ) + .then( () => { + const sourcemapProcessStart = Date.now(); + + task.emit( 'info', { + code: 'SOURCEMAP_PROCESS_START', + progressIndicator: true + }); + + return flattenSourcemaps( dir, dest, dest, task ).then( () => { + task.emit( 'info', { + code: 'SOURCEMAP_PROCESS_COMPLETE', + duration: Date.now() - sourcemapProcessStart + }); + + task.emit( 'info', { + code: 'BUILD_COMPLETE', + duration: Date.now() - buildStart, + watch: true + }); + }); + }) .then( () => task.emit( 'built', dest ) ) .catch( err => task.emit( 'error', err ) ); }); diff --git a/src/utils/sourcemap.js b/src/utils/sourcemap.js new file mode 100644 index 0000000..00b2cbc --- /dev/null +++ b/src/utils/sourcemap.js @@ -0,0 +1,17 @@ +let SOURCEMAPPING_URL = 'sourceMa'; +SOURCEMAPPING_URL += 'ppingURL'; + +const SOURCEMAP_COMMENT = new RegExp( `\n*(?:` + + `\\/\\/[@#]\\s*${SOURCEMAPPING_URL}=([^'"]+)|` + // js + `\\/\\*#?\\s*${SOURCEMAPPING_URL}=([^'"]+)\\s\\+\\/)` + // css +`\\s*$`, 'g' ); + +function getSourcemapComment ( url, ext ) { + if ( ext === '.css' ) { + return `\n/*# ${SOURCEMAPPING_URL}=${url} */\n`; + } + + return `\n//# ${SOURCEMAPPING_URL}=${url}\n`; +} + +export { getSourcemapComment, SOURCEMAP_COMMENT, SOURCEMAPPING_URL }; \ No newline at end of file diff --git a/test/sourcemaps.js b/test/sourcemaps.js index 7d69b6a..edc7f35 100644 --- a/test/sourcemaps.js +++ b/test/sourcemaps.js @@ -81,8 +81,7 @@ module.exports = function () { task.on( 'error', done ); task.on( 'built', function () { request( 'http://localhost:4567/foo.js' ).then( function ( body ) { - var sourceMappingURL = /sourceMappingURL=(.+)/.exec( body )[1]; - assert.equal( sourceMappingURL, 'foo.js.map' ); + assert.equal( extractSourceMappingURL( body ), 'foo.js.map' ); request( 'http://localhost:4567/foo.js.map' ).then( JSON.parse ).then( function ( map ) { assert.deepEqual( map.sourcesContent, [ sander.readFileSync( 'tmp/baz/foo.js' ).toString() ] ); done(); @@ -103,8 +102,7 @@ module.exports = function () { sander.readFile( 'tmp/output/foo.js' ) .then( String ) .then( function ( body ) { - var sourceMappingURL = /sourceMappingURL=(.+)/.exec( body )[1]; - assert.equal( sourceMappingURL, 'foo.js.map' ); + assert.equal( extractSourceMappingURL( body ), 'foo.js.map' ); }), sander.readFile( 'tmp/output/foo.js.map' ) @@ -205,7 +203,7 @@ module.exports = function () { return sander.readFile( 'tmp/output/file with spaces.js' ) .then( String ) .then( function ( contents ) { - var sourceMappingURL = /sourceMappingURL=([^\r\n]+)/.exec( contents )[1]; + var sourceMappingURL = extractSourceMappingURL( contents ); assert.ok( !/\s/.test( sourceMappingURL ) ); }); }); @@ -223,8 +221,7 @@ module.exports = function () { Promise.all([ request( 'http://localhost:4567/app.min.js' ) .then( function ( body ) { - var sourceMappingURL = /sourceMappingURL=([^\r\n]+)/.exec( body )[1]; - assert.equal( sourceMappingURL, 'app.min.js.map' ); + assert.equal( extractSourceMappingURL( body ), 'app.min.js.map' ); }), request( 'http://localhost:4567/app.min.js.map' ) @@ -258,8 +255,7 @@ module.exports = function () { sander.readFile( 'tmp/output/app.min.js' ) .then( String ) .then( function ( content ) { - var sourceMappingURL = /sourceMappingURL=([^\r\n]+)/.exec( content )[1]; - assert.equal( sourceMappingURL, 'app.min.js.map' ); + assert.equal( extractSourceMappingURL( content ), 'app.min.js.map' ); }), sander.readFile( 'tmp/output/app.min.js.map' ) @@ -365,20 +361,31 @@ module.exports = function () { } }; }) - .serve() + .serve(); task.on( 'error', done ); task.once( 'ready', function () { - console.log( 'READY' ); - // map file should not exist yet - assert.deepEqual( sander.readdirSync( '.gobble/.final/1' ), [ 'foo.js' ] ); - - request( 'http://localhost:4567/foo.js.map' ) - .then( JSON.parse ) - .then( function ( map ) { - console.log( 'map', map ); - assert.ok( false ); + // map file should not exist on disk + // TODO this is a really hacky way of testing + var dir = sander.readdirSync( '.gobble' )[0]; + var source = sander.readFileSync( '.gobble', dir, '.cache', 'foo.js' ).toString(); + var map = sander.readFileSync( '.gobble', dir, '.cache', 'foo.js.map' ).toString(); + + // sourceMappingURL on disk should be an absolute path... + assert.ok( /\.gobble/.test( extractSourceMappingURL( source ) ) ); + + request( 'http://localhost:4567/foo.js' ) + .then( function ( servedSource ) { + // ...but should be relative when served + assert.equal( extractSourceMappingURL( servedSource ), 'foo.js.map' ); + }) + .then( function () { + return request( 'http://localhost:4567/foo.js.map' ) + .then( JSON.parse ) + .then( function ( map ) { + assert.equal( map.file, 'foo.js' ); + }); }) .then( done ) .catch( done ); @@ -389,4 +396,9 @@ module.exports = function () { function btoa ( str ) { return new Buffer( str ).toString( 'base64' ); +} + +function extractSourceMappingURL ( data ) { + var match = /sourceMappingURL=([^\r\n]+)/.exec( data ); + return match && match[1]; } \ No newline at end of file