Skip to content
This repository has been archived by the owner on Jan 6, 2018. It is now read-only.

Commit

Permalink
Use recast to strip functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Rajiv Tirumalareddy committed Feb 22, 2015
1 parent 5020e42 commit 98d8b2d
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 42 deletions.
41 changes: 18 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,29 @@ Simple [Webpack](http://webpack.github.io/) loader to strip custom functions fro

## Usage

In your client js source files:
Before

```javascript

// the following variable will be stubbed to a noop function
var debug = require('debug')('MyFile');

var makeFoo = function () {
// The following two lines of code will be stripped with our webpack loader
debug('makeFoo called');
debug('makeFoo args', arguments);
// The following line of code will be stripped with our webpack loader
debug('making Foo');
// This code would remain
return 'Foo';
};

```

After

```javascript

var debug = function(){};

var makeFoo = function () {
// This code would remain
return 'Foo';
};
Expand All @@ -51,7 +64,7 @@ In your webpack config:
{
module: {
loaders: [
{ test: /\.js$/, loader: "strip-loader?strip[]=debug,strip[]=console.log" }
{ test: /\.js$/, loader: "strip-loader?strip[]=debug,strip[]=assert" }
]
}
};
Expand All @@ -72,24 +85,6 @@ var webpackConfig = {
};
```

### Replace unused module

So far we've removed the calls to the debug function, but your app still requires the `debug` module in the final bundle. Use the [`NormalModuleReplacementPlugin`](http://webpack.github.io/docs/list-of-plugins.html#normalmodulereplacementplugin) to replace it with an empty function:

```javascript
// webpack config
{
plugins: [
new webpack.NormalModuleReplacementPlugin(/debug/, process.cwd() + '/emptyDebug.js'),
]
}

// emptyDebug.js
module.exports = function() { return new Function(); };
```



## License

This software is free to use under the Yahoo! Inc. BSD license.
Expand Down
50 changes: 50 additions & 0 deletions lib/fix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* jshint ignore:start */
// copied and modified from https://github.com/jshint/fixmyjs/blob/v2.0/lib/index.js
// because they don't allow custom rules to be specified

var fu = require('fu');
var recast = require('recast');
var traverse = require('./traverse');
var SHEBANG = /^(\#\!.*)\n/;

function getRules(options) {
return require('./rules').map(function (rule) {
return rule(options);
});
}

function fix(code, config) {
config = config || {};

var shebang = SHEBANG.exec(code);
var pureCode = code.replace(SHEBANG, '');
var ast;
try {
ast = recast.parse(pureCode);
} catch (e) {
if (e.message === 'AST contains no nodes at all?') {
return code;
}
throw e;
}
var rules = getRules(config);
var options = { wrapColumn: Infinity };

var modifiedTree = traverse(ast, function (node, parent) {
return fu.foldl(function (node, f) {
return node && f.hasOwnProperty(node.type)
? f[node.type](node, parent)
: node;
}, rules, node);
});

var generatedCode = recast.print(modifiedTree, options).code;

return shebang === null
? generatedCode
: [shebang[1], generatedCode].join('\n');
}
/* jshint ignore:end */

module.exports = fix;

15 changes: 7 additions & 8 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@
"use strict";

var loaderUtils = require('loader-utils');
var fix = require('./fix');

function StripFnLoader(source) {
var query = loaderUtils.parseQuery(this.query);

if (!query || !query.strip) return;
if (!query || !query.strip) {
this.callback(new Error('strip-loader: no functions provided for stripping'));
}

var toStrip = query.strip.join('|');

var regexPattern = new RegExp('\\n[ \\t]*(' + toStrip + ')\\([^\\);]+\\)[ \\t]*[;\\n]', 'g');

var transformed = source.replace(regexPattern, '\n');

this.callback(null, transformed);
var output = fix(source, {strip: query.strip});
this.callback(null, output);
}

module.exports = StripFnLoader;

module.exports.loader = function () {
Expand Down
4 changes: 4 additions & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = [
require('./stripFnCall'),
require('./stripFnDecl')
];
17 changes: 17 additions & 0 deletions lib/rules/stripFnCall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = function (opts) {
return opts.strip ? {
ExpressionStatement: stripFnCalls.bind(stripFnCalls, opts.strip)
} : {};
};

function stripFnCalls (toStrip, node) {
if (!node.expression.type ||
node.expression.type !== 'CallExpression' ||
!node.expression.callee ||
node.expression.callee.type !== 'Identifier' ||
toStrip.indexOf(node.expression.callee.name) === -1) {
return node;
}

return [];
}
27 changes: 27 additions & 0 deletions lib/rules/stripFnDecl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = function (opts) {
return opts.strip ? {
VariableDeclarator: stripFnDecl.bind(stripFnDecl, opts.strip)
} : {};
};

var recast = require('recast');
var b = recast.types.builders;

var emptyFnNode = b.functionExpression(
null,
[],
{
type: 'BlockStatement',
body: []
});

function stripFnDecl (toStrip, node) {
if (node.id.type !== 'Identifier' ||
toStrip.indexOf(node.id.name) === -1) {
return node;
}

node.init = emptyFnNode;

return node;
}
35 changes: 35 additions & 0 deletions lib/traverse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* jshint ignore:start */
/* globals toString */
/* from https://github.com/jshint/fixmyjs/blob/v2.0/lib/traverse.js*/
module.exports = traverse

var fu = require('fu')

function traverse(o, f, p) {
var k

function make(parent) {
return function (node) {
var next = traverse(node, f, parent)
next === node || (next._fixmyjs = 1)
return next
}
}

if (o === undefined) {
return o
}

for (k in o) {
var call = make(o)

if (toString.call(o[k]) == '[object Object]') {
o[k] = call(o[k])
} else if (Array.isArray(o[k])) {
o[k] = fu.concatMap(call, o[k])
}
}

return f(o, p)
}
/* jshint ignore:end */
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "lib/index.js",
"scripts": {
"cover": "node node_modules/istanbul/lib/cli.js cover --dir artifacts -- ./node_modules/mocha/bin/_mocha tests/unit/ --recursive --reporter spec",
"lint": "./node_modules/.bin/jshint lib tests",
"lint": "./node_modules/.bin/jshint --exclude-path=.gitignore lib tests",
"test": "mocha tests/unit --recursive --reporter spec"
},
"keywords": [
Expand All @@ -26,7 +26,9 @@
"webpack": "^1.4.15"
},
"dependencies": {
"loader-utils": "^0.2.6"
"fu": "^0.1.0",
"loader-utils": "^0.2.6",
"recast": "^0.10.0"
},
"licenses": [
{
Expand Down
8 changes: 5 additions & 3 deletions tests/fixtures/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
* Copyright 2015, Yahoo! Inc.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/

var assert = require('chai').assert;
var debug = require('debug')('Foo');
var makeFoo = function (bar, baz) {
// The following 2 lines of code will be stripped with our webpack loader
console.log('some debug info');
// The following 3 lines of code will be stripped with our webpack loader
assert(bar, true);
debug('better debug info');
debug('This is fancy debug: ' + (baz) + '.');
// This code would remain
return new Foo(bar, baz);
};
23 changes: 17 additions & 6 deletions tests/unit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,31 @@ var createWebpackConfigWithLoader = function (loaderOpt) {

var createWebpackTest = function (done) {
return function(err, stats) {
var statsJson = stats.toJson();
if (stats.hasErrors()) {
done(statsJson.errors[0]);
}

expect(err).to.be.null();
expect(stats.hasErrors()).to.be.false();

var statsJson = stats.toJson();
expect(statsJson.errors).to.have.length(0);

var originalSource = fs.readFileSync(cwd + '/index.js', {encoding: 'utf8'});
expect(originalSource).to.contain('console.log');
expect(originalSource).to.contain('assert');
expect(originalSource).to.contain('debug');

var strippedSource = statsJson.modules[0].source;
expect(strippedSource).to.not.contain('console.log');
expect(strippedSource).to.not.contain('debug');
if (strippedSource.match(/assert/g)) {
expect(strippedSource.match(/assert/g)).to.have.length.of.at.most(1);
} else {
expect(strippedSource).to.not.contain('assert');
}
if (strippedSource.match(/debug/g)) {
expect(strippedSource.match(/debug/g)).to.have.length.of.at.most(1);
} else {
expect(strippedSource).to.not.contain('debug');
}

done(err);
};
Expand All @@ -56,14 +67,14 @@ describe('integration', function () {
describe('webpack', function () {
it('should work with loader query params', function (done) {
webpack(
createWebpackConfigWithLoader(loaderLibPath + '?strip[]=console.log,strip[]=debug'),
createWebpackConfigWithLoader(loaderLibPath + '?strip[]=assert,strip[]=debug'),
createWebpackTest(done)
);
});

it('should work with loader used as library', function (done) {
webpack(
createWebpackConfigWithLoader(loaderLib.loader('console.log', 'debug')),
createWebpackConfigWithLoader(loaderLib.loader('assert', 'debug')),
createWebpackTest(done)
);
});
Expand Down

0 comments on commit 98d8b2d

Please sign in to comment.