Skip to content

Commit

Permalink
update packaging and deps
Browse files Browse the repository at this point in the history
  • Loading branch information
developit committed Aug 29, 2022
1 parent 962337e commit e0376d7
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 100 deletions.
7 changes: 5 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[{*.json,*.md,.*rc,*.yml,*.txt}]
[{*.json,.*rc,*.yml}]
indent_style = space
indent_size = 2
insert_final_newline = false
insert_final_newline = false

[*.md]
trim_trailing_whitespace = false
10 changes: 10 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// eslint-disable-next-line
module.exports = {
env: {
node: true,
browser: true,
es2021: true,
},
extends: ['eslint-config-developit', 'prettier', 'eslint:recommended'],
ignorePatterns: ['**/dist/**'],
};
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,23 @@
<img src="https://i.imgur.com/Xqla6Ia.jpg" width="1100">
</p>


# jsdom-worker

> _Lets you use Web Workers in Jest!_
This is an experimental implementation of the Web Worker API (specifically Dedicated Worker) for JSDOM.

It does not currently do any real threading, rather it implements the `Worker` interface but all work is done in the current thread. `jsdom-worker` runs wherever JSDOM runs, and does not require Node.
It does not currently do any real threading, rather it implements the `Worker` interface but all work is done in the current thread. `jsdom-worker` runs wherever JSDOM runs, and does not require Node.

It supports both "inline" _(created via Blob)_ and standard _(loaded via URL)_ workers.

> **Hot Take:** this module likely works in the browser, where it could act as a simple inline worker "poorlyfill".
<a href="https://www.npmjs.org/package/jsdom-worker"><img src="https://img.shields.io/npm/v/jsdom-worker.svg?style=flat" alt="npm"></a> <a href="https://travis-ci.org/developit/jsdom-worker"><img src="https://travis-ci.org/developit/jsdom-worker.svg?branch=master" alt="travis"></a>


## Why?

Jest uses a JSDOM environment by default, which means it doesn't support Workers. This means it is impossible to test code that requires both NodeJS functionality _and_ Web Workers. `jsdom-worker` implements enough of the Worker spec that it is now possible to do so.
Jest uses a JSDOM environment by default, which means it doesn't support Workers. This means it is impossible to test code that requires both NodeJS functionality _and_ Web Workers. `jsdom-worker` implements enough of the Worker spec that it is now possible to do so.

## Installation

Expand All @@ -29,13 +27,13 @@ Jest uses a JSDOM environment by default, which means it doesn't support Workers
## Example

```js
import 'jsdom-global/register'
import 'jsdom-worker'
import 'jsdom-global/register';
import 'jsdom-worker';

let code = `onmessage = e => postMessage(e.data*2)`
let worker = new Worker(URL.createObjectURL(new Blob([code])))
worker.onmessage = console.log
worker.postMessage(5) // 10
let code = `onmessage = e => postMessage(e.data*2)`;
let worker = new Worker(URL.createObjectURL(new Blob([code])));
worker.onmessage = console.log;
worker.postMessage(5); // 10
```

