Skip to content

-> 0.5: Only create sourcemaps for concatenation, add tests, etc #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.DS_Store
node_modules
test/tests/*/output
index.js
!src/index.js
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# changelog

## 0.5.0

* Ignore input sourcemaps

## 0.4.0

* Handle sourcemaps
Expand All @@ -18,4 +22,4 @@

## 0.1.1

* First release
* First release
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var gobble = require( 'gobble' );
module.exports = gobble( 'js' ).transform( 'concat', { dest: 'bundle.js' });
```

The `dest` property is required. Other values - `files`, `sort`, `separator` and `writeSourcemap`, explained below - are optional.
The `dest` property is required. Other values - `files`, `sort` and `separator`, explained below - are optional.

### `files`

Expand Down Expand Up @@ -64,12 +64,6 @@ module.exports = gobble( 'js' )
});
```

### `writeSourcemap`

Concatenating javascript or CSS files requires some extra handling of their sourcemaps, specially in complex workflows. With this option set to `true`, the sourcemaps of the files to be concatenated will be parsed, files with no sourcemap will be assigned an identity (1:1) sourcemap, and a new sourcemap will be generated from all of them.

The default value is `true` when `dest` is a file with a `.js` or `.css` extension, and `false` otherwise.


## License

Expand Down
142 changes: 78 additions & 64 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
var sander = require( 'sander' ),
path = require( 'path' ),
mapSeries = require( 'promise-map-series' ),
minimatch = require( 'minimatch' );
var SourceNode = require( 'source-map' ).SourceNode;
var SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;

var sourceMapRegExp = new RegExp(/(?:\/\/#|\/\/@|\/\*#)\s*sourceMappingURL=(.*)\s*(?:\*\/\s*)?$/);
var extensionsRegExp = new RegExp(/(\.js|\.css)$/);
var ref = require( 'path' ), basename = ref.basename, extname = ref.extname, join = ref.join;
var sander = require( 'sander' );
var mapSeries = require( 'promise-map-series' );
var minimatch = require( 'minimatch' );
var ref$1 = require( 'vlq' ), encode = ref$1.encode;

var getSourcemapComment = {
'.js': function ( file ) { return ("//# sourceMappingURL=" + file + ".map"); },
'.css': function ( file ) { return ("/*# sourceMappingURL=" + file + ".map */"); }
};

