1
0
Fork 0

Initial prototype

pull/1/head
Ambrose Chua 2021-09-09 23:47:04 +08:00
commit c600d7182b
18 changed files with 5823 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

1
.npmrc Normal file
View File

@ -0,0 +1 @@
detect_chromedriver_version=true

6
fixtures/all.js Normal file
View File

@ -0,0 +1,6 @@
import request from './request.js';
const tests = {
...request,
};
export default tests;

17
fixtures/index-node.js Normal file
View File

@ -0,0 +1,17 @@
import process from 'node:process';
import fetch, {Request, Response, Headers} from 'node-fetch';
import run from './run.js';
global.Request = Request;
global.Response = Response;
global.Headers = Headers;
global.fetch = fetch;
async function main(file) {
const tests = await import('./' + file + '.js');
const result = await run.default(tests.default);
process.send(result);
}
main(process.argv[2]);

9
fixtures/index.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>blank</title>
</head>
<body></body>
</html>

3
fixtures/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

14
fixtures/request.js Normal file
View File

@ -0,0 +1,14 @@
/* eslint-disable no-new */
const tests = {
request: {
async invalidURL() {
try {
new Request('http://example.com%');
} catch ({name, message}) {
return {name, message};
}
},
},
};
export default tests;
/* eslint-enable no-new */

13
fixtures/run.js Normal file
View File

@ -0,0 +1,13 @@
export default async function run(tests) {
const results = {};
/* eslint-disable guard-for-in, no-await-in-loop */
for (const sectionKey in tests) {
results[sectionKey] = {};
for (const fnKey in tests[sectionKey]) {
results[sectionKey][fnKey] = await tests[sectionKey][fnKey]();
}
}
/* eslint-enable guard-for-in, no-await-in-loop */
return results;
}

5365
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "fetch-compare",
"version": "0.1.0",
"description": "Compare various implementations of the WHATWG fetch API",
"author": "Ambrose Chua",
"license": "MIT",
"scripts": {
"start": "node --loader ts-node/esm src/index.ts",
"type-check": "tsc --pretty --noEmit",
"format": "prettier --ignore-path ../.gitignore --write .",
"lint": "xo"
},
"dependencies": {
"@swc/core": "^1.2.85",
"@wdio/logger": "^7.7.0",
"chromedriver": "^93.0.1",
"cli-table3": "^0.6.0",
"deno-prebuilt": "^1.0.4",
"geckodriver": "^2.0.4",
"get-port": "^5.1.1",
"koa": "^2.13.1",
"koa-files": "^1.0.3",
"node-fetch": "^3.0.0",
"tcp-port-used": "^1.0.2",
"ts-node": "^10.2.1",
"webdriver": "^7.12.2"
},
"devDependencies": {
"@types/koa": "^2.13.4",
"@types/node": "^16.9.0",
"prettier": "^2.4.0",
"typescript": "^4.4.2",
"xo": "^0.44.0"
},
"prettier": {
"semi": true,
"useTabs": true,
"trailingComma": "all",
"singleQuote": true,
"bracketSpacing": false,
"jsxBracketSameLine": false
},
"xo": {
"prettier": true,
"globals": ["Request", "Response", "Headers", "fetch"]
}
}

60
src/browser-drivers.ts Normal file
View File