## Usage with Jest
Expand Down
131 changes: 78 additions & 53 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,55 +1,80 @@
{
"name": "jsdom-worker",
"version": "0.2.1",
"description": "Experimental Web Worker API implementation for JSDOM.",
"main": "dist/jsdom-worker.js",
"module": "dist/jsdom-worker.module.js",
"unpkg": "dist/jsdom-worker.umd.js",
"scripts": {
"build": "microbundle --external all",
"test": "eslint src test && npm run -s build && jest",
"prepare": "npm run -s build && npm t",
"release": "npm t && git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push && git push --tags && npm publish"
},
"repository": "developit/jsdom-worker",
"babel": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "8"
}
}
]
]
},
"keywords": [
"jsdom",
"web worker"
],
"eslintConfig": {
"extends": "eslint-config-developit"
},
"author": "Jason Miller <[email protected]> (http://jasonformat.com)",
"license": "MIT",
"files": [
"dist"
],
"devDependencies": {
"@babel/preset-env": "^7.10.2",
"babel-jest": "^26.0.1",
"eslint": "^7.2.0",
"eslint-config-developit": "^1.1.1",
"jest": "^26.0.1",
"microbundle": "^0.12.1",
"node-fetch": "^2.6.0"
},
"peerDependencies": {
"node-fetch": "*"
},
"dependencies": {
"mitt": "^1.1.3",
"uuid-v4": "^0.1.0"
}
"name": "jsdom-worker",
"version": "0.2.1",
"description": "Experimental Web Worker API implementation for JSDOM.",
"main": "./dist/jsdom-worker.js",
"module": "./dist/jsdom-worker.mjs",
"unpkg": "./dist/jsdom-worker.umd.js",
"exports": {
"import": "./dist/jsdom-worker.mjs",
"default": "./dist/jsdom-worker.js"
},
"scripts": {
"build": "microbundle --external all -f esm,cjs,umd",
"test": "eslint '{src,test}/**/*.js' && npm run -s build && jest",
"prepare": "npm run -s build && npm t",
"release": "npm t && git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push && git push --tags && npm publish"
},
"repository": "developit/jsdom-worker",
"babel": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "12"
}
}
]
]
},
"jest": {
"testEnvironment": "jsdom"
},
"keywords": [
"jsdom",
"web worker"
],
"author": "Jason Miller <[email protected]> (http://jasonformat.com)",
"license": "MIT",
"files": [
"dist"
],
"prettier": {
"useTabs": true,
"arrowParens": "avoid",
"singleQuote": true
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,yml}": [
"prettier --write"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"devDependencies": {
"@babel/preset-env": "^7.10.2",
"@types/jest": "^28.1.8",
"babel-jest": "^29.0.1",
"eslint": "^7.2.0",
"eslint-config-developit": "^1.1.1",
"eslint-config-prettier": "^8.5.0",
"husky": "^8.0.1",
"jest": "^29.0.1",
"jest-environment-jsdom": "^29.0.1",
"lint-staged": "^13.0.3",
"microbundle": "^0.15.1",
"node-fetch": "^2.6.7",
"prettier": "^2.7.1"
},
"peerDependencies": {
"node-fetch": "*"
},
"dependencies": {
"mitt": "^3.0.0",
"uuid-v4": "^0.1.0"
}
}
90 changes: 62 additions & 28 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,83 @@ import mitt from 'mitt';
import uuid from 'uuid-v4';
import fetch, { Response } from 'node-fetch';