module.exports = function concat ( inputdir, outputdir, options ) {
if ( !options.dest ) throw new Error( ("You must pass a 'dest' option to gobble-concat") );

if ( !options.dest ) {
throw new Error( 'You must pass a \'dest\' option to gobble-concat' );
}
var ext = extname( options.dest );

if ( options.writeSourcemap === undefined ) {
options.writeSourcemap = !!options.dest.match( extensionsRegExp );
}
var shouldCreateSourcemap = ( ext in getSourcemapComment ) && options.sourceMap !== false;
var separator = options.separator || '\n\n';
var separatorSemis = separator.split( '\n' ).join( ';' )

return sander.lsr( inputdir ).then( function ( allFiles ) {
var patterns = options.files,
alreadySeen = {},
nodes = [];
var patterns = options.files;
var alreadySeen = {};
var fileContents = [];

var sourceMap = shouldCreateSourcemap ? {
version: 3,
file: basename( options.dest ),
sources: [],
sourcesContent: [],
mappings: ''
} : null;

if ( !patterns ) {
// use all files
Expand All @@ -36,73 +44,79 @@ module.exports = function concat ( inputdir, outputdir, options ) {
var filtered = allFiles.filter( function ( filename ) {
var shouldInclude = !alreadySeen[ filename ] && minimatch( filename, pattern );

if ( shouldInclude ) {
alreadySeen[ filename ] = true;
}

if ( shouldInclude ) alreadySeen[ filename ] = true;
return shouldInclude;
});

return processFiles( filtered.sort( options.sort ) );
}).then( writeResult );


function processFiles ( filenames ) {
var sourceIndexOffset = 0;
var sourceLineOffset = 0;
var sourceColumnOffset = 0;

var lineCount = 0;

return mapSeries( filenames.sort( options.sort ), function ( filename ) {
return sander.readFile( inputdir, filename ).then( function ( fileContents ) {

/// Run a regexp against the code to check for source mappings.
var match = sourceMapRegExp.exec(fileContents);

if (!match) {
// if (options.verbose) console.log('Creating ident sourcemap for ', filename);
var newNode = new SourceNode(1, 1, filename, fileContents.toString());
newNode.setSourceContent(filename, fileContents.toString());
nodes.push( newNode );
} else {
var sourcemapFilename = match[1];

return sander.readFile( inputdir, path.dirname(filename), sourcemapFilename ).then( function ( mapContents ) {
// Sourcemap exists
var parsedMap = new SourceMapConsumer( mapContents.toString() );
nodes.push( SourceNode.fromStringWithSourceMap( fileContents.toString(), parsedMap ) );
// if (options.verbose) console.log('Loaded sourcemap for ', filename + ': ' + sourcemapFilename + "(" + mapContents.length + " bytes)");
}, function(err) {
throw new Error('File ' + inputdir + ' / ' + filename + ' refers to a non-existing sourcemap at ' + sourcemapFilename + ' ' + err);
});
return sander.readFile( inputdir, filename, { encoding: 'utf-8' }).then( function ( contents ) {
if ( sourceMap ) {
sourceMap.sources.push( join( inputdir, filename ) );
sourceMap.sourcesContent.push( contents );

contents = contents.replace( /^(?:\/\/[@#]\s*sourceMappingURL=(\S+)|\/\*#?\s*sourceMappingURL=(\S+)\s?\*\/)$/gm, '' );
var lines = contents.split( '\n' );

var encoded = lines
.map( function ( line ) {
var encodedLine = '';

if ( line.length ) {
encodedLine += encode([ 0, sourceIndexOffset, sourceLineOffset, sourceColumnOffset ]);

for ( var i = 1; i <= line.length; i += 1 ) {
encodedLine += ',CAAC'; // equivalent to encode([ 1, 0, 0, 1 ])
}

sourceLineOffset = 1;
sourceIndexOffset = 0;
sourceColumnOffset = -( line.length );

lineCount += 1;
}

return encodedLine;
})
.join( ';' );

sourceLineOffset = -lineCount;
lineCount = 0;

sourceMap.mappings += encoded + separatorSemis;
}

fileContents.push( contents );

sourceIndexOffset = 1;
});
});
}

function writeResult () {
if (!nodes[0]) {
// Degenerate case: no matched files
return sander.writeFile( outputdir, options.dest, '' );
}

var separatorNode = new SourceNode(null, null, null, options.separator || '\n\n');
var code = fileContents.join( separator );

var dest = new SourceNode(null, null, null, '');
dest.add(nodes[0]);

for (var i=1; i<nodes.length; i++) {
dest.add(separatorNode);
dest.add(nodes[i]);
}
var generated = dest.toStringWithSourceMap();
if ( shouldCreateSourcemap ) {
var comment = getSourcemapComment[ ext ]( basename( options.dest ) );

if ( options.writeSourcemap ) {
var sourceMapLocation = '\n\n//# sourceMappingURL=' + path.join(outputdir, options.dest + '.map') + '\n'
code += "\n" + comment;

return sander.Promise.all([
sander.writeFile( outputdir, options.dest, generated.code + sourceMapLocation ),
sander.writeFile( outputdir, options.dest + '.map', generated.map.toString() )
sander.writeFile( outputdir, options.dest, code ),
sander.writeFile( outputdir, options.dest + '.map', JSON.stringify( sourceMap, null, ' ' ) )
]);
} else {
return sander.writeFile( outputdir, options.dest, generated.code);
}

return sander.writeFile( outputdir, options.dest, code );
}
});
};
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
{
"name": "gobble-concat",
"description": "Concatenate files with gobble",
"version": "0.4.0",
"version": "0.5.0",
"authors": [
"Rich Harris",
"Iván Sánchez Ortega <[email protected]>"
],
"license": "MIT",
"repository": "https://github.com/gobblejs/gobble-concat",
"scripts": {
"test": "mocha --compilers js:buble/register",
"build": "buble -i src/index.js -o index.js --target node:0.10",
"pretest": "npm run build"
},
"files": [
"index.js"
],
Expand All @@ -18,6 +23,12 @@
"minimatch": "~1.0.0",
"promise-map-series": "~0.2.0",
"sander": "^0.1.6",
"source-map": "^0.5.3"
"vlq": "^0.2.1"
},
"devDependencies": {
"buble": "^0.6.2",
"glob": "^7.0.3",
"mocha": "^2.4.5",
"sander": "^0.1.6"
}
}
122 changes: 122 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const { basename, extname, join } = require( 'path' );
const sander = require( 'sander' );
const mapSeries = require( 'promise-map-series' );
const minimatch = require( 'minimatch' );
const { encode } = require( 'vlq' );

const getSourcemapComment = {
'.js': file => `//# sourceMappingURL=${file}.map`,
'.css': file => `/*# sourceMappingURL=${file}.map */`
};

module.exports = function concat ( inputdir, outputdir, options ) {
if ( !options.dest ) throw new Error( `You must pass a 'dest' option to gobble-concat` );

const ext = extname( options.dest );

const shouldCreateSourcemap = ( ext in getSourcemapComment ) && options.sourceMap !== false;
const separator = options.separator || '\n\n';
const separatorSemis = separator.split( '\n' ).join( ';' )

return sander.lsr( inputdir ).then( allFiles => {
let patterns = options.files;
let alreadySeen = {};
let fileContents = [];

let sourceMap = shouldCreateSourcemap ? {
version: 3,
file: basename( options.dest ),
sources: [],
sourcesContent: [],
mappings: ''
} : null;

if ( !patterns ) {
// use all files
return processFiles( allFiles.sort( options.sort ) ).then( writeResult );
}

if ( typeof patterns === 'string' ) {
patterns = [ patterns ];
}

return mapSeries( patterns, pattern => {
let filtered = allFiles.filter( filename => {
const shouldInclude = !alreadySeen[ filename ] && minimatch( filename, pattern );

if ( shouldInclude ) alreadySeen[ filename ] = true;
return shouldInclude;
});

return processFiles( filtered.sort( options.sort ) );
}).then( writeResult );

function processFiles ( filenames ) {
let sourceIndexOffset = 0;
let sourceLineOffset = 0;
let sourceColumnOffset = 0;

let lineCount = 0;

return mapSeries( filenames.sort( options.sort ), filename => {
return sander.readFile( inputdir, filename, { encoding: 'utf-8' }).then( contents => {
if ( sourceMap ) {
sourceMap.sources.push( join( inputdir, filename ) );
sourceMap.sourcesContent.push( contents );

contents = contents.replace( /^(?:\/\/[@#]\s*sourceMappingURL=(\S+)|\/\*#?\s*sourceMappingURL=(\S+)\s?\*\/)$/gm, '' );
const lines = contents.split( '\n' );

const encoded = lines
.map( line => {
let encodedLine = '';

if ( line.length ) {
encodedLine += encode([ 0, sourceIndexOffset, sourceLineOffset, sourceColumnOffset ]);

for ( let i = 1; i <= line.length; i += 1 ) {
encodedLine += ',CAAC'; // equivalent to encode([ 1, 0, 0, 1 ])
}

sourceLineOffset = 1;
sourceIndexOffset = 0;
sourceColumnOffset = -( line.length );

lineCount += 1;
}

return encodedLine;
})
.join( ';' );

sourceLineOffset = -lineCount;
lineCount = 0;

sourceMap.mappings += encoded + separatorSemis;
}

fileContents.push( contents );

sourceIndexOffset = 1;
});
});
}

function writeResult () {
let code = fileContents.join( separator );

if ( shouldCreateSourcemap ) {
const comment = getSourcemapComment[ ext ]( basename( options.dest ) );

code += `\n${comment}`;

return sander.Promise.all([
sander.writeFile( outputdir, options.dest, code ),
sander.writeFile( outputdir, options.dest + '.map', JSON.stringify( sourceMap, null, ' ' ) )
]);
}

return sander.writeFile( outputdir, options.dest, code );
}
});
};
Loading