Retool: esbuild + tap

Switched to node-tap

esbuild for bundling

pnpm for packaging

sr.ht for source code
master
Jason Staten 3 years ago
parent 086f5862cc
commit 933d7bb063

@ -0,0 +1,20 @@
image: archlinux
packages:
- nodejs-n
- pnpm
sources:
- https://git.sr.ht/~statianzo/pmrpc
secrets:
- 01f84277-3abe-4ed1-9d0d-d66b49bfcfe7
- 5756e542-1a3e-4c27-ac4c-4820cff2b965
- 576364b7-4764-4794-a536-1a3357fb222b
environment:
CI: "true"
tasks:
- setup: |
sudo n stable
cd pmrpc
pnpm i
- validate: |
cd pmrpc
pnpm test

3
.gitignore vendored

@ -2,3 +2,6 @@ lib
node_modules
.rpt2_cache
*.tgz
.nyc_output
coverage
.pnpm-lock.yaml

@ -1,11 +1,12 @@
{
"name": "@statianzo/pmrpc",
"main": "lib/index.js",
"module": "lib/index.esm.js",
"module": "lib/index.mjs",
"unpkg": "lib/index.iife.js",
"types": "lib/index.d.ts",
"version": "0.2.4",
"description": "JSON RPC over PostMessage",
"repository": "https://github.com/statianzo/pmrpc",
"repository": "https://git.sr.ht/~statianzo/pmrpc",
"author": "statianzo",
"license": "ISC",
"files": [
@ -17,32 +18,27 @@
"lib/JsonRpc.d.ts"
],
"scripts": {
"prepublishOnly": "yarn bundle",
"bundle": "rollup -c",
"test": "jest --verbose",
"test:unit": "jest --testPathPattern \\.test\\.",
"test:spec": "jest --testPathPattern \\.spec\\."
"prepublishOnly": "pnpm run build",
"build": "pnpm run build:bundle && pnpm run build:types",
"build:bundle": "node ./scripts/bundle.mjs",
"build:types": "tsc",
"test": "tap --node-arg=--require=esbuild-register"
},
"devDependencies": {
"@types/jest": "^23.3.1",
"@types/jsdom": "^16.2.13",
"@types/puppeteer": "^1.6.4",
"@types/rollup": "^0.54.0",
"jest": "^23.5.0",
"@types/sinon": "^10.0.6",
"@types/tap": "^15.0.5",
"esbuild": "^0.13.15",
"esbuild-register": "^3.1.2",
"jsdom": "^18.1.1",
"puppeteer": "^1.8.0",
"rollup": "^0.65.2",
"rollup-plugin-typescript2": "^0.17.0",
"ts-jest": "^23.1.4",
"typescript": "^3.0.3"
"sinon": "^12.0.1",
"tap": "^15.1.2",
"typescript": "^4.5.2"
},
"jest": {
"transform": {
"^.+\\.(ts|js)$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"js",
"json"
]
"tap": {
"reporter": "spec",
"branches": 95
}
}

File diff suppressed because it is too large Load Diff

@ -1,21 +0,0 @@
const typescript = require('rollup-plugin-typescript2');
const pkg = require('./package.json');
export default {
input: 'src/index.ts',
output: [
{file: pkg.main, format: 'cjs'},
{file: pkg.module, format: 'es'},
{file: 'lib/index.umd.js', format: 'umd', name: 'JsonRpc'}
],
plugins: [
typescript({
tsConfigOverride: {
compilerOptions: {
declaration: true,
allowJs: false,
},
},
}),
],
};

@ -0,0 +1,16 @@
/*!istanbul ignore file */
const { resolve } = require("path");
const { build } = require("esbuild");
const buildIndex = (config) =>
build({
bundle: true,
sourcemap: true,
entryPoints: [resolve(__dirname, "../src/index.ts")],
target: ["es2017"],
globalName: "JsonRpc",
...config,
});
module.exports = buildIndex;

@ -0,0 +1,25 @@
import buildIndex from './buildIndex.js';
import {createRequire} from 'module';
import { resolve } from 'path';
import {fileURLToPath} from 'url';
const require = createRequire(import.meta.url)
const pkg = require("../package.json");
const __dirname = resolve(fileURLToPath(import.meta.url), '..');
const run = () =>
Promise.all([
buildIndex({
format: "cjs",
outfile: resolve(__dirname, "..", pkg.main),
}),
buildIndex({
format: "esm",
outfile: resolve(__dirname, "..", pkg.module),
}),
buildIndex({
format: "iife",
outfile: resolve(__dirname, "..", pkg.unpkg),
}),
]);
run();

@ -1,35 +1,62 @@
import * as puppeteer from 'puppeteer';
import {rollup} from 'rollup';
import config from '../rollup.config';
const launchArgs = process.env.CI === 'true' ? ['--no-sandbox'] : [];
import * as puppeteer from "puppeteer";
import { test, beforeEach, afterEach } from "tap";
import buildIndex from "../scripts/buildIndex";
const launchArgs = process.env.CI === "true" ? ["--no-sandbox"] : [];
let browser: puppeteer.Browser;
let page: puppeteer.Page;
beforeAll(async () => {
const bundle = await rollup({
input: config['input'],
plugins: config['plugins'],
beforeEach(async () => {
const result = await buildIndex({
write: false,
sourcemap: false,
format: "esm",
});
const [file] = result.outputFiles;
const code = Buffer.from(file.contents);
browser = await puppeteer.launch({
args: [...launchArgs, "--disable-web-security"],
});
const {code} = await bundle.generate({format: 'iife', name: 'JsonRpc'});
browser = await puppeteer.launch({args: launchArgs});
page = await browser.newPage();
await page.addScriptTag({content: code});
await page.setContent(`<iframe sandbox="allow-scripts"></iframe>`);
await page.evaluate(`
await page.setRequestInterception(true);
page.on("request", (req) => {
if (req.url() === "https://fake.test/") {
return req.respond({
body: '<iframe sandbox="allow-scripts"></iframe>',
contentType: "text/html",
});
}
if (req.url().match(/pmrpc.js$/)) {
return req.respond({
body: code,
contentType: "application/javascript",
});
}
req.abort();
});
await page.goto("https://fake.test");
await page.addScriptTag({
type: "module",
content: `
import JsonRpc from './pmrpc.js'
let resolve;
let reject;
let rpc;
const handshakePromise = new Promise(r => resolve = r);
window.addEventListener('message', e => {
self.handshakePromise = handshakePromise;
self.addEventListener('message', e => {
const [port] = e.ports;
port.start();
rpc = new JsonRpc({source: port, destination: port});
self.rpc = new JsonRpc({source: port, destination: port});
resolve();
});
`);
`,
});
const [, frame] = page.frames();
await frame.addScriptTag({content: code});
await frame.evaluate(`
await frame.addScriptTag({
type: "module",
content: `
import JsonRpc from './pmrpc.js';
const {port1, port2} = new MessageChannel();
const rpc = new JsonRpc({
@ -42,21 +69,23 @@ beforeAll(async () => {
port1.start();
window.top.postMessage({}, '*', [port2]);
`);
await page.evaluate(`handshakePromise`);
`,
});
// await page.waitFor(100);
await page.evaluate(`window.handshakePromise`);
});
afterAll(async () => {
afterEach(async () => {
(await browser) && browser.close();
});
it('calls between iframes', async () => {
const result = await page.evaluate(`rpc.call('greet', 'Alice')`);
expect(result).toEqual('Hello, Alice');
test("calls between iframes", async (t) => {
const result = await page.evaluate(`self.rpc.call('greet', 'Alice')`);
t.same(result, "Hello, Alice");
});
it('fails on missing', async () => {
await expect(page.evaluate(`rpc.call('missing')`)).rejects.toMatchObject({
test('fails on missing', async t => {
await t.rejects(page.evaluate(`self.rpc.call('missing')`, {
message: /method not found/i,
});
}));
});

@ -1,4 +1,6 @@
import JsonRpc, {ErrorCodes} from './JsonRpc';
import {test} from 'tap';
import {spy} from 'sinon';
const defer = (ms: number, val?: any) =>
new Promise(resolve => setTimeout(() => resolve(val), ms));
@ -10,7 +12,7 @@ const buildRequest = (method: string, params?: any[] | object) => ({
params,
});
describe('handling requests', () => {
test('handling requests', async ({beforeEach, test}) => {
let rpc: any;
beforeEach(() => {
@ -19,7 +21,7 @@ describe('handling requests', () => {
hello: (name: string) => `goodbye ${name}`,
canVote: (voter: {age: number}) => voter.age > 18,
deferred: () => Promise.resolve('deferredResult'),
spy: jest.fn().mockImplementation(() => 'mock'),
spy: () => 'mock',
blowUp: () => {
throw Error('BOOM!');
},
@ -27,56 +29,56 @@ describe('handling requests', () => {
});
});
it('handles array params', async () => {
test('handles array params', async t => {
const request = buildRequest('hello', ['Alice']);
const response = await rpc.handleRequest(request);
expect(response.jsonrpc).toEqual('2.0');
expect(response.id).toEqual(request.id);
expect(response.result).toEqual('goodbye Alice');
t.same(response.jsonrpc, '2.0');
t.same(response.id, request.id);
t.same(response.result, 'goodbye Alice');
});
it('handles object params', async () => {
test('handles object params', async t => {
const request = buildRequest('canVote', {age: 22});
const response = await rpc.handleRequest(request);
expect(response.jsonrpc).toEqual('2.0');
expect(response.id).toEqual(request.id);
expect(response.result).toEqual(true);
t.same(response.jsonrpc, '2.0');
t.same(response.id, request.id);
t.same(response.result, true);
});
it('waits on promise results', async () => {
test('waits on promise results', async t => {
const request = buildRequest('deferred');
const response = await rpc.handleRequest(request);
expect(response.result).toEqual('deferredResult');
t.same(response.result, 'deferredResult');
});
it('errors on missing method', async () => {
test('errors on missing method', async t => {
const request = buildRequest('missing');
const response = await rpc.handleRequest(request);
expect(response.id).toEqual(request.id);
expect(response.error.code).toEqual(ErrorCodes.MethodNotFound);
t.same(response.id, request.id);
t.same(response.error.code, ErrorCodes.MethodNotFound);
});
it('errors on throwing method', async () => {
test('errors on throwing method', async t => {
const request = buildRequest('blowUp');
const response = await rpc.handleRequest(request);
expect(response.id).toEqual(request.id);
expect(response.error.code).toEqual(ErrorCodes.InternalError);
t.same(response.id, request.id);
t.same(response.error.code, ErrorCodes.InternalError);
});
});
describe('invalid messages', () => {
test('invalid messages', async ({beforeEach, test}) => {
let rpc: any;
let messageEvent : any;
beforeEach(() => {
rpc = new JsonRpc({
methods: {
spy: jest.fn().mockImplementation(() => 'mock'),
spy: spy(),
},
});
messageEvent = {
@ -85,33 +87,36 @@ describe('invalid messages', () => {
};
});
it('ignores messages missing data', async () => {
test('ignores messages missing data', async t => {
delete messageEvent.data;
await rpc.handleMessage(messageEvent);
expect(rpc.methods.spy).not.toHaveBeenCalled();
t.notOk(rpc.methods.spy.called);
});
it('ignores requests missing jsonrpc', async () => {
test('ignores requests missing jsonrpc', async t => {
delete messageEvent.data.jsonrpc;
await rpc.handleMessage(messageEvent);
expect(rpc.methods.spy).not.toHaveBeenCalled();
t.notOk(rpc.methods.spy.called);
});
it('ignores requests with wrong origin', async () => {
test('ignores requests with wrong origin', async t => {
rpc.origin = 'http://example.com';
messageEvent.origin = 'http://fake.com';
await rpc.handleMessage(messageEvent);
expect(rpc.methods.spy).not.toHaveBeenCalled();
t.notOk(rpc.methods.spy.called);
});
});
describe('mounted', () => {
test('mounted', async ({beforeEach, afterEach, test}) => {
let frame1: HTMLIFrameElement;
let frame2: HTMLIFrameElement;
let rpc1: JsonRpc;
let rpc2: JsonRpc;
let document: Document;
beforeEach(() => {
beforeEach(async () => {
const {JSDOM} = await import('jsdom');
document = new JSDOM().window.document;
frame1 = document.createElement('iframe');
frame2 = document.createElement('iframe');
document.body.appendChild(frame1);
@ -140,25 +145,53 @@ describe('mounted', () => {
document.body.removeChild(frame2);
});
it('communicates between iframes', async () => {
test('communicates between iframes', async t => {
const result = await rpc1.call('greet', ['Alice']);
expect(result).toEqual(`Hello, Alice`);
t.same(result, `Hello, Alice`);
});
it('propagates errors between iframes', async () => {
await expect(rpc1.apply('explode')).rejects.toMatchObject({
test('propagates errors between iframes', async t => {
await t.rejects(rpc1.apply('explode'), {
code: ErrorCodes.InternalError,
});
});
it('propagates missing between iframes', async () => {
await expect(rpc1.apply('missing')).rejects.toMatchObject({
test('propagates missing between iframes', async t => {
await t.rejects(rpc1.apply('missing'), {
code: ErrorCodes.MethodNotFound,
});
});
test('unmounts', async t => {
rpc1.unmount();
});
test('ignores unexpected responses', async t => {
const {contentWindow} = frame1;
contentWindow?.postMessage({jsonrpc: '2.0', result: {}}, '*');
await defer(0);
});
test('ignores responding without destination', async t => {
delete rpc2['destination'];
const result = await Promise.race([
defer(10, 'timeout'),
rpc1.call('greet', 'Norah'),
]);
t.same(result, 'timeout');
})
});
it('errors when applying without a destination', () => {
const rpc = new JsonRpc({});
expect(() => rpc.apply('yo')).toThrow('Attempted to apply with no destination');
test('errors when applying without a destination', async t => {
const rpc = new JsonRpc();
t.throws(() => rpc.apply('yo'));
});
test('does not pass origin to postMessage when not window', async t => {
const postMessage = spy();
const destination = {postMessage} as unknown as MessageEventSource;
const rpc = new JsonRpc({destination});
rpc.call('x');
t.notOk(postMessage.calledWith(null, undefined))
});

@ -110,14 +110,14 @@ class JsonRpc {
return this.apply(method, rest);
}
mount(source: EventTarget) {
mount(source: JsonRpcSource) {
this.source = source;
source.addEventListener('message', this.handleMessage);
source.addEventListener('message', this.handleMessage as EventListener);
}
unmount() {
this.source &&
this.source.removeEventListener('message', this.handleMessage);
this.source.removeEventListener('message', this.handleMessage as EventListener);
}
private handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {

@ -1,11 +1,15 @@
{
"compilerOptions": {
"esModuleInterop": true,
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"strict": true,
"strictFunctionTypes": false,
"target": "es5",
"module": "esnext",
"moduleResolution": "Node",
"lib": ["es5", "es2015", "es2016", "dom"]
}
"skipLibCheck": true,
"lib": ["esnext", "dom"],
"outDir": "lib",
"rootDir": "./src"
},
"include": ["./src"],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save