Skip to content

Commit

Permalink
Initial commit!
Browse files Browse the repository at this point in the history
  • Loading branch information
developit committed Jan 9, 2018
0 parents commit 12897fa
Show file tree
Hide file tree
Showing 11 changed files with 488 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[{package.json,.*rc,*.yml}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.DS_Store
.cache
.mocha-puppeteer
*.log
build
dist
package-lock.json
yarn.lock
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<p align="center">
<img src="https://i.imgur.com/JLAwk0S.png" width="300" height="300" alt="workerize-loader">
<br>
<a href="https://www.npmjs.org/package/workerize-loader"><img src="https://img.shields.io/npm/v/workerize-loader.svg?style=flat" alt="npm"></a> <a href="https://travis-ci.org/developit/workerize-loader"><img src="https://travis-ci.org/developit/workerize-loader.svg?branch=master" alt="travis"></a>
</p>

# workerize-loader

> A webpack loader that moves a module and its dependencies into a Web Worker, automatically reflecting exported functions as asynchronous proxies.
- Bundles a tiny, purpose-built RPC implementation into your app
- If exported module methods are already async, signature is unchanged
- Supports synchronous and asynchronous worker functions
- Works beautifully with async/await
- Imported value is instantiable, just a decorated `Worker`


## Install

```sh
npm install --save-dev workerize-loader
```


### Usage

**worker.js**:

```js
// block for `time` ms, then return the number of loops we could run in that time:
export function expensive(time) {
let start = Date.now(),
count = 0
while (Date.now() - start < time) count++
return count
}
```

**index.js**: _(our demo)_

```js
import worker from 'workerize-loader!./worker'

let instance = worker() // `new` is optional

instance.expensive(1000).then( count => {
console.log(`Ran ${count} loops`)
})
```

### License

[MIT License](LICENSE.md) © [Jason Miller](https://jasonformat.com)
59 changes: 59 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "workerize-loader",
"version": "1.0.0",
"description": "Automatically move a module into a Web Worker (Webpack loader)",
"main": "dist/index.js",
"scripts": {
"build": "microbundle --inline none --format cjs --no-compress src/*.js",
"prepublishOnly": "npm run build",
"dev": "webpack-dev-server --no-hot --config test/webpack.config.js",
"test": "npm run build && webpack --config test/webpack.config.js && npm run -s mocha",
"mocha": "concurrently -r --kill-others \"serve -p 42421 test/dist\" \"mocha-chrome http://localhost:42421\""
},
"eslintConfig": {
"extends": "eslint-config-developit",
"rules": {
"jest/valid-expect": 0,
"no-console": 0
}
},
"files": [
"src",
"dist"
],
"keywords": [
"webpack",
"loader",
"worker",
"web worker",
"thread",
"workerize"
],
"author": "Jason Miller <jason@developit.ca> (http://jasonformat.com)",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"concurrently": "^3.5.1",
"css-loader": "^0.28.8",
"eslint": "^4.14.0",
"eslint-config-developit": "^1.1.1",
"exports-loader": "^0.6.4",
"fast-async": "^6.3.0",
"html-webpack-plugin": "^2.30.1",
"microbundle": "^0.2.4",
"mocha": "^4.1.0",
"mocha-chrome": "^1.0.3",
"mocha-puppeteer": "^0.13.0",
"serve": "^6.4.4",
"style-loader": "^0.19.1",
"webpack": "^3.10.0",
"webpack-dev-server": "^2.10.1"
},
"dependencies": {
"loader-utils": "^1.1.0"
}
}
113 changes: 113 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import path from 'path';
import loaderUtils from 'loader-utils';

import NodeTargetPlugin from 'webpack/lib/node/NodeTargetPlugin';
import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin';
import WebWorkerTemplatePlugin from 'webpack/lib/webworker/WebWorkerTemplatePlugin';

export default function loader() {}

const CACHE = {};