@ -0,0 +1,60 @@
// @ts-expect-error: Missing types
import {path as chromepath} from 'chromedriver';
import {path as firefoxpath} from 'geckodriver';
import {BrowserDriver} from './types';
export const chrome: BrowserDriver = {
path: chromepath as string,
args: (port: number, logLevel = 'warn') => {
const logLevelArg = {
trace: 'ALL',
debug: 'DEBUG',
info: 'INFO',
warn: 'WARNING',
error: 'SEVERE',
silent: 'OFF',
}[logLevel];
if (!logLevelArg) {
throw new Error('invalid log level');
}
return [`--port=${port}`, `--log-level=${logLevelArg}`];
},
};
export const firefox: BrowserDriver = {
path: firefoxpath,
args: (port: number, logLevel = 'warn') => {
const logLevelArg = {
trace: 'trace',
debug: 'debug',
info: 'info',
warn: 'warn',
error: 'error',
silent: 'fatal',
}[logLevel];
if (!logLevelArg) {
throw new Error('invalid log level');
}
return ['--port', `${port}`, '--log', `${logLevelArg}`];
},
};
export const safari: BrowserDriver = {
path: '/usr/bin/safaridriver',
args: (port: number, logLevel = 'warn') => {
const logLevelArg = {
trace: '',
debug: '',
info: '',
warn: '',
error: '',
silent: '',
}[logLevel];
if (!logLevelArg) {
throw new Error('invalid log level');
}
return ['-p', `${port}`, '-l', `${logLevelArg}`];
},
};

95
src/index.ts Normal file
View File

@ -0,0 +1,95 @@
import logger from '@wdio/logger';
import Table from 'cli-table3';
import {Platform, Result, Context} from './types';
import PlatformBrowser from './platform-browser';
import PlatformNode from './platform-node';
import PlatformDeno from './platform-deno';
import {chrome, firefox, safari} from './browser-drivers';
import {FixturesServer} from './server';
const log = logger('fetch-compare');
logger.setLevel('fetch-compare', 'info');
const silent = true;
const platforms: Record<string, Platform> = {
chrome: new PlatformBrowser(chrome, 'chrome', silent),
firefox: new PlatformBrowser(firefox, 'firefox', silent),
safari: new PlatformBrowser(safari, 'safari', silent),
node: new PlatformNode(),
deno: new PlatformDeno(),
};
function* tabularNames(reference: Result): Iterable<[string, string, string]> {
/* eslint-disable guard-for-in */
for (const groupName in reference) {
const results = reference[groupName];
for (const resultName in results) {
const resultObject = results[resultName];
for (const key in resultObject) {
yield [groupName, resultName, key];
}
}
}
/* eslint-enable guard-for-in */
}
function* tabularResults(results: Record<string, Result>): Iterable<string[]> {
const ref = Object.values(results)[0];
if (!ref) {
return;
}
const names = Object.keys(results);
for (const [group, result, key] of tabularNames(ref)) {
for (const n of names) {
yield [
`${group}:${result}:${key}`,
n,
results[n][group][result][key].toString(),
];
}
}
}
async function run() {
// Set up fixtures server
const fixturesServer = new FixturesServer();
const addr = await fixturesServer.start();
// Shared data among platforms
const ctx: Context = {
fixturesURL: addr.base,
};
const results: Record<string, Result> = {};
// Call each platform
/* eslint-disable guard-for-in, no-await-in-loop */
for (const name in platforms) {
try {
results[name] = await platforms[name].run(ctx, 'all');
log.debug(name, results[name]);
} catch (error: Error) {
log.error(name, error);
}
}
/* eslint-enable guard-for-in, no-await-in-loop */
const table = new Table({
head: ['Test', 'Platform', 'Value'],
chars: {mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''},
});
for (const row of tabularResults(results)) {
table.push(row);
}
console.log(table.toString());
// Tear down fixtures server
await fixturesServer.stop();
}
run().catch((error) => {
log.error(error);
});

81
src/platform-browser.ts Normal file
View File

