├── .gitignore ├── README.md ├── example ├── common-js-worker.js ├── es6-worker.js ├── main.js └── webpack.config.js ├── index.html ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | output.js 4 | node_modules 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webworkify-webpack 2 | 3 | Generates a web worker at runtime from webpack's bundled modules with only the used dependencies. Possible because of webpack's module structure. Just `require.resolve(PATH_TO_MODULE)` the module you want to be the worker's entry point. 4 | 5 | inspired by [webworkify](https://github.com/substack/webworkify) 6 | 7 | # install 8 | 9 | ```sh 10 | npm install webworkify-webpack --save 11 | ``` 12 | 13 | # v2 vs v1 14 | 15 | For v1 go to: [1.1.8](https://github.com/borisirota/webworkify-webpack/tree/1.1.8) 16 | 17 | Version 2 uses webpack's api as much as possible vs the hacky implementation of version 1 (I wasn't aware of webpack's api while writing it) which did the job but with some inconsistency between different browsers and some caveats. 18 | 19 | In v2: 20 | * no limitation on webpack's devtool - `eval` was forbidden in v1. 21 | * no issues with anonymous functions exported as module.exports - there were issues with anonymous functions in v1. 22 | * `require.resolve` instead of regular `require\import` - The only limitation is using `require.resolve` which means that currently the code using `webworkify-webpack` is coupled to the build tool (webpack - but who uses `webworkify-webpack` already uses webpack) and its not possible to use es2015 modules => checkout out the [future work](#future-work) section. 23 | 24 | # webworkify-webpack vs webpack's [worker-loader](https://github.com/webpack/worker-loader) and [target: 'webworker'](https://webpack.github.io/docs/configuration.html#target) 25 | 26 | `webworkify-webpack` allows to use one bundle for running same code both on browser and web worker environments. 27 | webpack's current alternatives for web workers are creating bundle which can be run in a web worker environment only and can results in 2 separate files like in the `worker-loader` case (one file for browser and one for web worker => code duplication). 28 | 29 | The motivation for `webworkify-webpack` was creating a library which expose to the user the same functionality both in sync and async forms. 30 | I wanted to keep one bundle in order to reduce complexity of using external library to the minimum and make bundle size as minimal as possible when using external library which supports both sync and async functionality (without code duplication). 31 | 32 | Since webpack's solutions for web workers are being constructed at compile time, the added value is that its possible to use dev tools like `hmr` (at least when using `target: 'webworker'`) which isn't possible with `webworkify-webpack`. 33 | In addition, regular `js` syntax is being used without the need to use `require.resolve` as in the `webworkify-webpack` case => checkout out the [future work](#future-work) section. 34 | 35 | # methods 36 | 37 | ```js 38 | import work from 'webworkify-webpack' 39 | ``` 40 | 41 | ## let w = work(require.resolve(modulePath) [, options]) 42 | 43 | Return a new 44 | [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker) 45 | from the module at `modulePath`. 46 | 47 | The file at `modulePath` should export its worker code in `module.exports` as a 48 | function that will be run with no arguments. 49 | 50 | Note that all the code outside of the `module.exports` function will be run in 51 | the main thread too so don't put any computationally intensive code in that 52 | part. It is necessary for the main code to `require()` the worker code to fetch 53 | the module reference and load `modulePath`'s dependency graph into the bundle 54 | output. 55 | 56 | ### options 57 | - all - bundle all the dependencies in the web worker and not only the used ones. can be useful in edge cases that I'm not aware of when the used dependencies aren't being resolved as expected due to the runtime regex checking mechanism or just to avoid additional work at runtime to traverse the dependencies tree. 58 | - bare - the return value will be the blob constructed with the worker's code and not the web worker itself. 59 | 60 | # example 61 | 62 | First, a `main.js` file will launch the `worker.js` and print its output: 63 | 64 | ```js 65 | import work from 'webworkify-webpack'; 66 | 67 | let w = work(require.resolve('./worker.js')); 68 | w.addEventListener('message', event => { 69 | console.log(event.data); 70 | }); 71 | 72 | w.postMessage(4); // send the worker a message 73 | ``` 74 | 75 | then `worker.js` can `require()` modules of its own. The worker function lives 76 | inside of the `module.exports`: 77 | 78 | ```js 79 | import gamma from 'gamma' 80 | 81 | module.exports = function worker (self) { 82 | self.addEventListener('message', (event) => { 83 | const startNum = parseInt(event.data); // ev.data=4 from main.js 84 | setInterval(() => { 85 | const r = startNum / Math.random() - 1; 86 | self.postMessage([ startNum, r, gamma(r) ]); 87 | }, 500); 88 | }); 89 | }; 90 | ``` 91 | 92 | Now after [webpackifying](https://webpack.github.io) this example, the console will 93 | contain output from the worker: 94 | 95 | ``` 96 | [ 4, 0.09162078520553618, 10.421030346237066 ] 97 | [ 4, 2.026562457360466, 1.011522336481017 ] 98 | [ 4, 3.1853125018703716, 2.3887589540750214 ] 99 | [ 4, 5.6989969260510005, 72.40768854476167 ] 100 | [ 4, 8.679491643020487, 20427.19357947782 ] 101 | [ 4, 0.8528139834191428, 1.1098187157762498 ] 102 | [ 4, 8.068322137547542, 5785.928308309402 ] 103 | ... 104 | ``` 105 | 106 | # future work 107 | 108 | The goal is to make `webworkify-webpack` fully based on webpack's api. I'm not sure how to accomplish it since I never wrote a webpack loader\plugin (is it possible other way?) so I'm asking for help :) 109 | 110 | Points of view: 111 | 112 | 1. [webpackBootstrapFunc](https://github.com/borisirota/webworkify-webpack/blob/master/index.js#L1) - should be taken from webpack's source. 113 | 2. ability to use regular module import\require (not `require.resolve`) but still passing the module id to 'webworkify-webpack'. 114 | 3. ability to know all specific's module dependencies in compile time so there is no need to traverse the dependencies tree in runtime with regular expressions (when uglifying the code the web worker's bundle can include more dependencies than only the used ones because regular expressions nature). 115 | 4. if there is going to be build in compile time, what about hmr as dev tool ? 116 | 5. is the ability 'webworkify-webpack' provides should be part of webpack core as another form of web workers support or should it remain as external module ? 117 | 118 | # license 119 | 120 | MIT 121 | -------------------------------------------------------------------------------- /example/common-js-worker.js: -------------------------------------------------------------------------------- 1 | var gamma = require('gamma'); 2 | 3 | module.exports = function () { 4 | self.addEventListener('message',function (ev){ 5 | var startNum = parseInt(ev.data); // ev.data=4 from main.js 6 | 7 | setInterval(function () { 8 | var r = startNum / Math.random() - 1; 9 | self.postMessage([ startNum, r, gamma(r) ]); 10 | }, 500); 11 | }); 12 | }; -------------------------------------------------------------------------------- /example/es6-worker.js: -------------------------------------------------------------------------------- 1 | import gamma from 'gamma'; 2 | 3 | export default function () { 4 | self.addEventListener('message',function (ev){ 5 | var startNum = parseInt(ev.data); // ev.data=4 from main.js 6 | 7 | setInterval(function () { 8 | var r = startNum / Math.random() - 1; 9 | self.postMessage([ startNum, r, gamma(r) ]); 10 | }, 500); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import work from '../' 2 | 3 | var w1 = work(require.resolve('./common-js-worker.js')) 4 | w1.addEventListener('message', function (ev) { 5 | console.log('CommonJS Worker:', ev.data) 6 | }) 7 | 8 | w1.postMessage(4) // send the worker a message 9 | 10 | var w2 = work(require.resolve('./es6-worker.js')) 11 | w2.addEventListener('message', function (ev) { 12 | console.log('ES6 Worker', ev.data) 13 | }) 14 | 15 | w2.postMessage(4) // send the worker a message 16 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: { 5 | app: ['./example/main.js'] 6 | }, 7 | output: { 8 | filename: 'output.js', 9 | pathinfo: true 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | use: [ 17 | { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['es2015'] 21 | } 22 | } 23 | ] 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new webpack.NamedModulesPlugin() 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function webpackBootstrapFunc (modules) { 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | /******/ // expose the modules object (__webpack_modules__) 30 | /******/ __webpack_require__.m = modules; 31 | 32 | /******/ // expose the module cache 33 | /******/ __webpack_require__.c = installedModules; 34 | 35 | /******/ // identity function for calling harmony imports with the correct context 36 | /******/ __webpack_require__.i = function(value) { return value; }; 37 | 38 | /******/ // define getter function for harmony exports 39 | /******/ __webpack_require__.d = function(exports, name, getter) { 40 | /******/ if(!__webpack_require__.o(exports, name)) { 41 | /******/ Object.defineProperty(exports, name, { 42 | /******/ configurable: false, 43 | /******/ enumerable: true, 44 | /******/ get: getter 45 | /******/ }); 46 | /******/ } 47 | /******/ }; 48 | 49 | /******/ // define __esModule on exports 50 | /******/ __webpack_require__.r = function(exports) { 51 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 52 | /******/ }; 53 | 54 | /******/ // getDefaultExport function for compatibility with non-harmony modules 55 | /******/ __webpack_require__.n = function(module) { 56 | /******/ var getter = module && module.__esModule ? 57 | /******/ function getDefault() { return module['default']; } : 58 | /******/ function getModuleExports() { return module; }; 59 | /******/ __webpack_require__.d(getter, 'a', getter); 60 | /******/ return getter; 61 | /******/ }; 62 | 63 | /******/ // Object.prototype.hasOwnProperty.call 64 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 65 | 66 | /******/ // __webpack_public_path__ 67 | /******/ __webpack_require__.p = "/"; 68 | 69 | /******/ // on error function for async loading 70 | /******/ __webpack_require__.oe = function(err) { console.error(err); throw err; }; 71 | 72 | var f = __webpack_require__(__webpack_require__.s = ENTRY_MODULE) 73 | return f.default || f // try to call default if defined to also support babel esmodule exports 74 | } 75 | 76 | var moduleNameReqExp = '[\\.|\\-|\\+|\\w|\/|@]+' 77 | var dependencyRegExp = '\\(\\s*(\/\\*.*?\\*\/)?\\s*.*?(' + moduleNameReqExp + ').*?\\)' // additional chars when output.pathinfo is true 78 | 79 | // http://stackoverflow.com/a/2593661/130442 80 | function quoteRegExp (str) { 81 | return (str + '').replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&') 82 | } 83 | 84 | function isNumeric(n) { 85 | return !isNaN(1 * n); // 1 * n converts integers, integers as string ("123"), 1e3 and "1e3" to integers and strings to NaN 86 | } 87 | 88 | function getModuleDependencies (sources, module, queueName) { 89 | var retval = {} 90 | retval[queueName] = [] 91 | 92 | var fnString = module.toString() 93 | var wrapperSignature = fnString.match(/^function\s?\w*\(\w+,\s*\w+,\s*(\w+)\)/) 94 | if (!wrapperSignature) return retval 95 | var webpackRequireName = wrapperSignature[1] 96 | 97 | // main bundle deps 98 | var re = new RegExp('(\\\\n|\\W)' + quoteRegExp(webpackRequireName) + dependencyRegExp, 'g') 99 | var match 100 | while ((match = re.exec(fnString))) { 101 | if (match[3] === 'dll-reference') continue 102 | retval[queueName].push(match[3]) 103 | } 104 | 105 | // dll deps 106 | re = new RegExp('\\(' + quoteRegExp(webpackRequireName) + '\\("(dll-reference\\s(' + moduleNameReqExp + '))"\\)\\)' + dependencyRegExp, 'g') 107 | while ((match = re.exec(fnString))) { 108 | if (!sources[match[2]]) { 109 | retval[queueName].push(match[1]) 110 | sources[match[2]] = __webpack_require__(match[1]).m 111 | } 112 | retval[match[2]] = retval[match[2]] || [] 113 | retval[match[2]].push(match[4]) 114 | } 115 | 116 | // convert 1e3 back to 1000 - this can be important after uglify-js converted 1000 to 1e3 117 | var keys = Object.keys(retval); 118 | for (var i = 0; i < keys.length; i++) { 119 | for (var j = 0; j < retval[keys[i]].length; j++) { 120 | if (isNumeric(retval[keys[i]][j])) { 121 | retval[keys[i]][j] = 1 * retval[keys[i]][j]; 122 | } 123 | } 124 | } 125 | 126 | return retval 127 | } 128 | 129 | function hasValuesInQueues (queues) { 130 | var keys = Object.keys(queues) 131 | return keys.reduce(function (hasValues, key) { 132 | return hasValues || queues[key].length > 0 133 | }, false) 134 | } 135 | 136 | function getRequiredModules (sources, moduleId) { 137 | var modulesQueue = { 138 | main: [moduleId] 139 | } 140 | var requiredModules = { 141 | main: [] 142 | } 143 | var seenModules = { 144 | main: {} 145 | } 146 | 147 | while (hasValuesInQueues(modulesQueue)) { 148 | var queues = Object.keys(modulesQueue) 149 | for (var i = 0; i < queues.length; i++) { 150 | var queueName = queues[i] 151 | var queue = modulesQueue[queueName] 152 | var moduleToCheck = queue.pop() 153 | seenModules[queueName] = seenModules[queueName] || {} 154 | if (seenModules[queueName][moduleToCheck] || !sources[queueName][moduleToCheck]) continue 155 | seenModules[queueName][moduleToCheck] = true 156 | requiredModules[queueName] = requiredModules[queueName] || [] 157 | requiredModules[queueName].push(moduleToCheck) 158 | var newModules = getModuleDependencies(sources, sources[queueName][moduleToCheck], queueName) 159 | var newModulesKeys = Object.keys(newModules) 160 | for (var j = 0; j < newModulesKeys.length; j++) { 161 | modulesQueue[newModulesKeys[j]] = modulesQueue[newModulesKeys[j]] || [] 162 | modulesQueue[newModulesKeys[j]] = modulesQueue[newModulesKeys[j]].concat(newModules[newModulesKeys[j]]) 163 | } 164 | } 165 | } 166 | 167 | return requiredModules 168 | } 169 | 170 | module.exports = function (moduleId, options) { 171 | options = options || {} 172 | var sources = { 173 | main: __webpack_modules__ 174 | } 175 | 176 | var requiredModules = options.all ? { main: Object.keys(sources.main) } : getRequiredModules(sources, moduleId) 177 | 178 | var src = '' 179 | 180 | Object.keys(requiredModules).filter(function (m) { return m !== 'main' }).forEach(function (module) { 181 | var entryModule = 0 182 | while (requiredModules[module][entryModule]) { 183 | entryModule++ 184 | } 185 | requiredModules[module].push(entryModule) 186 | sources[module][entryModule] = '(function(module, exports, __webpack_require__) { module.exports = __webpack_require__; })' 187 | src = src + 'var ' + module + ' = (' + webpackBootstrapFunc.toString().replace('ENTRY_MODULE', JSON.stringify(entryModule)) + ')({' + requiredModules[module].map(function (id) { return '' + JSON.stringify(id) + ': ' + sources[module][id].toString() }).join(',') + '});\n' 188 | }) 189 | 190 | src = src + 'new ((' + webpackBootstrapFunc.toString().replace('ENTRY_MODULE', JSON.stringify(moduleId)) + ')({' + requiredModules.main.map(function (id) { return '' + JSON.stringify(id) + ': ' + sources.main[id].toString() }).join(',') + '}))(self);' 191 | 192 | var blob = new window.Blob([src], { type: 'text/javascript' }) 193 | if (options.bare) { return blob } 194 | 195 | var URL = window.URL || window.webkitURL || window.mozURL || window.msURL 196 | 197 | var workerUrl = URL.createObjectURL(blob) 198 | var worker = new window.Worker(workerUrl) 199 | worker.objectURL = workerUrl 200 | 201 | return worker 202 | } 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webworkify-webpack", 3 | "version": "2.1.5", 4 | "description": "launch a web worker at runtime that can require() in the browser with webpack", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config example/webpack.config.js", 8 | "serve": "http-server", 9 | "example": "npm run build && npm run serve" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/borisirota/webworkify-webpack.git" 14 | }, 15 | "keywords": [ 16 | "web", 17 | "worker", 18 | "webworker", 19 | "background", 20 | "browser", 21 | "inline", 22 | "runtime", 23 | "webpack" 24 | ], 25 | "author": "Boris Sirota