loader.pitch = function(request) {
this.cacheable(false);

const options = loaderUtils.getOptions(this) || {};

const cb = this.async();

const filename = loaderUtils.interpolateName(this, `${options.name || '[hash]'}.worker.js`, {
context: options.context || this.options.context,
regExp: options.regExp
});

const worker = {};

worker.options = {
filename,
chunkFilename: `[id].${filename}`,
namedChunkFilename: null
};

worker.compiler = this._compilation.createChildCompiler('worker', worker.options);

worker.compiler.apply(new WebWorkerTemplatePlugin(worker.options));

if (this.target!=='webworker' && this.target!=='web') {
worker.compiler.apply(new NodeTargetPlugin());
}

worker.compiler.apply(new SingleEntryPlugin(this.context, `!!${path.resolve(__dirname, 'rpc-worker-loader.js')}!${request}`, 'main'));

const subCache = `subcache ${__dirname} ${request}`;

worker.compiler.plugin('compilation', (compilation, data) => {
if (compilation.cache) {
if (!compilation.cache[subCache]) compilation.cache[subCache] = {};

compilation.cache = compilation.cache[subCache];
}

data.normalModuleFactory.plugin('parser', (parser, options) => {
parser.plugin('export declaration', expr => {
let decl = expr.declaration || expr,
{ compilation, current } = parser.state,
entry = compilation.entries[0].resource;

// only process entry exports
if (current.resource!==entry) return;

let exports = compilation.__workerizeExports || (compilation.__workerizeExports = {});

if (decl.id) {
exports[decl.id.name] = true;
}
else if (decl.declarations) {
for (let i=0; i<decl.declarations.length; i++) {
exports[decl.declarations[i].id.name] = true;
}
}
else {
console.warn('[workerize] unknown export declaration: ', expr);
}
});
});
});

worker.compiler.runAsChild((err, entries, compilation) => {
if (err) return cb(err);

if (entries[0]) {
worker.file = entries[0].files[0];

let contents = compilation.assets[worker.file].source();
let exports = Object.keys(CACHE[worker.file] = compilation.__workerizeExports || CACHE[worker.file] || {});

// console.log('Workerized exports: ', exports.join(', '));

if (options.inline) {
worker.url = `URL.createObjectURL(new Blob([${JSON.stringify(contents)}]))`;
}
else {
worker.url = `__webpack_public_path__ + ${JSON.stringify(worker.file)}`;
}

if (options.fallback === false) {
delete this._compilation.assets[worker.file];
}

return cb(null, `
var addMethods = require(${loaderUtils.stringifyRequest(this, path.resolve(__dirname, 'rpc-wrapper.js'))})
var methods = ${JSON.stringify(exports)}
module.exports = function() {
var w = new Worker(${worker.url}, { name: ${JSON.stringify(filename)} })
addMethods(w, methods)
${ options.ready ? 'w.ready = new Promise(function(r) { w.addEventListener("ready", function(){ r(w) }) })' : '' }
return w
}
`);
}

return cb(null, null);
});
};
29 changes: 29 additions & 0 deletions src/rpc-worker-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* global __webpack_exports__ */

function workerSetup() {
addEventListener('message', (e) => {
let { type, method, id, params } = e.data, f, p;
if (type==='RPC' && method) {
if ((f = __webpack_exports__[method])) {
p = Promise.resolve().then( () => f.apply(__webpack_exports__, params) );
}
else {
p = Promise.reject('No such method');
}
p.then(
result => {
postMessage({ type: 'RPC', id, result });
},
error => {
postMessage({ type: 'RPC', id, error });
});
}
});
postMessage({ type: 'RPC', method: 'ready' });
}

const workerScript = '\n' + Function.prototype.toString.call(workerSetup).replace(/(^.*\{|\}.*$|\n\s*)/g, '');

export default function rpcWorkerLoader(content) {
return content + workerScript;
}
29 changes: 29 additions & 0 deletions src/rpc-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default function addMethods(worker, methods) {
let c = 0;
let callbacks = {};
worker.addEventListener('message', (e) => {
let d = e.data;
if (d.type!=='RPC') return;
if (d.id) {
let f = callbacks[d.id];
if (f) {
delete callbacks[d.id];
if (d.error) f[1](d.error);
else f[0](d.result);
}
}
else {
let evt = document.createEvent('Event');
evt.initEvent(d.method);
evt.data = d.params;
worker.dispatchEvent(evt);
}
});
methods.forEach( method => {
worker[method] = (...params) => new Promise( (a, b) => {
let id = ++c;
callbacks[id] = [a, b];
worker.postMessage({ type: 'RPC', id, method, params });
});
});
}
Loading

0 comments on commit 12897fa

Please sign in to comment.