├── .gitignore ├── .travis.yml ├── CHANGELOG.MD ├── README.MD ├── callback-loader.js ├── package.json └── test └── callback-loader.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | script: node_modules/.bin/mocha -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | ## 0.2.4 2 | - Fixed error with colons in object arguments (#2). 3 | 4 | ## 0.2.3 5 | - Option for disable cache 6 | 7 | ## 0.2.2 8 | - The loader became cacheable 9 | - Callbacks now calls with loader context in **this** 10 | 11 | ## 0.2.1 12 | - Added Object properties support 13 | - Fixed bug with multiple functions processing -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # callback-loader 2 | [![Build Status](https://travis-ci.org/Kreozot/callback-loader.svg?branch=master)](https://travis-ci.org/Kreozot/callback-loader) 3 | [![npm version](https://badge.fury.io/js/callback-loader.svg)](https://badge.fury.io/js/callback-loader) 4 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/Kreozot/callback-loader) 5 | 6 | Webpack loader that parses your JS, calls specified functions and replaces their with the results. 7 | 8 | ## Installation 9 | 10 | `npm install callback-loader --save-dev` 11 | 12 | ## Usage 13 | 14 | Source file: 15 | 16 | ```javascript 17 | var a = multBy2(10); 18 | var b = mult(10, 3); 19 | var c = concat("foo", "bar"); 20 | ``` 21 | 22 | Webpack config: 23 | 24 | ```javascript 25 | { 26 | ... 27 | callbackLoader: { 28 | multBy2: function(num) { 29 | return num * 2; 30 | }, 31 | mult: function(num1, num2) { 32 | return num1 * num2; 33 | }, 34 | concat: function(str1, str2) { 35 | return '"' + str1 + str2 + '"'; 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | Result: 42 | 43 | ```javascript 44 | var a = 20; 45 | var b = 30; 46 | var c = "foobar"; 47 | ``` 48 | 49 | Notice that quotes was added in **concat** function. 50 | 51 | ### Loader parameters 52 | 53 | You can choose which functions will be processed in your query: 54 | 55 | ```javascript 56 | 'callback?mult&multBy2!example.js' 57 | ``` 58 | 59 | Result for this query will be this: 60 | 61 | ```javascript 62 | var a = 20; 63 | var b = 30; 64 | var c = concat("foo", "bar"); 65 | ``` 66 | 67 | ### Different configs 68 | 69 | Webpack config: 70 | 71 | ```javascript 72 | { 73 | ... 74 | callbackLoader: { 75 | concat: function(str1, str2) { 76 | return '"' + str1 + str2 + '"'; 77 | } 78 | }, 79 | anotherConfig: { 80 | concat: function(str1, str2) { 81 | return '"' + str1 + str2 + '-version2"'; 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | Loader query: 88 | 89 | ```javascript 90 | 'callback?config=anotherConfig!example.js' 91 | ``` 92 | 93 | Result for this query will be this: 94 | 95 | ```javascript 96 | var a = multBy2(10); 97 | var b = mult(10, 3); 98 | var c = "foobar-version2"; 99 | ``` 100 | 101 | ### Cache 102 | 103 | The loader is cacheable by default, but you can disable cache if you want: 104 | 105 | ```javascript 106 | 'callback?cacheable=false!example.js' 107 | ``` 108 | 109 | ## Restrictions 110 | 111 | * No expressions and variables in function arguments yet, sorry. Only raw values. 112 | * No async callbacks yet. 113 | 114 | ## Use cases 115 | 116 | * Build time cache. 117 | * Using node modules. 118 | * Localization (see example below) 119 | * Using any other build-time stuff (compiler directives, version number, parameters, etc) 120 | 121 | ## Real life examples 122 | 123 | ### Localization 124 | 125 | Let's say we have two language versions and we want to use messages for both of them in a same place, like this: 126 | 127 | ```javascript 128 | showMessage(localize{en: 'Hello, world!', ru: 'Привет, мир!'}); 129 | ``` 130 | 131 | But in this case we should require *localize* function everywhere. Besides it is an redundant call and excess result code size. 132 | 133 | So let's just move all the *localize* calls to build time! 134 | 135 | Webpack config: 136 | 137 | ```javascript 138 | var languages = ['en', 'ru']; 139 | 140 | module.exports = languages.map(function (language) { 141 | return { 142 | ... 143 | output: { 144 | filename: '[name].' + language + '.js' 145 | }, 146 | callbackLoader: { 147 | localize: function (textObj) { 148 | return '"' + textObj[language] + '"'; 149 | } 150 | } 151 | } 152 | }); 153 | ``` 154 | 155 | That's all! Now take a look at our localized code. 156 | 157 | bundle.en.js: 158 | 159 | ```javascript 160 | showMessage("Hello, world!"); 161 | ``` 162 | 163 | bundle.ru.js 164 | 165 | ```javascript 166 | showMessage("Привет, мир!"); 167 | ``` 168 | 169 | ### Using API at build time 170 | 171 | Ok, let's say we need an array of points for a map in *points.js* file: 172 | 173 | ```javascript 174 | module.exports = [ 175 | { 176 | name: 'Moscow', 177 | coords: [37.617, 55.756] 178 | }, { 179 | name: 'Tokyo', 180 | coords: [139.692, 35.689] 181 | }, ... 182 | ] 183 | ``` 184 | 185 | But we don't want to search and write coordinates by yourself. We just want to type city names and let Google Maps API do the REST. But at the same time it's not a good idea to send hundreds of requests each time user open your map. Can we do this once in a build time? 186 | 187 | Let's write something like this: 188 | 189 | ```javascript 190 | module.exports = [ 191 | { 192 | name: 'Moscow', 193 | coords: okGoogleGiveMeTheCoords('Moscow, Russia') 194 | }, { 195 | name: 'Tokyo', 196 | coords: okGoogleGiveMeTheCoords('Tokyo, Japan') 197 | }, ... 198 | ] 199 | ``` 200 | 201 | Looks much more pretty, right? Now we just need to implement *okGoogleGiveMeTheCoords* and config callback-loader: 202 | 203 | ```javascript 204 | var request = require('sync-request'); 205 | ... 206 | var webpackConfig = { 207 | ... 208 | pointsCallback: { 209 | okGoogleGiveMeTheCoords: function (address) { 210 | var response = request('GET', 'http://maps.google.com/maps/api/geocode/json?address=' + address + '&sensor=false'); 211 | var data = JSON.parse(response.getBody()); 212 | var coords = data.results[0].geometry.location; 213 | return '[' + coords.lng + ', ' + coords.lat + ']'; 214 | } 215 | } 216 | } 217 | ``` 218 | 219 | Now write a *require* statement: 220 | 221 | ```javascript 222 | var points = require('callback?config=pointsCallback!./points.js'); 223 | ``` 224 | 225 | And in *points* we have the array from the first example but we didn't write none of coordinates. 226 | 227 | ### Using dynamic formed requires 228 | 229 | Webpack only knows about requires which had been written by your hands. But what if we want to write requires by a script? E.g. we want to require all modules from array (in case we need to configure them in external config). 230 | 231 | components.js 232 | ```javascript 233 | module.exports = function () { 234 | requireComponents(); 235 | }; 236 | ``` 237 | 238 | webpack config 239 | ```javascript 240 | callbackLoader: { 241 | requireComponents: function() { 242 | var modules = ["menu", "buttons", "forms"]; 243 | return modules.map(function (module) { 244 | var moduleLink = 'components/' + module + '/index.js'; 245 | return 'require("' + moduleLink + '");'; 246 | }).join('\n'); 247 | } 248 | } 249 | ``` 250 | 251 | So if we load our *components.js* with callback-loader, result will be this: 252 | ```javascript 253 | module.exports = function () { 254 | require("components/menu/index.js"); 255 | require("components/buttons/index.js"); 256 | require("components/forms/index.js"); 257 | }; 258 | ``` 259 | 260 | Now we have to apply this script (using apply-loader which simply adds an execution statement to the module) and all this dependencies will be resolved: 261 | ```javascript 262 | require('apply!callback!./components.js'); 263 | ``` 264 | -------------------------------------------------------------------------------- /callback-loader.js: -------------------------------------------------------------------------------- 1 | var astQuery = require('ast-query'); 2 | var loaderUtils = require('loader-utils'); 3 | var escodegen = require('escodegen'); 4 | 5 | module.exports = function (source) { 6 | var self = this; 7 | var query = loaderUtils.parseQuery(this.query); 8 | var configKey = query.config || 'callbackLoader'; 9 | var cacheable = (typeof query.cacheable !== 'undefined') ? query.cacheable : true; 10 | if (cacheable) { 11 | this.cacheable(); 12 | } 13 | //Disabling async mode for this loader. 14 | this.async = function() {}; 15 | 16 | var functions = this.options[configKey]; 17 | var functionNames = Object.keys(query).filter(function (key) { 18 | return (key !== 'config') && (key !== 'cacheable'); 19 | }); 20 | if (functionNames.length === 0) { 21 | functionNames = Object.keys(functions); 22 | } 23 | 24 | //Offset for future replaces 25 | var indexOffset; 26 | 27 | //Replace substring between *indexFrom* and *indexTo* in *text* with *replaceText* 28 | function replaceIn(text, indexFrom, indexTo, replaceText) { 29 | var actualIndexFrom = indexFrom + indexOffset; 30 | var actualIndexTo = indexTo + indexOffset; 31 | //Correcting the offset 32 | indexOffset = indexOffset + replaceText.length - (indexTo - indexFrom); 33 | return text.substr(0, actualIndexFrom) + replaceText + text.substr(actualIndexTo, text.length); 34 | } 35 | 36 | functionNames.forEach(function (funcName) { 37 | var ast = astQuery(source); 38 | var query = ast.callExpression(funcName); 39 | 40 | indexOffset = 0; 41 | 42 | query.nodes.forEach(function (node) { 43 | var args = node.arguments.map(function (argument) { 44 | if (argument.type == 'Literal') { 45 | return argument.value; 46 | } else if (argument.type == 'ObjectExpression') { 47 | var value = escodegen.generate(argument, {format: {json: true}}); 48 | // Take the keys of the object to quotes for JSON.parse 49 | value = value.replace(/([{,])(?:\s*)([A-Za-z0-9_\-]+?)\s*:/g, '$1"$2":'); 50 | value = JSON.parse(value); 51 | return value; 52 | } else { 53 | var msg = 'Error when parsing arguments of function ' + funcName + '. Only absolute values accepted. Index: ' + argument.range[0]; 54 | console.error(msg); 55 | throw msg; 56 | } 57 | }); 58 | var value = functions[funcName].apply(self, args); 59 | source = replaceIn(source, node.range[0], node.range[1], value.toString()); 60 | }); 61 | }); 62 | 63 | return source; 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callback-loader", 3 | "version": "0.2.4", 4 | "description": "Webpack loader that parses your JS, calls specified functions and replaces their with the results.", 5 | "main": "callback-loader.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": "https://github.com/Kreozot/callback-loader", 10 | "keywords": [ 11 | "webpack", 12 | "loader", 13 | "callback", 14 | "functions", 15 | "parse", 16 | "js" 17 | ], 18 | "author": "Kreozot", 19 | "license": "ISC", 20 | "dependencies": { 21 | "ast-query": "^1.0.1", 22 | "escodegen": "^1.7.0", 23 | "loader-utils": "^0.2.11" 24 | }, 25 | "devDependencies": { 26 | "mocha": "^2.3.3", 27 | "should": "^7.1.0", 28 | "webpack": "^1.12.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/callback-loader.test.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var path = require("path"); 3 | var loader = require("../"); 4 | 5 | loader.cacheable = function () {}; 6 | 7 | describe("loader", function () { 8 | 9 | var options, 10 | context; 11 | 12 | beforeEach(function() { 13 | options = { 14 | callbackLoader: { 15 | multBy2: function(num) { 16 | return num * 2; 17 | }, 18 | mult: function(num1, num2) { 19 | return num1 * num2; 20 | }, 21 | concat: function(str1, str2) { 22 | return '"' + str1 + str2 + '"'; 23 | }, 24 | getSecond: function(obj) { 25 | return obj['second']; 26 | }, 27 | urlParse: function (urls) { 28 | return '"' + urls.arg1 + '"'; 29 | } 30 | }, 31 | callbackLoader2: { 32 | multBy2: function(num) { 33 | return num * 2; 34 | }, 35 | mult: function(num1, num2) { 36 | return num1 * num2; 37 | }, 38 | concat: function(str1, str2) { 39 | return '"' + str1 + str2 + '-version2"'; 40 | } 41 | } 42 | } 43 | 44 | context = { 45 | cacheable: function() {}, 46 | options: options 47 | }; 48 | }); 49 | 50 | it("should process all functions without query", function() { 51 | context.query = ''; 52 | loader.call(context, 'var a = multBy2(10); var b = mult(10, 3);') 53 | .should.be.eql( 54 | 'var a = 20; var b = 30;' 55 | ); 56 | }); 57 | 58 | it("should process complicated structure", function() { 59 | context.query = ''; 60 | loader.call(context, 'var a = multBy2(10); var b = mult(10, 3); var c = multBy2(12); var d = mult(12, 3);') 61 | .should.be.eql( 62 | 'var a = 20; var b = 30; var c = 24; var d = 36;' 63 | ); 64 | }); 65 | 66 | it("should process one function with one parameter", function() { 67 | context.query = '?multBy2'; 68 | loader.call(context, 'var a = multBy2(10); var b = mult(10, 3);') 69 | .should.be.eql( 70 | 'var a = 20; var b = mult(10, 3);' 71 | ); 72 | }); 73 | 74 | it("should process multiple function with multiple parameters", function() { 75 | context.query = '?multBy2&mult'; 76 | loader.call(context, 'var a = multBy2(10); var b = mult(10, 3);') 77 | .should.be.eql( 78 | 'var a = 20; var b = 30;' 79 | ); 80 | }); 81 | 82 | it("should process function with float args", function() { 83 | context.query = '?multBy2'; 84 | loader.call(context, 'var a = multBy2(1.2);') 85 | .should.be.eql( 86 | 'var a = 2.4;' 87 | ); 88 | }); 89 | 90 | it("should process function with string args", function() { 91 | context.query = ''; 92 | loader.call(context, 'var a = concat("foo", "bar");') 93 | .should.be.eql( 94 | 'var a = "foobar";' 95 | ); 96 | }); 97 | 98 | it("should take another config", function() { 99 | context.query = '?config=callbackLoader2'; 100 | loader.call(context, 'var a = concat("foo", "bar");') 101 | .should.be.eql( 102 | 'var a = "foobar-version2";' 103 | ); 104 | }); 105 | 106 | it("should process function with object args", function() { 107 | context.query = ''; 108 | var result = loader.call(context, 'var a = getSecond({first: 1, second: 2});') 109 | .should.be.eql( 110 | 'var a = 2;' 111 | ); 112 | }); 113 | 114 | it("should not breaks at urls", function() { 115 | context.query = ''; 116 | loader.call(context, 'const url = urlParse({arg1: "http://localhost:8000/", arg2: "http://localhost:8000/"});') 117 | .should.be.eql( 118 | 'const url = "http://localhost:8000/";' 119 | ); 120 | }); 121 | 122 | }); --------------------------------------------------------------------------------