From 9ae6b70efb9f3d3251820403597085cfa30ace05 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Sat, 18 Apr 2020 02:46:52 -0400 Subject: [PATCH] Fixes for dist builds without injected XMLHttpRequest (#789, #506). --- packages/providers/package.json | 2 +- packages/providers/src.ts/browser-ws.ts | 7 +- packages/web/package.json | 6 +- packages/web/src.ts/browser-geturl.ts | 51 +++++++++++++ packages/web/src.ts/geturl.ts | 95 +++++++++++++++++++++++++ packages/web/src.ts/index.ts | 72 ++++++------------- packages/web/thirdparty.d.ts | 19 +---- rollup.config.js | 48 +++++++++---- 8 files changed, 215 insertions(+), 85 deletions(-) create mode 100644 packages/web/src.ts/browser-geturl.ts create mode 100644 packages/web/src.ts/geturl.ts diff --git a/packages/providers/package.json b/packages/providers/package.json index 777c9adfe5..0aa4d75df6 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -1,7 +1,7 @@ { "author": "Richard Moore ", "browser": { - "./ipc-provider": "./lib/browser-ipc-provider.js", + "./lib/ipc-provider": "./lib/browser-ipc-provider.js", "net": "./lib/browser-net.js", "ws": "./lib/browser-ws.js" }, diff --git a/packages/providers/src.ts/browser-ws.ts b/packages/providers/src.ts/browser-ws.ts index 90956d9c05..9b8e04027f 100644 --- a/packages/providers/src.ts/browser-ws.ts +++ b/packages/providers/src.ts/browser-ws.ts @@ -3,9 +3,12 @@ import { Logger } from "@ethersproject/logger"; import { version } from "./_version"; -let WS = (WebSocket as any); +let WS: any = null; -if (WS == null) { +try { + WS = (WebSocket as any); + if (WS == null) { throw new Error("inject please"); } +} catch (error) { const logger = new Logger(version); WS = function() { logger.throwError("WebSockets not supported in this environment", Logger.errors.UNSUPPORTED_OPERATION, { diff --git a/packages/web/package.json b/packages/web/package.json index 32e50ba016..447b5c4c7b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,11 +1,13 @@ { "author": "Richard Moore ", + "browser": { + "./lib/geturl": "./lib/browser-geturl.js" + }, "dependencies": { "@ethersproject/base64": ">=5.0.0-beta.126", "@ethersproject/logger": ">=5.0.0-beta.129", "@ethersproject/properties": ">=5.0.0-beta.131", - "@ethersproject/strings": ">=5.0.0-beta.130", - "cross-fetch": "3.0.4" + "@ethersproject/strings": ">=5.0.0-beta.130" }, "description": "Utility fucntions for managing web requests for ethers.", "ethereum": "donations.ethers.eth", diff --git a/packages/web/src.ts/browser-geturl.ts b/packages/web/src.ts/browser-geturl.ts new file mode 100644 index 0000000000..51c35777a0 --- /dev/null +++ b/packages/web/src.ts/browser-geturl.ts @@ -0,0 +1,51 @@ +"use strict"; + +export type GetUrlResponse = { + statusCode: number, + statusMessage: string; + headers: { [ key: string] : string }; + body: string; +}; + +export type Options = { + method?: string, + body?: string + headers?: { [ key: string] : string }, +}; + +export async function getUrl(href: string, options?: Options): Promise { + if (options == null) { options = { }; } + + const request = { + method: (options.method || "GET"), + headers: (options.headers || { }), + body: (options.body || undefined), + + mode: "cors", // no-cors, cors, *same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "same-origin", // include, *same-origin, omit + redirect: "follow", // manual, *follow, error + referrer: "client", // no-referrer, *client + }; + + const response = await fetch(href, request); + const body = await response.text(); + + const headers: { [ name: string ]: string } = { }; + if (response.headers.forEach) { + response.headers.forEach((value, key) => { + headers[key.toLowerCase()] = value; + }); + } else { + (<() => Array>(((response.headers)).keys))().forEach((key) => { + headers[key.toLowerCase()] = response.headers.get(key); + }); + } + + return { + headers: headers, + statusCode: response.status, + statusMessage: response.statusText, + body: body, + } +} diff --git a/packages/web/src.ts/geturl.ts b/packages/web/src.ts/geturl.ts new file mode 100644 index 0000000000..bb88f25261 --- /dev/null +++ b/packages/web/src.ts/geturl.ts @@ -0,0 +1,95 @@ +"use strict"; + +import http from "http"; +import https from "https"; +import { URL } from "url" + +import { Logger } from "@ethersproject/logger"; +import { version } from "./_version"; +const logger = new Logger(version); + +export type GetUrlResponse = { + statusCode: number, + statusMessage: string; + headers: { [ key: string] : string }; + body: string; +}; + +export type Options = { + method?: string, + body?: string + headers?: { [ key: string] : string }, +}; + +function getResponse(request: http.ClientRequest): Promise { + return new Promise((resolve, reject) => { + request.once("response", (resp: http.IncomingMessage) => { + const response: GetUrlResponse = { + statusCode: resp.statusCode, + statusMessage: resp.statusMessage, + headers: Object.keys(resp.headers).reduce((accum, name) => { + let value = resp.headers[name]; + if (Array.isArray(value)) { + value = value.join(", "); + } + accum[name] = value; + return accum; + }, <{ [ name: string ]: string }>{ }), + body: null + }; + resp.setEncoding("utf8"); + + resp.on("data", (chunk: string) => { + if (response.body == null) { response.body = ""; } + response.body += chunk; + }); + + resp.on("end", () => { + resolve(response); + }); + + resp.on("error", (error) => { + (error).response = response; + reject(error); + }); + }); + + request.on("error", (error) => { reject(error); }); + }); +} + +export async function getUrl(href: string, options?: Options): Promise { + if (options == null) { options = { }; } + + const url = new URL(href); + + const request = { + method: (options.method || "GET"), + headers: (options.headers || { }), + }; + + let req: http.ClientRequest = null; + switch (url.protocol) { + case "http:": { + req = http.request(url, request); + break; + } + case "https:": + req = https.request(url, request); + break; + default: + logger.throwError(`unsupported protocol ${ url.protocol }`, Logger.errors.UNSUPPORTED_OPERATION, { + protocol: url.protocol, + operation: "request" + }); + } + + if (options.body) { + req.write(options.body); + } + req.end(); + + const response = await getResponse(req); + return response; +} + diff --git a/packages/web/src.ts/index.ts b/packages/web/src.ts/index.ts index e6761165c2..e70b916718 100644 --- a/packages/web/src.ts/index.ts +++ b/packages/web/src.ts/index.ts @@ -1,7 +1,5 @@ "use strict"; -import fetch from "cross-fetch"; - import { encode as base64Encode } from "@ethersproject/base64"; import { shallowCopy } from "@ethersproject/properties"; import { toUtf8Bytes } from "@ethersproject/strings"; @@ -10,6 +8,8 @@ import { Logger } from "@ethersproject/logger"; import { version } from "./_version"; const logger = new Logger(version); +import { getUrl, GetUrlResponse } from "./geturl"; + // Exported Types export type ConnectionInfo = { url: string, @@ -36,31 +36,12 @@ export type PollOptions = { export type FetchJsonResponse = { statusCode: number; - status: string; headers: { [ header: string ]: string }; }; type Header = { key: string, value: string }; -function getResponse(response: Response): FetchJsonResponse { - const headers: { [ header: string ]: string } = { }; - if (response.headers.forEach) { - response.headers.forEach((value, key) => { - headers[key.toLowerCase()] = value; - }); - } else { - (<() => Array>(((response.headers)).keys))().forEach((key) => { - headers[key.toLowerCase()] = response.headers.get(key); - }); - } - - return { - statusCode: response.status, - status: response.statusText, - headers: headers - }; -} export function fetchJson(connection: string | ConnectionInfo, json?: string, processFunc?: (value: any, response: FetchJsonResponse) => any): Promise { const headers: { [key: string]: Header } = { }; @@ -69,11 +50,6 @@ export function fetchJson(connection: string | ConnectionInfo, json?: string, pr // @TODO: Allow ConnectionInfo to override some of these values const options: any = { method: "GET", - mode: "cors", // no-cors, cors, *same-origin - cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", // include, *same-origin, omit - redirect: "follow", // manual, *follow, error - referrer: "client", // no-referrer, *client }; let allow304 = false; @@ -157,33 +133,27 @@ export function fetchJson(connection: string | ConnectionInfo, json?: string, pr const runningFetch = (async function() { - let response: Response = null; - let body: string = null; - - while (true) { - try { - response = await fetch(url, options); - } catch (error) { - console.log(error); - } - body = await response.text(); + let response: GetUrlResponse = null; + try { + response = await getUrl(url, options); + } catch (error) { + console.log(error); + response = (error).response; + } - if (allow304 && response.status === 304) { - body = null; - break; + let body = response.body; - } else if (!response.ok) { - runningTimeout.cancel(); - logger.throwError("bad response", Logger.errors.SERVER_ERROR, { - status: response.status, - body: body, - type: response.type, - url: response.url - }); + if (allow304 && response.statusCode === 304) { + body = null; - } else { - break; - } + } else if (response.statusCode < 200 || response.statusCode >= 300) { + runningTimeout.cancel(); + logger.throwError("bad response", Logger.errors.SERVER_ERROR, { + status: response.statusCode, + headers: response.headers, + body: body, + url: url + }); } runningTimeout.cancel(); @@ -203,7 +173,7 @@ export function fetchJson(connection: string | ConnectionInfo, json?: string, pr if (processFunc) { try { - json = await processFunc(json, getResponse(response)); + json = await processFunc(json, response); } catch (error) { logger.throwError("processing response error", Logger.errors.SERVER_ERROR, { body: json, diff --git a/packages/web/thirdparty.d.ts b/packages/web/thirdparty.d.ts index 72a8f6c18c..689c7e6eeb 100644 --- a/packages/web/thirdparty.d.ts +++ b/packages/web/thirdparty.d.ts @@ -1,17 +1,4 @@ -declare module "xmlhttprequest" { - export class XMLHttpRequest { - readyState: number; - status: number; - responseText: string; - - constructor(); - open(method: string, url: string, async?: boolean): void; - setRequestHeader(key: string, value: string): void; - send(body?: string): void; - abort(): void; - - onreadystatechange: () => void; - onerror: (error: Error) => void; - } +declare module "node-fetch" { + function fetch(url: string, options: any): Promise; + export default fetch; } - diff --git a/rollup.config.js b/rollup.config.js index a8c96ffe30..bf42c4b347 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,7 @@ "use strict"; +import path from "path"; + import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import json from 'rollup-plugin-json'; @@ -8,9 +10,10 @@ import { terser } from "rollup-plugin-terser"; import { createFilter } from 'rollup-pluginutils'; -function Replacer(options = {}) { +function Replacer(basePath, options = {}) { const filter = createFilter(options.include, options.exclude); - let suffixes = Object.keys(options.replace); + const suffixes = Object.keys(options.replace); + const pathUp = path.resolve(basePath, ".."); return { name: "file-replacer", transform(code, id) { @@ -26,14 +29,20 @@ function Replacer(options = {}) { for (let i = 0; i < suffixes.length; i++) { const suffix = suffixes[i]; if (id.match(new RegExp(suffix))) { - let newCode = options.replace[suffix]; - console.log(`Replace: ${ id } (${ code.length } => ${ newCode.length })`); + const newCode = options.replace[suffix]; + console.log(`Replace: ${ id.substring(pathUp.length + 1) } (${ code.length } => ${ newCode.length })`); return { code: newCode, map: { mappings: '' } }; } + + } + + if (id.substring(0, basePath.length) !== basePath) { + console.log(`Keep: ${ id.substring(pathUp.length + 1) }`); } + return null; } }; @@ -48,11 +57,7 @@ const ellipticPackage = (function() { return JSON.stringify({ version: ellipticPackage.version }); })(); -export default commandLineArgs => { - let minify = commandLineArgs.configMinify; - let buildModule = commandLineArgs.configModule; - let testing = commandLineArgs.configTest; - +function getConfig(minify, buildModule, testing) { let input = "packages/ethers/lib/index.js" let output = [ "umd" ]; let format = "umd"; @@ -65,7 +70,7 @@ export default commandLineArgs => { mainFields = [ "browser", "module", "main" ]; } - const replacer = Replacer({ + const replacer = Replacer(path.resolve("packages"), { replace: { // Remove the precomputed secp256k1 points "elliptic/lib/elliptic/precomputed/secp256k1.js$": undef, @@ -108,9 +113,11 @@ export default commandLineArgs => { plugins.push(terser()); } - let outputFile = (("packages/") + - (testing ? "tests": "ethers") + - ("/dist/ethers." + output.join(".") + ".js")); + const outputFile = [ + "packages", + (testing ? "tests": "ethers"), + ("/dist/ethers." + output.join(".") + ".js") + ].join("/"); return { input: input, @@ -125,3 +132,18 @@ export default commandLineArgs => { plugins: plugins }; } + +export default commandLineArgs => { + const testing = commandLineArgs.configTest; + + if (commandLineArgs.configAll) { + return [ + getConfig(false, false, testing), + getConfig(false, true, testing), + getConfig(true, false, testing), + getConfig(true, true, testing) + ]; + } + + return getConfig(commandLineArgs.configMinify, commandLineArgs.configModule, testing); +}