if (!global.URL) global.URL = {};
if (!global.URL.$$objects) {
global.URL.$$objects = new Map();
global.URL.createObjectURL = blob => {
// eslint-disable-next-line no-undef

const self = /** @type {globalThis} */ (
typeof global === 'object'
? global
: typeof globalThis === 'object'
? globalThis
: this
);

// @ts-ignore-next-line
if (!self.URL) self.URL = {};

// @ts-ignore
let objects = /** @type {Map<any, string>} */ (self.URL.$$objects);

if (!objects) {
objects = new Map();
// @ts-ignore
self.URL.$$objects = objects;
self.URL.createObjectURL = blob => {
let id = uuid();
global.URL.$$objects[id] = blob;
objects[id] = blob;
return `blob:http://localhost/${id}`;
};
}

if (!global.fetch || !global.fetch.jsdomWorker) {
let oldFetch = global.fetch || fetch;
global.fetch = function(url, opts) {
if (url.match(/^blob:/)) {
return new Promise( (resolve, reject) => {
if (!self.fetch || !('jsdomWorker' in self.fetch)) {
let oldFetch = self.fetch || fetch;
self.fetch = function (url, opts) {
let _url = typeof url === 'object' ? url.url || url.href : url;
if (_url.match(/^blob:/)) {
return new Promise((resolve, reject) => {
let fr = new FileReader();
fr.onload = () => {
let Res = global.Response || Response;
let Res = self.Response || Response;
resolve(new Res(fr.result, { status: 200, statusText: 'OK' }));
};
fr.onerror = () => {
reject(fr.error);
};
let id = url.match(/[^/]+$/)[0];
fr.readAsText(global.URL.$$objects[id]);
let id = _url.match(/[^/]+$/)[0];
fr.readAsText(objects[id]);
});
}
return oldFetch.call(this, url, opts);
};
global.fetch.jsdomWorker = true;
Object.defineProperty(self.fetch, 'jsdomWorker', {
configurable: true,
value: true,
});
}

if (!global.document) {
global.document = {};
}
// @ts-ignore
if (!self.document) self.document = {};

function Event(type) { this.type = type; }
function Event(type) {
this.type = type;
}
Event.prototype.initEvent = Object;
if (!global.document.createEvent) {
global.document.createEvent = function(type) {
if (!self.document.createEvent) {
self.document.createEvent = function (type) {
let Ctor = global[type] || Event;
return new Ctor(type);
};
}

// @ts-ignore
self.Worker = Worker;

global.Worker = function Worker(url) {
/**
* @param {string | URL} url
* @param {object} [options = {}]
*/
function Worker(url, options) {
let getScopeVar;
/** @type {any[] | null} */
let messageQueue = [];
let inside = mitt();
let outside = mitt();
Expand All @@ -62,8 +91,8 @@ global.Worker = function Worker(url) {
postMessage(data) {
outside.emit('message', { data });
},
fetch: global.fetch,
importScripts() {}
fetch: self.fetch,
importScripts() {},
};
inside.on('message', e => {
if (terminated) return;
Expand All @@ -76,30 +105,35 @@ global.Worker = function Worker(url) {
outside.on('message', e => {
if (this.onmessage) this.onmessage(e);
});
this.onmessage = null;
this.postMessage = data => {
if (terminated) return;
if (messageQueue != null) messageQueue.push(data);
else inside.emit('message', { data });
};
this.terminate = () => {
console.warn('Worker.prototype.terminate() not supported in jsdom-worker.');
messageQueue = null;
terminated = true;
messageQueue = null;
};
global.fetch(url)
self
.fetch(url)
.then(r => r.text())
.then(code => {
let vars = 'var self=this,global=self';
for (let k in scope) vars += `,${k}=self.${k}`;
getScopeVar = Function(
vars + ';\n' + code + '\nreturn function(n){return n=="onmessage"?onmessage:null;}'
vars +
';\n' +
code +
'\nreturn function(n){return n=="onmessage"?onmessage:null;}'
).call(scope);
let q = messageQueue;
messageQueue = null;
q.forEach(this.postMessage);
if (q) q.forEach(this.postMessage);
})
.catch(e => {
outside.emit('error', e);
console.error(e);
});
};
}
13 changes: 8 additions & 5 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import 'jsdom-worker';
import fs from 'fs';
import path from 'path';

const sleep = t => new Promise( r => { setTimeout(r, t); });
const sleep = t =>
new Promise(r => {
setTimeout(r, t);
});

describe('jsdom-worker', () => {
it('should work', async () => {
Expand All @@ -16,11 +19,11 @@ describe('jsdom-worker', () => {
});

it('should work with importScripts', async () => {
const mod = fs.readFileSync(path.join(__dirname, './module.js'));
const code = fs.readFileSync(path.join(__dirname, './worker.js'));
const mod = fs.readFileSync(path.join(__dirname, './module.js'), 'utf-8');
const code = fs.readFileSync(path.join(__dirname, './worker.js'), 'utf-8');
const worker = new Worker(URL.createObjectURL(new Blob([mod + code])));
worker.onmessage = jest.fn();
worker.postMessage();
worker.postMessage(0);
await sleep(10);
expect(worker.onmessage).toHaveBeenCalledWith({ data: 'test' });
});
Expand All @@ -30,7 +33,7 @@ describe('jsdom-worker', () => {
const code = `(function(n){ onmessage = e => { postMessage(n) } })(${n})`;
const worker = new Worker(URL.createObjectURL(new Blob([code])));
worker.onmessage = jest.fn();
worker.postMessage();
worker.postMessage({ hi: 'bye' });
await sleep(10);
expect(worker.onmessage).toHaveBeenCalledWith({ data: n });
});
Expand Down
Loading

0 comments on commit e0376d7

Please sign in to comment.