From 4138c6fe95c59c449c3c8a4801eafe0421a364ba Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 27 Nov 2024 16:34:40 +0100 Subject: [PATCH 1/5] polyfill: first draft to polyfill fetch api --- LICENSING.md | 1 + src/browser/browser.zig | 5 + src/polyfill/fetch.zig | 713 ++++++++++++++++++++++++++++++++++++ src/polyfill/polyfill.zig | 38 ++ src/run_tests.zig | 1 + src/wpt/run.zig | 5 + tests/html/await-fetch.html | 10 + 7 files changed, 773 insertions(+) create mode 100644 src/polyfill/fetch.zig create mode 100644 src/polyfill/polyfill.zig create mode 100644 tests/html/await-fetch.html diff --git a/LICENSING.md b/LICENSING.md index d54e4d79..0d716b41 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -11,6 +11,7 @@ The following files are licensed under MIT: ``` src/http/Client.zig +src/polyfill/fetch.zig ``` The following directories and their subdirectories are licensed under their diff --git a/src/browser/browser.zig b/src/browser/browser.zig index b399e755..407794ff 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -42,6 +42,8 @@ const FetchResult = @import("../http/Client.zig").Client.FetchResult; const UserContext = @import("../user_context.zig").UserContext; const HttpClient = @import("asyncio").Client; +const polyfill = @import("../polyfill/polyfill.zig"); + const log = std.log.scoped(.browser); // Browser is an instance of the browser. @@ -355,6 +357,9 @@ pub const Page = struct { log.debug("start js env", .{}); try self.session.env.start(); + // load polyfills + try polyfill.load(alloc, self.session.env); + // inspector if (self.session.inspector) |inspector| { inspector.contextCreated(self.session.env, "", self.origin.?, auxData); diff --git a/src/polyfill/fetch.zig b/src/polyfill/fetch.zig new file mode 100644 index 00000000..5911707d --- /dev/null +++ b/src/polyfill/fetch.zig @@ -0,0 +1,713 @@ +const std = @import("std"); +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + +// fetch_polyfill js code comes from +// https://github.com/JakeChampion/fetch/blob/main/fetch.js +// +// The original code source is available in MIT license. +// +// The script comes from the built version from npm. +// You can get the package with the command: +// +// wget $(npm view whatwg-fetch dist.tarball) +// +// The source is the content of `package/dist/fetch.umd.js` file. +pub const source = + \\(function (global, factory) { + \\ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + \\ typeof define === 'function' && define.amd ? define(['exports'], factory) : + \\ (factory((global.WHATWGFetch = {}))); + \\}(this, (function (exports) { 'use strict'; + \\ + \\ /* eslint-disable no-prototype-builtins */ + \\ var g = + \\ (typeof globalThis !== 'undefined' && globalThis) || + \\ (typeof self !== 'undefined' && self) || + \\ // eslint-disable-next-line no-undef + \\ (typeof global !== 'undefined' && global) || + \\ {}; + \\ + \\ var support = { + \\ searchParams: 'URLSearchParams' in g, + \\ iterable: 'Symbol' in g && 'iterator' in Symbol, + \\ blob: + \\ 'FileReader' in g && + \\ 'Blob' in g && + \\ (function() { + \\ try { + \\ new Blob(); + \\ return true + \\ } catch (e) { + \\ return false + \\ } + \\ })(), + \\ formData: 'FormData' in g, + \\ arrayBuffer: 'ArrayBuffer' in g + \\ }; + \\ + \\ function isDataView(obj) { + \\ return obj && DataView.prototype.isPrototypeOf(obj) + \\ } + \\ + \\ if (support.arrayBuffer) { + \\ var viewClasses = [ + \\ '[object Int8Array]', + \\ '[object Uint8Array]', + \\ '[object Uint8ClampedArray]', + \\ '[object Int16Array]', + \\ '[object Uint16Array]', + \\ '[object Int32Array]', + \\ '[object Uint32Array]', + \\ '[object Float32Array]', + \\ '[object Float64Array]' + \\ ]; + \\ + \\ var isArrayBufferView = + \\ ArrayBuffer.isView || + \\ function(obj) { + \\ return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 + \\ }; + \\ } + \\ + \\ function normalizeName(name) { + \\ if (typeof name !== 'string') { + \\ name = String(name); + \\ } + \\ if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { + \\ throw new TypeError('Invalid character in header field name: "' + name + '"') + \\ } + \\ return name.toLowerCase() + \\ } + \\ + \\ function normalizeValue(value) { + \\ if (typeof value !== 'string') { + \\ value = String(value); + \\ } + \\ return value + \\ } + \\ + \\ // Build a destructive iterator for the value list + \\ function iteratorFor(items) { + \\ var iterator = { + \\ next: function() { + \\ var value = items.shift(); + \\ return {done: value === undefined, value: value} + \\ } + \\ }; + \\ + \\ if (support.iterable) { + \\ iterator[Symbol.iterator] = function() { + \\ return iterator + \\ }; + \\ } + \\ + \\ return iterator + \\ } + \\ + \\ function Headers(headers) { + \\ this.map = {}; + \\ + \\ if (headers instanceof Headers) { + \\ headers.forEach(function(value, name) { + \\ this.append(name, value); + \\ }, this); + \\ } else if (Array.isArray(headers)) { + \\ headers.forEach(function(header) { + \\ if (header.length != 2) { + \\ throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length) + \\ } + \\ this.append(header[0], header[1]); + \\ }, this); + \\ } else if (headers) { + \\ Object.getOwnPropertyNames(headers).forEach(function(name) { + \\ this.append(name, headers[name]); + \\ }, this); + \\ } + \\ } + \\ + \\ Headers.prototype.append = function(name, value) { + \\ name = normalizeName(name); + \\ value = normalizeValue(value); + \\ var oldValue = this.map[name]; + \\ this.map[name] = oldValue ? oldValue + ', ' + value : value; + \\ }; + \\ + \\ Headers.prototype['delete'] = function(name) { + \\ delete this.map[normalizeName(name)]; + \\ }; + \\ + \\ Headers.prototype.get = function(name) { + \\ name = normalizeName(name); + \\ return this.has(name) ? this.map[name] : null + \\ }; + \\ + \\ Headers.prototype.has = function(name) { + \\ return this.map.hasOwnProperty(normalizeName(name)) + \\ }; + \\ + \\ Headers.prototype.set = function(name, value) { + \\ this.map[normalizeName(name)] = normalizeValue(value); + \\ }; + \\ + \\ Headers.prototype.forEach = function(callback, thisArg) { + \\ for (var name in this.map) { + \\ if (this.map.hasOwnProperty(name)) { + \\ callback.call(thisArg, this.map[name], name, this); + \\ } + \\ } + \\ }; + \\ + \\ Headers.prototype.keys = function() { + \\ var items = []; + \\ this.forEach(function(value, name) { + \\ items.push(name); + \\ }); + \\ return iteratorFor(items) + \\ }; + \\ + \\ Headers.prototype.values = function() { + \\ var items = []; + \\ this.forEach(function(value) { + \\ items.push(value); + \\ }); + \\ return iteratorFor(items) + \\ }; + \\ + \\ Headers.prototype.entries = function() { + \\ var items = []; + \\ this.forEach(function(value, name) { + \\ items.push([name, value]); + \\ }); + \\ return iteratorFor(items) + \\ }; + \\ + \\ if (support.iterable) { + \\ Headers.prototype[Symbol.iterator] = Headers.prototype.entries; + \\ } + \\ + \\ function consumed(body) { + \\ if (body._noBody) return + \\ if (body.bodyUsed) { + \\ return Promise.reject(new TypeError('Already read')) + \\ } + \\ body.bodyUsed = true; + \\ } + \\ + \\ function fileReaderReady(reader) { + \\ return new Promise(function(resolve, reject) { + \\ reader.onload = function() { + \\ resolve(reader.result); + \\ }; + \\ reader.onerror = function() { + \\ reject(reader.error); + \\ }; + \\ }) + \\ } + \\ + \\ function readBlobAsArrayBuffer(blob) { + \\ var reader = new FileReader(); + \\ var promise = fileReaderReady(reader); + \\ reader.readAsArrayBuffer(blob); + \\ return promise + \\ } + \\ + \\ function readBlobAsText(blob) { + \\ var reader = new FileReader(); + \\ var promise = fileReaderReady(reader); + \\ var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type); + \\ var encoding = match ? match[1] : 'utf-8'; + \\ reader.readAsText(blob, encoding); + \\ return promise + \\ } + \\ + \\ function readArrayBufferAsText(buf) { + \\ var view = new Uint8Array(buf); + \\ var chars = new Array(view.length); + \\ + \\ for (var i = 0; i < view.length; i++) { + \\ chars[i] = String.fromCharCode(view[i]); + \\ } + \\ return chars.join('') + \\ } + \\ + \\ function bufferClone(buf) { + \\ if (buf.slice) { + \\ return buf.slice(0) + \\ } else { + \\ var view = new Uint8Array(buf.byteLength); + \\ view.set(new Uint8Array(buf)); + \\ return view.buffer + \\ } + \\ } + \\ + \\ function Body() { + \\ this.bodyUsed = false; + \\ + \\ this._initBody = function(body) { + \\ /* + \\ fetch-mock wraps the Response object in an ES6 Proxy to + \\ provide useful test harness features such as flush. However, on + \\ ES5 browsers without fetch or Proxy support pollyfills must be used; + \\ the proxy-pollyfill is unable to proxy an attribute unless it exists + \\ on the object before the Proxy is created. This change ensures + \\ Response.bodyUsed exists on the instance, while maintaining the + \\ semantic of setting Request.bodyUsed in the constructor before + \\ _initBody is called. + \\ */ + \\ // eslint-disable-next-line no-self-assign + \\ this.bodyUsed = this.bodyUsed; + \\ this._bodyInit = body; + \\ if (!body) { + \\ this._noBody = true; + \\ this._bodyText = ''; + \\ } else if (typeof body === 'string') { + \\ this._bodyText = body; + \\ } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + \\ this._bodyBlob = body; + \\ } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + \\ this._bodyFormData = body; + \\ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + \\ this._bodyText = body.toString(); + \\ } else if (support.arrayBuffer && support.blob && isDataView(body)) { + \\ this._bodyArrayBuffer = bufferClone(body.buffer); + \\ // IE 10-11 can't handle a DataView body. + \\ this._bodyInit = new Blob([this._bodyArrayBuffer]); + \\ } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + \\ this._bodyArrayBuffer = bufferClone(body); + \\ } else { + \\ this._bodyText = body = Object.prototype.toString.call(body); + \\ } + \\ + \\ if (!this.headers.get('content-type')) { + \\ if (typeof body === 'string') { + \\ this.headers.set('content-type', 'text/plain;charset=UTF-8'); + \\ } else if (this._bodyBlob && this._bodyBlob.type) { + \\ this.headers.set('content-type', this._bodyBlob.type); + \\ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + \\ this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); + \\ } + \\ } + \\ }; + \\ + \\ if (support.blob) { + \\ this.blob = function() { + \\ var rejected = consumed(this); + \\ if (rejected) { + \\ return rejected + \\ } + \\ + \\ if (this._bodyBlob) { + \\ return Promise.resolve(this._bodyBlob) + \\ } else if (this._bodyArrayBuffer) { + \\ return Promise.resolve(new Blob([this._bodyArrayBuffer])) + \\ } else if (this._bodyFormData) { + \\ throw new Error('could not read FormData body as blob') + \\ } else { + \\ return Promise.resolve(new Blob([this._bodyText])) + \\ } + \\ }; + \\ } + \\ + \\ this.arrayBuffer = function() { + \\ if (this._bodyArrayBuffer) { + \\ var isConsumed = consumed(this); + \\ if (isConsumed) { + \\ return isConsumed + \\ } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + \\ return Promise.resolve( + \\ this._bodyArrayBuffer.buffer.slice( + \\ this._bodyArrayBuffer.byteOffset, + \\ this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength + \\ ) + \\ ) + \\ } else { + \\ return Promise.resolve(this._bodyArrayBuffer) + \\ } + \\ } else if (support.blob) { + \\ return this.blob().then(readBlobAsArrayBuffer) + \\ } else { + \\ throw new Error('could not read as ArrayBuffer') + \\ } + \\ }; + \\ + \\ this.text = function() { + \\ var rejected = consumed(this); + \\ if (rejected) { + \\ return rejected + \\ } + \\ + \\ if (this._bodyBlob) { + \\ return readBlobAsText(this._bodyBlob) + \\ } else if (this._bodyArrayBuffer) { + \\ return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) + \\ } else if (this._bodyFormData) { + \\ throw new Error('could not read FormData body as text') + \\ } else { + \\ return Promise.resolve(this._bodyText) + \\ } + \\ }; + \\ + \\ if (support.formData) { + \\ this.formData = function() { + \\ return this.text().then(decode) + \\ }; + \\ } + \\ + \\ this.json = function() { + \\ return this.text().then(JSON.parse) + \\ }; + \\ + \\ return this + \\ } + \\ + \\ // HTTP methods whose capitalization should be normalized + \\ var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']; + \\ + \\ function normalizeMethod(method) { + \\ var upcased = method.toUpperCase(); + \\ return methods.indexOf(upcased) > -1 ? upcased : method + \\ } + \\ + \\ function Request(input, options) { + \\ if (!(this instanceof Request)) { + \\ throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + \\ } + \\ + \\ options = options || {}; + \\ var body = options.body; + \\ + \\ if (input instanceof Request) { + \\ if (input.bodyUsed) { + \\ throw new TypeError('Already read') + \\ } + \\ this.url = input.url; + \\ this.credentials = input.credentials; + \\ if (!options.headers) { + \\ this.headers = new Headers(input.headers); + \\ } + \\ this.method = input.method; + \\ this.mode = input.mode; + \\ this.signal = input.signal; + \\ if (!body && input._bodyInit != null) { + \\ body = input._bodyInit; + \\ input.bodyUsed = true; + \\ } + \\ } else { + \\ this.url = String(input); + \\ } + \\ + \\ this.credentials = options.credentials || this.credentials || 'same-origin'; + \\ if (options.headers || !this.headers) { + \\ this.headers = new Headers(options.headers); + \\ } + \\ this.method = normalizeMethod(options.method || this.method || 'GET'); + \\ this.mode = options.mode || this.mode || null; + \\ this.signal = options.signal || this.signal || (function () { + \\ if ('AbortController' in g) { + \\ var ctrl = new AbortController(); + \\ return ctrl.signal; + \\ } + \\ }()); + \\ this.referrer = null; + \\ + \\ if ((this.method === 'GET' || this.method === 'HEAD') && body) { + \\ throw new TypeError('Body not allowed for GET or HEAD requests') + \\ } + \\ this._initBody(body); + \\ + \\ if (this.method === 'GET' || this.method === 'HEAD') { + \\ if (options.cache === 'no-store' || options.cache === 'no-cache') { + \\ // Search for a '_' parameter in the query string + \\ var reParamSearch = /([?&])_=[^&]*/; + \\ if (reParamSearch.test(this.url)) { + \\ // If it already exists then set the value with the current time + \\ this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()); + \\ } else { + \\ // Otherwise add a new '_' parameter to the end with the current time + \\ var reQueryString = /\?/; + \\ this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime(); + \\ } + \\ } + \\ } + \\ } + \\ + \\ Request.prototype.clone = function() { + \\ return new Request(this, {body: this._bodyInit}) + \\ }; + \\ + \\ function decode(body) { + \\ var form = new FormData(); + \\ body + \\ .trim() + \\ .split('&') + \\ .forEach(function(bytes) { + \\ if (bytes) { + \\ var split = bytes.split('='); + \\ var name = split.shift().replace(/\+/g, ' '); + \\ var value = split.join('=').replace(/\+/g, ' '); + \\ form.append(decodeURIComponent(name), decodeURIComponent(value)); + \\ } + \\ }); + \\ return form + \\ } + \\ + \\ function parseHeaders(rawHeaders) { + \\ var headers = new Headers(); + \\ // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space + \\ // https://tools.ietf.org/html/rfc7230#section-3.2 + \\ var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); + \\ // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill + \\ // https://github.com/github/fetch/issues/748 + \\ // https://github.com/zloirock/core-js/issues/751 + \\ preProcessedHeaders + \\ .split('\r') + \\ .map(function(header) { + \\ return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header + \\ }) + \\ .forEach(function(line) { + \\ var parts = line.split(':'); + \\ var key = parts.shift().trim(); + \\ if (key) { + \\ var value = parts.join(':').trim(); + \\ try { + \\ headers.append(key, value); + \\ } catch (error) { + \\ console.warn('Response ' + error.message); + \\ } + \\ } + \\ }); + \\ return headers + \\ } + \\ + \\ Body.call(Request.prototype); + \\ + \\ function Response(bodyInit, options) { + \\ if (!(this instanceof Response)) { + \\ throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + \\ } + \\ if (!options) { + \\ options = {}; + \\ } + \\ + \\ this.type = 'default'; + \\ this.status = options.status === undefined ? 200 : options.status; + \\ if (this.status < 200 || this.status > 599) { + \\ throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].") + \\ } + \\ this.ok = this.status >= 200 && this.status < 300; + \\ this.statusText = options.statusText === undefined ? '' : '' + options.statusText; + \\ this.headers = new Headers(options.headers); + \\ this.url = options.url || ''; + \\ this._initBody(bodyInit); + \\ } + \\ + \\ Body.call(Response.prototype); + \\ + \\ Response.prototype.clone = function() { + \\ return new Response(this._bodyInit, { + \\ status: this.status, + \\ statusText: this.statusText, + \\ headers: new Headers(this.headers), + \\ url: this.url + \\ }) + \\ }; + \\ + \\ Response.error = function() { + \\ var response = new Response(null, {status: 200, statusText: ''}); + \\ response.ok = false; + \\ response.status = 0; + \\ response.type = 'error'; + \\ return response + \\ }; + \\ + \\ var redirectStatuses = [301, 302, 303, 307, 308]; + \\ + \\ Response.redirect = function(url, status) { + \\ if (redirectStatuses.indexOf(status) === -1) { + \\ throw new RangeError('Invalid status code') + \\ } + \\ + \\ return new Response(null, {status: status, headers: {location: url}}) + \\ }; + \\ + \\ exports.DOMException = g.DOMException; + \\ try { + \\ new exports.DOMException(); + \\ } catch (err) { + \\ exports.DOMException = function(message, name) { + \\ this.message = message; + \\ this.name = name; + \\ var error = Error(message); + \\ this.stack = error.stack; + \\ }; + \\ exports.DOMException.prototype = Object.create(Error.prototype); + \\ exports.DOMException.prototype.constructor = exports.DOMException; + \\ } + \\ + \\ function fetch(input, init) { + \\ return new Promise(function(resolve, reject) { + \\ var request = new Request(input, init); + \\ + \\ if (request.signal && request.signal.aborted) { + \\ return reject(new exports.DOMException('Aborted', 'AbortError')) + \\ } + \\ + \\ var xhr = new XMLHttpRequest(); + \\ + \\ function abortXhr() { + \\ xhr.abort(); + \\ } + \\ + \\ xhr.onload = function() { + \\ var options = { + \\ statusText: xhr.statusText, + \\ headers: parseHeaders(xhr.getAllResponseHeaders() || '') + \\ }; + \\ // This check if specifically for when a user fetches a file locally from the file system + \\ // Only if the status is out of a normal range + \\ if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) { + \\ options.status = 200; + \\ } else { + \\ options.status = xhr.status; + \\ } + \\ options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); + \\ var body = 'response' in xhr ? xhr.response : xhr.responseText; + \\ setTimeout(function() { + \\ resolve(new Response(body, options)); + \\ }, 0); + \\ }; + \\ + \\ xhr.onerror = function() { + \\ setTimeout(function() { + \\ reject(new TypeError('Network request failed')); + \\ }, 0); + \\ }; + \\ + \\ xhr.ontimeout = function() { + \\ setTimeout(function() { + \\ reject(new TypeError('Network request timed out')); + \\ }, 0); + \\ }; + \\ + \\ xhr.onabort = function() { + \\ setTimeout(function() { + \\ reject(new exports.DOMException('Aborted', 'AbortError')); + \\ }, 0); + \\ }; + \\ + \\ function fixUrl(url) { + \\ try { + \\ return url === '' && g.location.href ? g.location.href : url + \\ } catch (e) { + \\ return url + \\ } + \\ } + \\ + \\ xhr.open(request.method, fixUrl(request.url), true); + \\ + \\ if (request.credentials === 'include') { + \\ xhr.withCredentials = true; + \\ } else if (request.credentials === 'omit') { + \\ xhr.withCredentials = false; + \\ } + \\ + \\ if ('responseType' in xhr) { + \\ if (support.blob) { + \\ xhr.responseType = 'blob'; + \\ } else if ( + \\ support.arrayBuffer + \\ ) { + \\ xhr.responseType = 'arraybuffer'; + \\ } + \\ } + \\ + \\ if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) { + \\ var names = []; + \\ Object.getOwnPropertyNames(init.headers).forEach(function(name) { + \\ names.push(normalizeName(name)); + \\ xhr.setRequestHeader(name, normalizeValue(init.headers[name])); + \\ }); + \\ request.headers.forEach(function(value, name) { + \\ if (names.indexOf(name) === -1) { + \\ xhr.setRequestHeader(name, value); + \\ } + \\ }); + \\ } else { + \\ request.headers.forEach(function(value, name) { + \\ xhr.setRequestHeader(name, value); + \\ }); + \\ } + \\ + \\ if (request.signal) { + \\ request.signal.addEventListener('abort', abortXhr); + \\ + \\ xhr.onreadystatechange = function() { + \\ // DONE (success or failure) + \\ if (xhr.readyState === 4) { + \\ request.signal.removeEventListener('abort', abortXhr); + \\ } + \\ }; + \\ } + \\ + \\ xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit); + \\ }) + \\ } + \\ + \\ fetch.polyfill = true; + \\ + \\ if (!g.fetch) { + \\ g.fetch = fetch; + \\ g.Headers = Headers; + \\ g.Request = Request; + \\ g.Response = Response; + \\ } + \\ + \\ exports.Headers = Headers; + \\ exports.Request = Request; + \\ exports.Response = Response; + \\ exports.fetch = fetch; + \\ + \\ Object.defineProperty(exports, '__esModule', { value: true }); + \\ + \\}))); +; + +pub fn testExecFn( + alloc: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + try @import("polyfill.zig").load(alloc, js_env.*); + + var fetch = [_]Case{ + .{ + .src = + \\var ok = false; + \\const request = new Request("https://httpbin.io/json"); + \\fetch(request) + \\ .then((response) => { ok = response.ok; }); + \\false; + , + .ex = "false", + }, + // all events have been resolved. + .{ .src = "ok", .ex = "true" }, + }; + try checkCases(js_env, &fetch); + + var fetch2 = [_]Case{ + .{ + .src = + \\var ok2 = false; + \\const request2 = new Request("https://httpbin.io/json"); + \\(async function () { resp = await fetch(request2); ok2 = resp.ok; }()); + \\false; + , + .ex = "false", + }, + // all events have been resolved. + .{ .src = "ok2", .ex = "true" }, + }; + try checkCases(js_env, &fetch2); +} diff --git a/src/polyfill/polyfill.zig b/src/polyfill/polyfill.zig new file mode 100644 index 00000000..5189c3e9 --- /dev/null +++ b/src/polyfill/polyfill.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const jsruntime = @import("jsruntime"); +const Env = jsruntime.Env; + +const fetch = @import("fetch.zig").fetch_polyfill; + +const log = std.log.scoped(.polyfill); + +const modules = [_]struct { + name: []const u8, + source: []const u8, +}{ + .{ .name = "polyfill-fetch", .source = @import("fetch.zig").source }, +}; + +pub fn load(alloc: std.mem.Allocator, env: Env) !void { + var try_catch: jsruntime.TryCatch = undefined; + try_catch.init(env); + defer try_catch.deinit(); + + for (modules) |m| { + const res = env.exec(m.source, m.name) catch { + if (try try_catch.err(alloc, env)) |msg| { + defer alloc.free(msg); + log.err("load {s}: {s}", .{ m.name, msg }); + } + return; + }; + + if (builtin.mode == .Debug) { + const msg = try res.toString(alloc, env); + defer alloc.free(msg); + log.debug("load {s}: {s}", .{ m.name, msg }); + } + } +} diff --git a/src/run_tests.zig b/src/run_tests.zig index a9073d4e..b506e5aa 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -136,6 +136,7 @@ fn testsAllExecFn( URLTestExecFn, HTMLElementTestExecFn, MutationObserverTestExecFn, + @import("polyfill/fetch.zig").testExecFn, }; inline for (testFns) |testFn| { diff --git a/src/wpt/run.zig b/src/wpt/run.zig index a44b12d7..07c3f740 100644 --- a/src/wpt/run.zig +++ b/src/wpt/run.zig @@ -33,6 +33,8 @@ const Client = @import("asyncio").Client; const Types = @import("../main_wpt.zig").Types; const UserContext = @import("../main_wpt.zig").UserContext; +const polyfill = @import("../polyfill/polyfill.zig"); + // runWPT parses the given HTML file, starts a js env and run the first script // tags containing javascript sources. // It loads first the js libs files. @@ -74,6 +76,9 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const try js_env.start(); defer js_env.stop(); + // load polyfills + try polyfill.load(alloc, js_env); + // display console logs defer { const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable; diff --git a/tests/html/await-fetch.html b/tests/html/await-fetch.html new file mode 100644 index 00000000..dae982fa --- /dev/null +++ b/tests/html/await-fetch.html @@ -0,0 +1,10 @@ + From adfffd2b081483ea481ac5a223ae9d8ce4b5a317 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 28 Nov 2024 15:52:39 +0100 Subject: [PATCH 2/5] polyfill: fetch: disable Arraybuffer usage --- src/polyfill/fetch.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/polyfill/fetch.zig b/src/polyfill/fetch.zig index 5911707d..1414d56f 100644 --- a/src/polyfill/fetch.zig +++ b/src/polyfill/fetch.zig @@ -44,7 +44,13 @@ pub const source = \\ } \\ })(), \\ formData: 'FormData' in g, - \\ arrayBuffer: 'ArrayBuffer' in g + \\ + + // Arraybuffer is available but xhr doesn't implement it for now. + //\\ arrayBuffer: 'ArrayBuffer' in g + \\ arrayBuffer: false + // + \\ }; \\ \\ function isDataView(obj) { From 47eef392d136d323365a771d3afab1394804a849 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 3 Dec 2024 14:34:04 +0100 Subject: [PATCH 3/5] add missing license header --- src/polyfill/polyfill.zig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/polyfill/polyfill.zig b/src/polyfill/polyfill.zig index 5189c3e9..0c696e55 100644 --- a/src/polyfill/polyfill.zig +++ b/src/polyfill/polyfill.zig @@ -1,3 +1,21 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + const std = @import("std"); const builtin = @import("builtin"); From 4e2b35b585b650cbfabc2e7acffe85d125d692c0 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 4 Dec 2024 14:26:21 +0100 Subject: [PATCH 4/5] tests: remove useless test file --- tests/html/await-fetch.html | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 tests/html/await-fetch.html diff --git a/tests/html/await-fetch.html b/tests/html/await-fetch.html deleted file mode 100644 index dae982fa..00000000 --- a/tests/html/await-fetch.html +++ /dev/null @@ -1,10 +0,0 @@ - From fbe883562606dff74f80067ee806ad6e05d6de2f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 4 Dec 2024 14:26:51 +0100 Subject: [PATCH 5/5] polyfill: use @embedfile to embed polyfill --- LICENSING.md | 2 +- src/polyfill/fetch.js | 671 +++++++++++++++++++++++++++++++++++++++++ src/polyfill/fetch.zig | 668 +--------------------------------------- 3 files changed, 674 insertions(+), 667 deletions(-) create mode 100644 src/polyfill/fetch.js diff --git a/LICENSING.md b/LICENSING.md index 0d716b41..e6988ade 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -11,7 +11,7 @@ The following files are licensed under MIT: ``` src/http/Client.zig -src/polyfill/fetch.zig +src/polyfill/fetch.js ``` The following directories and their subdirectories are licensed under their diff --git a/src/polyfill/fetch.js b/src/polyfill/fetch.js new file mode 100644 index 00000000..75efab54 --- /dev/null +++ b/src/polyfill/fetch.js @@ -0,0 +1,671 @@ +// fetch.js code comes from +// https://github.com/JakeChampion/fetch/blob/main/fetch.js +// +// The original code source is available in MIT license. +// +// The script comes from the built version from npm. +// You can get the package with the command: +// +// wget $(npm view whatwg-fetch dist.tarball) +// +// The source is the content of `package/dist/fetch.umd.js` file. +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.WHATWGFetch = {}))); +}(this, (function (exports) { 'use strict'; + + /* eslint-disable no-prototype-builtins */ + var g = + (typeof globalThis !== 'undefined' && globalThis) || + (typeof self !== 'undefined' && self) || + // eslint-disable-next-line no-undef + (typeof global !== 'undefined' && global) || + {}; + + var support = { + searchParams: 'URLSearchParams' in g, + iterable: 'Symbol' in g && 'iterator' in Symbol, + blob: + 'FileReader' in g && + 'Blob' in g && + (function() { + try { + new Blob(); + return true + } catch (e) { + return false + } + })(), + formData: 'FormData' in g, + + // Arraybuffer is available but xhr doesn't implement it for now. + // arrayBuffer: 'ArrayBuffer' in g + arrayBuffer: false + }; + + function isDataView(obj) { + return obj && DataView.prototype.isPrototypeOf(obj) + } + + if (support.arrayBuffer) { + var viewClasses = [ + '[object Int8Array]', + '[object Uint8Array]', + '[object Uint8ClampedArray]', + '[object Int16Array]', + '[object Uint16Array]', + '[object Int32Array]', + '[object Uint32Array]', + '[object Float32Array]', + '[object Float64Array]' + ]; + + var isArrayBufferView = + ArrayBuffer.isView || + function(obj) { + return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 + }; + } + + function normalizeName(name) { + if (typeof name !== 'string') { + name = String(name); + } + if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { + throw new TypeError('Invalid character in header field name: "' + name + '"') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = String(value); + } + return value + } + + // Build a destructive iterator for the value list + function iteratorFor(items) { + var iterator = { + next: function() { + var value = items.shift(); + return {done: value === undefined, value: value} + } + }; + + if (support.iterable) { + iterator[Symbol.iterator] = function() { + return iterator + }; + } + + return iterator + } + + function Headers(headers) { + this.map = {}; + + if (headers instanceof Headers) { + headers.forEach(function(value, name) { + this.append(name, value); + }, this); + } else if (Array.isArray(headers)) { + headers.forEach(function(header) { + if (header.length != 2) { + throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length) + } + this.append(header[0], header[1]); + }, this); + } else if (headers) { + Object.getOwnPropertyNames(headers).forEach(function(name) { + this.append(name, headers[name]); + }, this); + } + } + + Headers.prototype.append = function(name, value) { + name = normalizeName(name); + value = normalizeValue(value); + var oldValue = this.map[name]; + this.map[name] = oldValue ? oldValue + ', ' + value : value; + }; + + Headers.prototype['delete'] = function(name) { + delete this.map[normalizeName(name)]; + }; + + Headers.prototype.get = function(name) { + name = normalizeName(name); + return this.has(name) ? this.map[name] : null + }; + + Headers.prototype.has = function(name) { + return this.map.hasOwnProperty(normalizeName(name)) + }; + + Headers.prototype.set = function(name, value) { + this.map[normalizeName(name)] = normalizeValue(value); + }; + + Headers.prototype.forEach = function(callback, thisArg) { + for (var name in this.map) { + if (this.map.hasOwnProperty(name)) { + callback.call(thisArg, this.map[name], name, this); + } + } + }; + + Headers.prototype.keys = function() { + var items = []; + this.forEach(function(value, name) { + items.push(name); + }); + return iteratorFor(items) + }; + + Headers.prototype.values = function() { + var items = []; + this.forEach(function(value) { + items.push(value); + }); + return iteratorFor(items) + }; + + Headers.prototype.entries = function() { + var items = []; + this.forEach(function(value, name) { + items.push([name, value]); + }); + return iteratorFor(items) + }; + + if (support.iterable) { + Headers.prototype[Symbol.iterator] = Headers.prototype.entries; + } + + function consumed(body) { + if (body._noBody) return + if (body.bodyUsed) { + return Promise.reject(new TypeError('Already read')) + } + body.bodyUsed = true; + } + + function fileReaderReady(reader) { + return new Promise(function(resolve, reject) { + reader.onload = function() { + resolve(reader.result); + }; + reader.onerror = function() { + reject(reader.error); + }; + }) + } + + function readBlobAsArrayBuffer(blob) { + var reader = new FileReader(); + var promise = fileReaderReady(reader); + reader.readAsArrayBuffer(blob); + return promise + } + + function readBlobAsText(blob) { + var reader = new FileReader(); + var promise = fileReaderReady(reader); + var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type); + var encoding = match ? match[1] : 'utf-8'; + reader.readAsText(blob, encoding); + return promise + } + + function readArrayBufferAsText(buf) { + var view = new Uint8Array(buf); + var chars = new Array(view.length); + + for (var i = 0; i < view.length; i++) { + chars[i] = String.fromCharCode(view[i]); + } + return chars.join('') + } + + function bufferClone(buf) { + if (buf.slice) { + return buf.slice(0) + } else { + var view = new Uint8Array(buf.byteLength); + view.set(new Uint8Array(buf)); + return view.buffer + } + } + + function Body() { + this.bodyUsed = false; + + this._initBody = function(body) { + /* + fetch-mock wraps the Response object in an ES6 Proxy to + provide useful test harness features such as flush. However, on + ES5 browsers without fetch or Proxy support pollyfills must be used; + the proxy-pollyfill is unable to proxy an attribute unless it exists + on the object before the Proxy is created. This change ensures + Response.bodyUsed exists on the instance, while maintaining the + semantic of setting Request.bodyUsed in the constructor before + _initBody is called. + */ + // eslint-disable-next-line no-self-assign + this.bodyUsed = this.bodyUsed; + this._bodyInit = body; + if (!body) { + this._noBody = true; + this._bodyText = ''; + } else if (typeof body === 'string') { + this._bodyText = body; + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body; + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body; + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this._bodyText = body.toString(); + } else if (support.arrayBuffer && support.blob && isDataView(body)) { + this._bodyArrayBuffer = bufferClone(body.buffer); + // IE 10-11 can't handle a DataView body. + this._bodyInit = new Blob([this._bodyArrayBuffer]); + } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + this._bodyArrayBuffer = bufferClone(body); + } else { + this._bodyText = body = Object.prototype.toString.call(body); + } + + if (!this.headers.get('content-type')) { + if (typeof body === 'string') { + this.headers.set('content-type', 'text/plain;charset=UTF-8'); + } else if (this._bodyBlob && this._bodyBlob.type) { + this.headers.set('content-type', this._bodyBlob.type); + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); + } + } + }; + + if (support.blob) { + this.blob = function() { + var rejected = consumed(this); + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(new Blob([this._bodyArrayBuffer])) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } + }; + } + + this.arrayBuffer = function() { + if (this._bodyArrayBuffer) { + var isConsumed = consumed(this); + if (isConsumed) { + return isConsumed + } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + return Promise.resolve( + this._bodyArrayBuffer.buffer.slice( + this._bodyArrayBuffer.byteOffset, + this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength + ) + ) + } else { + return Promise.resolve(this._bodyArrayBuffer) + } + } else if (support.blob) { + return this.blob().then(readBlobAsArrayBuffer) + } else { + throw new Error('could not read as ArrayBuffer') + } + }; + + this.text = function() { + var rejected = consumed(this); + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } + }; + + if (support.formData) { + this.formData = function() { + return this.text().then(decode) + }; + } + + this.json = function() { + return this.text().then(JSON.parse) + }; + + return this + } + + // HTTP methods whose capitalization should be normalized + var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']; + + function normalizeMethod(method) { + var upcased = method.toUpperCase(); + return methods.indexOf(upcased) > -1 ? upcased : method + } + + function Request(input, options) { + if (!(this instanceof Request)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + + options = options || {}; + var body = options.body; + + if (input instanceof Request) { + if (input.bodyUsed) { + throw new TypeError('Already read') + } + this.url = input.url; + this.credentials = input.credentials; + if (!options.headers) { + this.headers = new Headers(input.headers); + } + this.method = input.method; + this.mode = input.mode; + this.signal = input.signal; + if (!body && input._bodyInit != null) { + body = input._bodyInit; + input.bodyUsed = true; + } + } else { + this.url = String(input); + } + + this.credentials = options.credentials || this.credentials || 'same-origin'; + if (options.headers || !this.headers) { + this.headers = new Headers(options.headers); + } + this.method = normalizeMethod(options.method || this.method || 'GET'); + this.mode = options.mode || this.mode || null; + this.signal = options.signal || this.signal || (function () { + if ('AbortController' in g) { + var ctrl = new AbortController(); + return ctrl.signal; + } + }()); + this.referrer = null; + + if ((this.method === 'GET' || this.method === 'HEAD') && body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(body); + + if (this.method === 'GET' || this.method === 'HEAD') { + if (options.cache === 'no-store' || options.cache === 'no-cache') { + // Search for a '_' parameter in the query string + var reParamSearch = /([?&])_=[^&]*/; + if (reParamSearch.test(this.url)) { + // If it already exists then set the value with the current time + this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()); + } else { + // Otherwise add a new '_' parameter to the end with the current time + var reQueryString = /\?/; + this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime(); + } + } + } + } + + Request.prototype.clone = function() { + return new Request(this, {body: this._bodyInit}) + }; + + function decode(body) { + var form = new FormData(); + body + .trim() + .split('&') + .forEach(function(bytes) { + if (bytes) { + var split = bytes.split('='); + var name = split.shift().replace(/\+/g, ' '); + var value = split.join('=').replace(/\+/g, ' '); + form.append(decodeURIComponent(name), decodeURIComponent(value)); + } + }); + return form + } + + function parseHeaders(rawHeaders) { + var headers = new Headers(); + // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space + // https://tools.ietf.org/html/rfc7230#section-3.2 + var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); + // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill + // https://github.com/github/fetch/issues/748 + // https://github.com/zloirock/core-js/issues/751 + preProcessedHeaders + .split('\r') + .map(function(header) { + return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header + }) + .forEach(function(line) { + var parts = line.split(':'); + var key = parts.shift().trim(); + if (key) { + var value = parts.join(':').trim(); + try { + headers.append(key, value); + } catch (error) { + console.warn('Response ' + error.message); + } + } + }); + return headers + } + + Body.call(Request.prototype); + + function Response(bodyInit, options) { + if (!(this instanceof Response)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + if (!options) { + options = {}; + } + + this.type = 'default'; + this.status = options.status === undefined ? 200 : options.status; + if (this.status < 200 || this.status > 599) { + throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].") + } + this.ok = this.status >= 200 && this.status < 300; + this.statusText = options.statusText === undefined ? '' : '' + options.statusText; + this.headers = new Headers(options.headers); + this.url = options.url || ''; + this._initBody(bodyInit); + } + + Body.call(Response.prototype); + + Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) + }; + + Response.error = function() { + var response = new Response(null, {status: 200, statusText: ''}); + response.ok = false; + response.status = 0; + response.type = 'error'; + return response + }; + + var redirectStatuses = [301, 302, 303, 307, 308]; + + Response.redirect = function(url, status) { + if (redirectStatuses.indexOf(status) === -1) { + throw new RangeError('Invalid status code') + } + + return new Response(null, {status: status, headers: {location: url}}) + }; + + exports.DOMException = g.DOMException; + try { + new exports.DOMException(); + } catch (err) { + exports.DOMException = function(message, name) { + this.message = message; + this.name = name; + var error = Error(message); + this.stack = error.stack; + }; + exports.DOMException.prototype = Object.create(Error.prototype); + exports.DOMException.prototype.constructor = exports.DOMException; + } + + function fetch(input, init) { + return new Promise(function(resolve, reject) { + var request = new Request(input, init); + + if (request.signal && request.signal.aborted) { + return reject(new exports.DOMException('Aborted', 'AbortError')) + } + + var xhr = new XMLHttpRequest(); + + function abortXhr() { + xhr.abort(); + } + + xhr.onload = function() { + var options = { + statusText: xhr.statusText, + headers: parseHeaders(xhr.getAllResponseHeaders() || '') + }; + // This check if specifically for when a user fetches a file locally from the file system + // Only if the status is out of a normal range + if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) { + options.status = 200; + } else { + options.status = xhr.status; + } + options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); + var body = 'response' in xhr ? xhr.response : xhr.responseText; + setTimeout(function() { + resolve(new Response(body, options)); + }, 0); + }; + + xhr.onerror = function() { + setTimeout(function() { + reject(new TypeError('Network request failed')); + }, 0); + }; + + xhr.ontimeout = function() { + setTimeout(function() { + reject(new TypeError('Network request timed out')); + }, 0); + }; + + xhr.onabort = function() { + setTimeout(function() { + reject(new exports.DOMException('Aborted', 'AbortError')); + }, 0); + }; + + function fixUrl(url) { + try { + return url === '' && g.location.href ? g.location.href : url + } catch (e) { + return url + } + } + + xhr.open(request.method, fixUrl(request.url), true); + + if (request.credentials === 'include') { + xhr.withCredentials = true; + } else if (request.credentials === 'omit') { + xhr.withCredentials = false; + } + + if ('responseType' in xhr) { + if (support.blob) { + xhr.responseType = 'blob'; + } else if ( + support.arrayBuffer + ) { + xhr.responseType = 'arraybuffer'; + } + } + + if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) { + var names = []; + Object.getOwnPropertyNames(init.headers).forEach(function(name) { + names.push(normalizeName(name)); + xhr.setRequestHeader(name, normalizeValue(init.headers[name])); + }); + request.headers.forEach(function(value, name) { + if (names.indexOf(name) === -1) { + xhr.setRequestHeader(name, value); + } + }); + } else { + request.headers.forEach(function(value, name) { + xhr.setRequestHeader(name, value); + }); + } + + if (request.signal) { + request.signal.addEventListener('abort', abortXhr); + + xhr.onreadystatechange = function() { + // DONE (success or failure) + if (xhr.readyState === 4) { + request.signal.removeEventListener('abort', abortXhr); + } + }; + } + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit); + }) + } + + fetch.polyfill = true; + + if (!g.fetch) { + g.fetch = fetch; + g.Headers = Headers; + g.Request = Request; + g.Response = Response; + } + + exports.Headers = Headers; + exports.Request = Request; + exports.Response = Response; + exports.fetch = fetch; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/src/polyfill/fetch.zig b/src/polyfill/fetch.zig index 1414d56f..fc0fcc09 100644 --- a/src/polyfill/fetch.zig +++ b/src/polyfill/fetch.zig @@ -3,7 +3,7 @@ const jsruntime = @import("jsruntime"); const Case = jsruntime.test_utils.Case; const checkCases = jsruntime.test_utils.checkCases; -// fetch_polyfill js code comes from +// fetch.js code comes from // https://github.com/JakeChampion/fetch/blob/main/fetch.js // // The original code source is available in MIT license. @@ -14,671 +14,7 @@ const checkCases = jsruntime.test_utils.checkCases; // wget $(npm view whatwg-fetch dist.tarball) // // The source is the content of `package/dist/fetch.umd.js` file. -pub const source = - \\(function (global, factory) { - \\ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - \\ typeof define === 'function' && define.amd ? define(['exports'], factory) : - \\ (factory((global.WHATWGFetch = {}))); - \\}(this, (function (exports) { 'use strict'; - \\ - \\ /* eslint-disable no-prototype-builtins */ - \\ var g = - \\ (typeof globalThis !== 'undefined' && globalThis) || - \\ (typeof self !== 'undefined' && self) || - \\ // eslint-disable-next-line no-undef - \\ (typeof global !== 'undefined' && global) || - \\ {}; - \\ - \\ var support = { - \\ searchParams: 'URLSearchParams' in g, - \\ iterable: 'Symbol' in g && 'iterator' in Symbol, - \\ blob: - \\ 'FileReader' in g && - \\ 'Blob' in g && - \\ (function() { - \\ try { - \\ new Blob(); - \\ return true - \\ } catch (e) { - \\ return false - \\ } - \\ })(), - \\ formData: 'FormData' in g, - \\ - - // Arraybuffer is available but xhr doesn't implement it for now. - //\\ arrayBuffer: 'ArrayBuffer' in g - \\ arrayBuffer: false - // - - \\ }; - \\ - \\ function isDataView(obj) { - \\ return obj && DataView.prototype.isPrototypeOf(obj) - \\ } - \\ - \\ if (support.arrayBuffer) { - \\ var viewClasses = [ - \\ '[object Int8Array]', - \\ '[object Uint8Array]', - \\ '[object Uint8ClampedArray]', - \\ '[object Int16Array]', - \\ '[object Uint16Array]', - \\ '[object Int32Array]', - \\ '[object Uint32Array]', - \\ '[object Float32Array]', - \\ '[object Float64Array]' - \\ ]; - \\ - \\ var isArrayBufferView = - \\ ArrayBuffer.isView || - \\ function(obj) { - \\ return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 - \\ }; - \\ } - \\ - \\ function normalizeName(name) { - \\ if (typeof name !== 'string') { - \\ name = String(name); - \\ } - \\ if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { - \\ throw new TypeError('Invalid character in header field name: "' + name + '"') - \\ } - \\ return name.toLowerCase() - \\ } - \\ - \\ function normalizeValue(value) { - \\ if (typeof value !== 'string') { - \\ value = String(value); - \\ } - \\ return value - \\ } - \\ - \\ // Build a destructive iterator for the value list - \\ function iteratorFor(items) { - \\ var iterator = { - \\ next: function() { - \\ var value = items.shift(); - \\ return {done: value === undefined, value: value} - \\ } - \\ }; - \\ - \\ if (support.iterable) { - \\ iterator[Symbol.iterator] = function() { - \\ return iterator - \\ }; - \\ } - \\ - \\ return iterator - \\ } - \\ - \\ function Headers(headers) { - \\ this.map = {}; - \\ - \\ if (headers instanceof Headers) { - \\ headers.forEach(function(value, name) { - \\ this.append(name, value); - \\ }, this); - \\ } else if (Array.isArray(headers)) { - \\ headers.forEach(function(header) { - \\ if (header.length != 2) { - \\ throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length) - \\ } - \\ this.append(header[0], header[1]); - \\ }, this); - \\ } else if (headers) { - \\ Object.getOwnPropertyNames(headers).forEach(function(name) { - \\ this.append(name, headers[name]); - \\ }, this); - \\ } - \\ } - \\ - \\ Headers.prototype.append = function(name, value) { - \\ name = normalizeName(name); - \\ value = normalizeValue(value); - \\ var oldValue = this.map[name]; - \\ this.map[name] = oldValue ? oldValue + ', ' + value : value; - \\ }; - \\ - \\ Headers.prototype['delete'] = function(name) { - \\ delete this.map[normalizeName(name)]; - \\ }; - \\ - \\ Headers.prototype.get = function(name) { - \\ name = normalizeName(name); - \\ return this.has(name) ? this.map[name] : null - \\ }; - \\ - \\ Headers.prototype.has = function(name) { - \\ return this.map.hasOwnProperty(normalizeName(name)) - \\ }; - \\ - \\ Headers.prototype.set = function(name, value) { - \\ this.map[normalizeName(name)] = normalizeValue(value); - \\ }; - \\ - \\ Headers.prototype.forEach = function(callback, thisArg) { - \\ for (var name in this.map) { - \\ if (this.map.hasOwnProperty(name)) { - \\ callback.call(thisArg, this.map[name], name, this); - \\ } - \\ } - \\ }; - \\ - \\ Headers.prototype.keys = function() { - \\ var items = []; - \\ this.forEach(function(value, name) { - \\ items.push(name); - \\ }); - \\ return iteratorFor(items) - \\ }; - \\ - \\ Headers.prototype.values = function() { - \\ var items = []; - \\ this.forEach(function(value) { - \\ items.push(value); - \\ }); - \\ return iteratorFor(items) - \\ }; - \\ - \\ Headers.prototype.entries = function() { - \\ var items = []; - \\ this.forEach(function(value, name) { - \\ items.push([name, value]); - \\ }); - \\ return iteratorFor(items) - \\ }; - \\ - \\ if (support.iterable) { - \\ Headers.prototype[Symbol.iterator] = Headers.prototype.entries; - \\ } - \\ - \\ function consumed(body) { - \\ if (body._noBody) return - \\ if (body.bodyUsed) { - \\ return Promise.reject(new TypeError('Already read')) - \\ } - \\ body.bodyUsed = true; - \\ } - \\ - \\ function fileReaderReady(reader) { - \\ return new Promise(function(resolve, reject) { - \\ reader.onload = function() { - \\ resolve(reader.result); - \\ }; - \\ reader.onerror = function() { - \\ reject(reader.error); - \\ }; - \\ }) - \\ } - \\ - \\ function readBlobAsArrayBuffer(blob) { - \\ var reader = new FileReader(); - \\ var promise = fileReaderReady(reader); - \\ reader.readAsArrayBuffer(blob); - \\ return promise - \\ } - \\ - \\ function readBlobAsText(blob) { - \\ var reader = new FileReader(); - \\ var promise = fileReaderReady(reader); - \\ var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type); - \\ var encoding = match ? match[1] : 'utf-8'; - \\ reader.readAsText(blob, encoding); - \\ return promise - \\ } - \\ - \\ function readArrayBufferAsText(buf) { - \\ var view = new Uint8Array(buf); - \\ var chars = new Array(view.length); - \\ - \\ for (var i = 0; i < view.length; i++) { - \\ chars[i] = String.fromCharCode(view[i]); - \\ } - \\ return chars.join('') - \\ } - \\ - \\ function bufferClone(buf) { - \\ if (buf.slice) { - \\ return buf.slice(0) - \\ } else { - \\ var view = new Uint8Array(buf.byteLength); - \\ view.set(new Uint8Array(buf)); - \\ return view.buffer - \\ } - \\ } - \\ - \\ function Body() { - \\ this.bodyUsed = false; - \\ - \\ this._initBody = function(body) { - \\ /* - \\ fetch-mock wraps the Response object in an ES6 Proxy to - \\ provide useful test harness features such as flush. However, on - \\ ES5 browsers without fetch or Proxy support pollyfills must be used; - \\ the proxy-pollyfill is unable to proxy an attribute unless it exists - \\ on the object before the Proxy is created. This change ensures - \\ Response.bodyUsed exists on the instance, while maintaining the - \\ semantic of setting Request.bodyUsed in the constructor before - \\ _initBody is called. - \\ */ - \\ // eslint-disable-next-line no-self-assign - \\ this.bodyUsed = this.bodyUsed; - \\ this._bodyInit = body; - \\ if (!body) { - \\ this._noBody = true; - \\ this._bodyText = ''; - \\ } else if (typeof body === 'string') { - \\ this._bodyText = body; - \\ } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { - \\ this._bodyBlob = body; - \\ } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { - \\ this._bodyFormData = body; - \\ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { - \\ this._bodyText = body.toString(); - \\ } else if (support.arrayBuffer && support.blob && isDataView(body)) { - \\ this._bodyArrayBuffer = bufferClone(body.buffer); - \\ // IE 10-11 can't handle a DataView body. - \\ this._bodyInit = new Blob([this._bodyArrayBuffer]); - \\ } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { - \\ this._bodyArrayBuffer = bufferClone(body); - \\ } else { - \\ this._bodyText = body = Object.prototype.toString.call(body); - \\ } - \\ - \\ if (!this.headers.get('content-type')) { - \\ if (typeof body === 'string') { - \\ this.headers.set('content-type', 'text/plain;charset=UTF-8'); - \\ } else if (this._bodyBlob && this._bodyBlob.type) { - \\ this.headers.set('content-type', this._bodyBlob.type); - \\ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { - \\ this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); - \\ } - \\ } - \\ }; - \\ - \\ if (support.blob) { - \\ this.blob = function() { - \\ var rejected = consumed(this); - \\ if (rejected) { - \\ return rejected - \\ } - \\ - \\ if (this._bodyBlob) { - \\ return Promise.resolve(this._bodyBlob) - \\ } else if (this._bodyArrayBuffer) { - \\ return Promise.resolve(new Blob([this._bodyArrayBuffer])) - \\ } else if (this._bodyFormData) { - \\ throw new Error('could not read FormData body as blob') - \\ } else { - \\ return Promise.resolve(new Blob([this._bodyText])) - \\ } - \\ }; - \\ } - \\ - \\ this.arrayBuffer = function() { - \\ if (this._bodyArrayBuffer) { - \\ var isConsumed = consumed(this); - \\ if (isConsumed) { - \\ return isConsumed - \\ } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) { - \\ return Promise.resolve( - \\ this._bodyArrayBuffer.buffer.slice( - \\ this._bodyArrayBuffer.byteOffset, - \\ this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength - \\ ) - \\ ) - \\ } else { - \\ return Promise.resolve(this._bodyArrayBuffer) - \\ } - \\ } else if (support.blob) { - \\ return this.blob().then(readBlobAsArrayBuffer) - \\ } else { - \\ throw new Error('could not read as ArrayBuffer') - \\ } - \\ }; - \\ - \\ this.text = function() { - \\ var rejected = consumed(this); - \\ if (rejected) { - \\ return rejected - \\ } - \\ - \\ if (this._bodyBlob) { - \\ return readBlobAsText(this._bodyBlob) - \\ } else if (this._bodyArrayBuffer) { - \\ return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) - \\ } else if (this._bodyFormData) { - \\ throw new Error('could not read FormData body as text') - \\ } else { - \\ return Promise.resolve(this._bodyText) - \\ } - \\ }; - \\ - \\ if (support.formData) { - \\ this.formData = function() { - \\ return this.text().then(decode) - \\ }; - \\ } - \\ - \\ this.json = function() { - \\ return this.text().then(JSON.parse) - \\ }; - \\ - \\ return this - \\ } - \\ - \\ // HTTP methods whose capitalization should be normalized - \\ var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']; - \\ - \\ function normalizeMethod(method) { - \\ var upcased = method.toUpperCase(); - \\ return methods.indexOf(upcased) > -1 ? upcased : method - \\ } - \\ - \\ function Request(input, options) { - \\ if (!(this instanceof Request)) { - \\ throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') - \\ } - \\ - \\ options = options || {}; - \\ var body = options.body; - \\ - \\ if (input instanceof Request) { - \\ if (input.bodyUsed) { - \\ throw new TypeError('Already read') - \\ } - \\ this.url = input.url; - \\ this.credentials = input.credentials; - \\ if (!options.headers) { - \\ this.headers = new Headers(input.headers); - \\ } - \\ this.method = input.method; - \\ this.mode = input.mode; - \\ this.signal = input.signal; - \\ if (!body && input._bodyInit != null) { - \\ body = input._bodyInit; - \\ input.bodyUsed = true; - \\ } - \\ } else { - \\ this.url = String(input); - \\ } - \\ - \\ this.credentials = options.credentials || this.credentials || 'same-origin'; - \\ if (options.headers || !this.headers) { - \\ this.headers = new Headers(options.headers); - \\ } - \\ this.method = normalizeMethod(options.method || this.method || 'GET'); - \\ this.mode = options.mode || this.mode || null; - \\ this.signal = options.signal || this.signal || (function () { - \\ if ('AbortController' in g) { - \\ var ctrl = new AbortController(); - \\ return ctrl.signal; - \\ } - \\ }()); - \\ this.referrer = null; - \\ - \\ if ((this.method === 'GET' || this.method === 'HEAD') && body) { - \\ throw new TypeError('Body not allowed for GET or HEAD requests') - \\ } - \\ this._initBody(body); - \\ - \\ if (this.method === 'GET' || this.method === 'HEAD') { - \\ if (options.cache === 'no-store' || options.cache === 'no-cache') { - \\ // Search for a '_' parameter in the query string - \\ var reParamSearch = /([?&])_=[^&]*/; - \\ if (reParamSearch.test(this.url)) { - \\ // If it already exists then set the value with the current time - \\ this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()); - \\ } else { - \\ // Otherwise add a new '_' parameter to the end with the current time - \\ var reQueryString = /\?/; - \\ this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime(); - \\ } - \\ } - \\ } - \\ } - \\ - \\ Request.prototype.clone = function() { - \\ return new Request(this, {body: this._bodyInit}) - \\ }; - \\ - \\ function decode(body) { - \\ var form = new FormData(); - \\ body - \\ .trim() - \\ .split('&') - \\ .forEach(function(bytes) { - \\ if (bytes) { - \\ var split = bytes.split('='); - \\ var name = split.shift().replace(/\+/g, ' '); - \\ var value = split.join('=').replace(/\+/g, ' '); - \\ form.append(decodeURIComponent(name), decodeURIComponent(value)); - \\ } - \\ }); - \\ return form - \\ } - \\ - \\ function parseHeaders(rawHeaders) { - \\ var headers = new Headers(); - \\ // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space - \\ // https://tools.ietf.org/html/rfc7230#section-3.2 - \\ var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); - \\ // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill - \\ // https://github.com/github/fetch/issues/748 - \\ // https://github.com/zloirock/core-js/issues/751 - \\ preProcessedHeaders - \\ .split('\r') - \\ .map(function(header) { - \\ return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header - \\ }) - \\ .forEach(function(line) { - \\ var parts = line.split(':'); - \\ var key = parts.shift().trim(); - \\ if (key) { - \\ var value = parts.join(':').trim(); - \\ try { - \\ headers.append(key, value); - \\ } catch (error) { - \\ console.warn('Response ' + error.message); - \\ } - \\ } - \\ }); - \\ return headers - \\ } - \\ - \\ Body.call(Request.prototype); - \\ - \\ function Response(bodyInit, options) { - \\ if (!(this instanceof Response)) { - \\ throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') - \\ } - \\ if (!options) { - \\ options = {}; - \\ } - \\ - \\ this.type = 'default'; - \\ this.status = options.status === undefined ? 200 : options.status; - \\ if (this.status < 200 || this.status > 599) { - \\ throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].") - \\ } - \\ this.ok = this.status >= 200 && this.status < 300; - \\ this.statusText = options.statusText === undefined ? '' : '' + options.statusText; - \\ this.headers = new Headers(options.headers); - \\ this.url = options.url || ''; - \\ this._initBody(bodyInit); - \\ } - \\ - \\ Body.call(Response.prototype); - \\ - \\ Response.prototype.clone = function() { - \\ return new Response(this._bodyInit, { - \\ status: this.status, - \\ statusText: this.statusText, - \\ headers: new Headers(this.headers), - \\ url: this.url - \\ }) - \\ }; - \\ - \\ Response.error = function() { - \\ var response = new Response(null, {status: 200, statusText: ''}); - \\ response.ok = false; - \\ response.status = 0; - \\ response.type = 'error'; - \\ return response - \\ }; - \\ - \\ var redirectStatuses = [301, 302, 303, 307, 308]; - \\ - \\ Response.redirect = function(url, status) { - \\ if (redirectStatuses.indexOf(status) === -1) { - \\ throw new RangeError('Invalid status code') - \\ } - \\ - \\ return new Response(null, {status: status, headers: {location: url}}) - \\ }; - \\ - \\ exports.DOMException = g.DOMException; - \\ try { - \\ new exports.DOMException(); - \\ } catch (err) { - \\ exports.DOMException = function(message, name) { - \\ this.message = message; - \\ this.name = name; - \\ var error = Error(message); - \\ this.stack = error.stack; - \\ }; - \\ exports.DOMException.prototype = Object.create(Error.prototype); - \\ exports.DOMException.prototype.constructor = exports.DOMException; - \\ } - \\ - \\ function fetch(input, init) { - \\ return new Promise(function(resolve, reject) { - \\ var request = new Request(input, init); - \\ - \\ if (request.signal && request.signal.aborted) { - \\ return reject(new exports.DOMException('Aborted', 'AbortError')) - \\ } - \\ - \\ var xhr = new XMLHttpRequest(); - \\ - \\ function abortXhr() { - \\ xhr.abort(); - \\ } - \\ - \\ xhr.onload = function() { - \\ var options = { - \\ statusText: xhr.statusText, - \\ headers: parseHeaders(xhr.getAllResponseHeaders() || '') - \\ }; - \\ // This check if specifically for when a user fetches a file locally from the file system - \\ // Only if the status is out of a normal range - \\ if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) { - \\ options.status = 200; - \\ } else { - \\ options.status = xhr.status; - \\ } - \\ options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); - \\ var body = 'response' in xhr ? xhr.response : xhr.responseText; - \\ setTimeout(function() { - \\ resolve(new Response(body, options)); - \\ }, 0); - \\ }; - \\ - \\ xhr.onerror = function() { - \\ setTimeout(function() { - \\ reject(new TypeError('Network request failed')); - \\ }, 0); - \\ }; - \\ - \\ xhr.ontimeout = function() { - \\ setTimeout(function() { - \\ reject(new TypeError('Network request timed out')); - \\ }, 0); - \\ }; - \\ - \\ xhr.onabort = function() { - \\ setTimeout(function() { - \\ reject(new exports.DOMException('Aborted', 'AbortError')); - \\ }, 0); - \\ }; - \\ - \\ function fixUrl(url) { - \\ try { - \\ return url === '' && g.location.href ? g.location.href : url - \\ } catch (e) { - \\ return url - \\ } - \\ } - \\ - \\ xhr.open(request.method, fixUrl(request.url), true); - \\ - \\ if (request.credentials === 'include') { - \\ xhr.withCredentials = true; - \\ } else if (request.credentials === 'omit') { - \\ xhr.withCredentials = false; - \\ } - \\ - \\ if ('responseType' in xhr) { - \\ if (support.blob) { - \\ xhr.responseType = 'blob'; - \\ } else if ( - \\ support.arrayBuffer - \\ ) { - \\ xhr.responseType = 'arraybuffer'; - \\ } - \\ } - \\ - \\ if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) { - \\ var names = []; - \\ Object.getOwnPropertyNames(init.headers).forEach(function(name) { - \\ names.push(normalizeName(name)); - \\ xhr.setRequestHeader(name, normalizeValue(init.headers[name])); - \\ }); - \\ request.headers.forEach(function(value, name) { - \\ if (names.indexOf(name) === -1) { - \\ xhr.setRequestHeader(name, value); - \\ } - \\ }); - \\ } else { - \\ request.headers.forEach(function(value, name) { - \\ xhr.setRequestHeader(name, value); - \\ }); - \\ } - \\ - \\ if (request.signal) { - \\ request.signal.addEventListener('abort', abortXhr); - \\ - \\ xhr.onreadystatechange = function() { - \\ // DONE (success or failure) - \\ if (xhr.readyState === 4) { - \\ request.signal.removeEventListener('abort', abortXhr); - \\ } - \\ }; - \\ } - \\ - \\ xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit); - \\ }) - \\ } - \\ - \\ fetch.polyfill = true; - \\ - \\ if (!g.fetch) { - \\ g.fetch = fetch; - \\ g.Headers = Headers; - \\ g.Request = Request; - \\ g.Response = Response; - \\ } - \\ - \\ exports.Headers = Headers; - \\ exports.Request = Request; - \\ exports.Response = Response; - \\ exports.fetch = fetch; - \\ - \\ Object.defineProperty(exports, '__esModule', { value: true }); - \\ - \\}))); -; +pub const source = @embedFile("fetch.js"); pub fn testExecFn( alloc: std.mem.Allocator,