Skip to content

Commit 12897fa

Browse files
committed
Initial commit!
0 parents  commit 12897fa

File tree

11 files changed

+488
-0
lines changed

11 files changed

+488
-0
lines changed

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
indent_style = tab
5+
end_of_line = lf
6+
charset = utf-8
7+
trim_trailing_whitespace = true
8+
insert_final_newline = true
9+
10+
[{package.json,.*rc,*.yml}]
11+
indent_style = space
12+
indent_size = 2
13+
14+
[*.md]
15+
trim_trailing_whitespace = false

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
.DS_Store
3+
.cache
4+
.mocha-puppeteer
5+
*.log
6+
build
7+
dist
8+
package-lock.json
9+
yarn.lock

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<p align="center">
2+
<img src="https://i.imgur.com/JLAwk0S.png" width="300" height="300" alt="workerize-loader">
3+
<br>
4+
<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>
5+
</p>
6+
7+
# workerize-loader
8+
9+
> A webpack loader that moves a module and its dependencies into a Web Worker, automatically reflecting exported functions as asynchronous proxies.
10+
11+
- Bundles a tiny, purpose-built RPC implementation into your app
12+
- If exported module methods are already async, signature is unchanged
13+
- Supports synchronous and asynchronous worker functions
14+
- Works beautifully with async/await
15+
- Imported value is instantiable, just a decorated `Worker`
16+
17+
18+
## Install
19+
20+
```sh
21+
npm install --save-dev workerize-loader
22+
```
23+
24+
25+
### Usage
26+
27+
**worker.js**:
28+
29+
```js
30+
// block for `time` ms, then return the number of loops we could run in that time:
31+
export function expensive(time) {
32+
let start = Date.now(),
33+
count = 0
34+
while (Date.now() - start < time) count++
35+
return count
36+
}
37+
```
38+
39+
**index.js**: _(our demo)_
40+
41+
```js
42+
import worker from 'workerize-loader!./worker'
43+
44+
let instance = worker() // `new` is optional
45+
46+
instance.expensive(1000).then( count => {
47+
console.log(`Ran ${count} loops`)
48+
})
49+
```
50+
51+
### License
52+
53+
[MIT License](LICENSE.md) © [Jason Miller](https://jasonformat.com)

package.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "workerize-loader",
3+
"version": "1.0.0",
4+
"description": "Automatically move a module into a Web Worker (Webpack loader)",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"build": "microbundle --inline none --format cjs --no-compress src/*.js",
8+
"prepublishOnly": "npm run build",
9+
"dev": "webpack-dev-server --no-hot --config test/webpack.config.js",
10+
"test": "npm run build && webpack --config test/webpack.config.js && npm run -s mocha",
11+
"mocha": "concurrently -r --kill-others \"serve -p 42421 test/dist\" \"mocha-chrome http://localhost:42421\""
12+
},
13+
"eslintConfig": {
14+
"extends": "eslint-config-developit",
15+
"rules": {
16+
"jest/valid-expect": 0,
17+
"no-console": 0
18+
}
19+
},
20+
"files": [
21+
"src",
22+
"dist"
23+
],
24+
"keywords": [
25+
"webpack",
26+
"loader",
27+
"worker",
28+
"web worker",
29+
"thread",
30+
"workerize"
31+
],
32+
"author": "Jason Miller <[email protected]> (http://jasonformat.com)",
33+
"license": "MIT",
34+
"devDependencies": {
35+
"babel-core": "^6.26.0",
36+
"babel-loader": "^7.1.2",
37+
"babel-preset-env": "^1.6.1",
38+
"chai": "^4.1.2",
39+
"chai-as-promised": "^7.1.1",
40+
"concurrently": "^3.5.1",
41+
"css-loader": "^0.28.8",
42+
"eslint": "^4.14.0",
43+
"eslint-config-developit": "^1.1.1",
44+
"exports-loader": "^0.6.4",
45+
"fast-async": "^6.3.0",
46+
"html-webpack-plugin": "^2.30.1",
47+
"microbundle": "^0.2.4",
48+
"mocha": "^4.1.0",
49+
"mocha-chrome": "^1.0.3",
50+
"mocha-puppeteer": "^0.13.0",
51+
"serve": "^6.4.4",
52+
"style-loader": "^0.19.1",
53+
"webpack": "^3.10.0",
54+
"webpack-dev-server": "^2.10.1"
55+
},
56+
"dependencies": {
57+
"loader-utils": "^1.1.0"
58+
}
59+
}