@ -0,0 +1,81 @@
import cp from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import getPort from 'get-port';
// @ts-expect-error: Missing types
import tcpPortUsed from 'tcp-port-used';
import WebDriver from 'webdriver';
import {Platform, Result, BrowserDriver, Context} from './types.js';
async function start(
driver: BrowserDriver,
silent = false,
): Promise<{child: cp.ChildProcess; port: number}> {
const port = await getPort();
let command = driver.path;
try {
await fs.access(command);
} catch {
command = path.basename(command);
}
const stdout = silent ? 'ignore' : 'inherit';
const child = cp.spawn(command, driver.args(port), {
stdio: ['ignore', stdout, stdout],
});
await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('spawn', resolve);
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
await tcpPortUsed.waitUntilUsed(port, 100, 10_000);
return {child, port};
}
async function stop(child: cp.ChildProcess): Promise<void> {
if (child.exitCode === null) {
child.kill();
}
}
export default class PlatformBrowser implements Platform {
private readonly driver: BrowserDriver;
private readonly name: string;
private readonly silent: boolean;
constructor(driver: BrowserDriver, name: string, silent = false) {
this.driver = driver;
this.name = name;
this.silent = silent;
}
async run(ctx: Context, file: string): Promise<Result> {
const {child, port} = await start(this.driver, this.silent);
const client = await WebDriver.newSession({
logLevel: 'warn',
port,
capabilities: {browserName: this.name},
});
await client.navigateTo(`${ctx.fixturesURL}/index.html`);
const result: Result = (await client.executeScript(
`
async function main(file) {
document.body.style.backgroundColor = '#f00';
const run = await import('./run.js');
const tests = await import('./' + file + '.js');
const result = await run.default(tests.default);
document.body.style.backgroundColor = '#0f0';
return result;
}
return main(arguments[0]);
`,
[file],
)) as Result;
await client.deleteSession();
await stop(child);
return result;
}
}

10
src/platform-deno.ts Normal file
View File

@ -0,0 +1,10 @@
// @ts-expect-error: Missing types
import {binary} from 'deno-prebuilt';
import {Platform, Result} from './types.js';
export default class PlatformDeno implements Platform {
async run(): Promise<Result> {
throw new Error('not implemented');
}
}

21
src/platform-node.ts Normal file
View File

@ -0,0 +1,21 @@
import process from 'node:process';
import cp from 'node:child_process';
import path from 'node:path';
import {Platform, Result} from './types.js';
export default class PlatformNode implements Platform {
async run(ctx: Record<string, any>, file: string): Promise<Result> {
const child = cp.fork(
path.join(process.cwd(), 'fixtures', 'index-node.js'),
[file],
);
const result: Result = await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('message', resolve);
});
child.kill();
return result;
}
}

45
src/server.ts Normal file
View File

@ -0,0 +1,45 @@
import process from 'node:process';
import net from 'node:net';
import path from 'node:path';
import Koa from 'koa';
import files from 'koa-files';
import {ServerAddress} from './types.js';
export class FixturesServer {
private readonly koa: Koa;
private server: net.Server | null = null;
constructor() {
this.koa = new Koa();
this.koa.use(files(path.join(process.cwd(), 'fixtures')));
}
async start(): Promise<ServerAddress> {
return new Promise((resolve, reject) => {
this.server = this.koa.listen(0, 'localhost', () => {
const addr = this.server?.address();
if (typeof addr === 'string' || !addr) {
reject(new Error('invalid address'));
return;
}
resolve({
address: addr.address,
port: addr.port,
base: `http://localhost:${addr.port}`,
});
});
});
}
async stop(): Promise<void> {
return new Promise((resolve) => {
this.server?.close(() => {
resolve();
});
});
}
}

21
src/types.ts Normal file
View File

@ -0,0 +1,21 @@
export type ResultObject = Record<string, string | number>;
export type Result = Record<string, Record<string, ResultObject>>;
export interface Context {
fixturesURL: string;
}
export interface Platform {
run(ctx: Context, file: string): Promise<Result>;
}
export interface BrowserDriver {
path: string;
args: (port: number) => string[];
}
export interface ServerAddress {
address: string;
port: number;
base: string;
}

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2020",
"types": ["node"],
"esModuleInterop": true,
"moduleResolution": "node",
"strict": true
},
"exclude": ["node_modules"],
"ts-node": {
"transpileOnly": true,
"transpiler": "ts-node/transpilers/swc-experimental"
}
}