src/index.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import path from 'path';
2+
import loaderUtils from 'loader-utils';
3+
4+
import NodeTargetPlugin from 'webpack/lib/node/NodeTargetPlugin';
5+
import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin';
6+
import WebWorkerTemplatePlugin from 'webpack/lib/webworker/WebWorkerTemplatePlugin';
7+
8+
export default function loader() {}
9+
10+
const CACHE = {};
11+
12+
loader.pitch = function(request) {
13+
this.cacheable(false);
14+
15+
const options = loaderUtils.getOptions(this) || {};
16+
17+
const cb = this.async();
18+
19+
const filename = loaderUtils.interpolateName(this, `${options.name || '[hash]'}.worker.js`, {
20+
context: options.context || this.options.context,
21+
regExp: options.regExp
22+
});
23+
24+
const worker = {};
25+
26+
worker.options = {
27+
filename,
28+
chunkFilename: `[id].${filename}`,
29+
namedChunkFilename: null
30+
};
31+
32+
worker.compiler = this._compilation.createChildCompiler('worker', worker.options);
33+
34+
worker.compiler.apply(new WebWorkerTemplatePlugin(worker.options));
35+
36+
if (this.target!=='webworker' && this.target!=='web') {
37+
worker.compiler.apply(new NodeTargetPlugin());
38+
}
39+
40+
worker.compiler.apply(new SingleEntryPlugin(this.context, `!!${path.resolve(__dirname, 'rpc-worker-loader.js')}!${request}`, 'main'));
41+
42+
const subCache = `subcache ${__dirname} ${request}`;
43+
44+
worker.compiler.plugin('compilation', (compilation, data) => {
45+
if (compilation.cache) {
46+
if (!compilation.cache[subCache]) compilation.cache[subCache] = {};
47+
48+
compilation.cache = compilation.cache[subCache];
49+
}
50+
51+
data.normalModuleFactory.plugin('parser', (parser, options) => {
52+
parser.plugin('export declaration', expr => {
53+
let decl = expr.declaration || expr,
54+
{ compilation, current } = parser.state,
55+
entry = compilation.entries[0].resource;
56+
57+
// only process entry exports
58+
if (current.resource!==entry) return;
59+
60+
let exports = compilation.__workerizeExports || (compilation.__workerizeExports = {});
61+
62+
if (decl.id) {
63+
exports[decl.id.name] = true;
64+
}
65+
else if (decl.declarations) {
66+
for (let i=0; i<decl.declarations.length; i++) {
67+
exports[decl.declarations[i].id.name] = true;
68+
}
69+
}
70+
else {
71+
console.warn('[workerize] unknown export declaration: ', expr);
72+
}
73+
});
74+
});
75+
});
76+
77+
worker.compiler.runAsChild((err, entries, compilation) => {
78+
if (err) return cb(err);
79+
80+
if (entries[0]) {
81+
worker.file = entries[0].files[0];
82+
83+
let contents = compilation.assets[worker.file].source();
84+
let exports = Object.keys(CACHE[worker.file] = compilation.__workerizeExports || CACHE[worker.file] || {});
85+
86+
// console.log('Workerized exports: ', exports.join(', '));
87+
88+
if (options.inline) {
89+
worker.url = `URL.createObjectURL(new Blob([${JSON.stringify(contents)}]))`;
90+
}
91+
else {
92+
worker.url = `__webpack_public_path__ + ${JSON.stringify(worker.file)}`;
93+
}
94+
95+
if (options.fallback === false) {
96+
delete this._compilation.assets[worker.file];
97+
}
98+
99+
return cb(null, `
100+
var addMethods = require(${loaderUtils.stringifyRequest(this, path.resolve(__dirname, 'rpc-wrapper.js'))})
101+
var methods = ${JSON.stringify(exports)}
102+
module.exports = function() {
103+
var w = new Worker(${worker.url}, { name: ${JSON.stringify(filename)} })
104+
addMethods(w, methods)
105+
${ options.ready ? 'w.ready = new Promise(function(r) { w.addEventListener("ready", function(){ r(w) }) })' : '' }
106+
return w
107+
}
108+
`);
109+
}
110+
111+
return cb(null, null);
112+
});
113+
};

src/rpc-worker-loader.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* global __webpack_exports__ */
2+
3+
function workerSetup() {
4+
addEventListener('message', (e) => {
5+
let { type, method, id, params } = e.data, f, p;
6+
if (type==='RPC' && method) {
7+
if ((f = __webpack_exports__[method])) {
8+
p = Promise.resolve().then( () => f.apply(__webpack_exports__, params) );
9+
}
10+
else {
11+
p = Promise.reject('No such method');
12+
}
13+
p.then(
14+
result => {
15+
postMessage({ type: 'RPC', id, result });
16+
},
17+
error => {
18+
postMessage({ type: 'RPC', id, error });
19+
});
20+
}
21+
});
22+
postMessage({ type: 'RPC', method: 'ready' });
23+
}
24+
25+
const workerScript = '\n' + Function.prototype.toString.call(workerSetup).replace(/(^.*\{|\}.*$|\n\s*)/g, '');
26+
27+
export default function rpcWorkerLoader(content) {
28+
return content + workerScript;
29+
}

src/rpc-wrapper.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export default function addMethods(worker, methods) {
2+
let c = 0;
3+
let callbacks = {};
4+
worker.addEventListener('message', (e) => {
5+
let d = e.data;
6+
if (d.type!=='RPC') return;
7+
if (d.id) {
8+
let f = callbacks[d.id];
9+
if (f) {
10+
delete callbacks[d.id];
11+
if (d.error) f[1](d.error);
12+
else f[0](d.result);
13+
}
14+
}
15+
else {
16+
let evt = document.createEvent('Event');
17+
evt.initEvent(d.method);
18+
evt.data = d.params;
19+
worker.dispatchEvent(evt);
20+
}
21+
});
22+
methods.forEach( method => {
23+
worker[method] = (...params) => new Promise( (a, b) => {
24+
let id = ++c;
25+
callbacks[id] = [a, b];
26+
worker.postMessage({ type: 'RPC', id, method, params });
27+
});
28+
});
29+
}

0 commit comments

Comments
 (0)