1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-12-04 14:46:47 -05:00

[spidermonkey] polyfilled fetch

fetch.js code from https://github.com/JakeChampion/fetch
modified to pass test assertion.
Module import in spidermonkey based on examples.
This commit is contained in:
Witold Filipczyk 2024-07-03 20:25:11 +02:00
parent 55979c1551
commit 31e4eb240a
10 changed files with 793 additions and 1 deletions

View File

@ -345,6 +345,9 @@ ecmascript_put_interpreter(struct ecmascript_interpreter *interpreter)
}
free_list(interpreter->timeouts);
}
#ifdef CONFIG_ECMASCRIPT_SMJS
//js::StopDrainingJobQueue((JSContext *)interpreter->backend_data);
#endif
interpreter->vs->ecmascript = NULL;
interpreter->vs->ecmascript_fragile = 1;
mem_free(interpreter);

View File

@ -277,12 +277,23 @@ delayed_reload(void *data)
mem_free(rel);
}
static void
run_jobs(void *data)
{
#ifdef CONFIG_ECMASCRIPT_SMJS
struct ecmascript_interpreter *interpreter = (struct ecmascript_interpreter *)data;
js::RunJobs((JSContext *)interpreter->backend_data);
#endif
}
void
check_for_rerender(struct ecmascript_interpreter *interpreter, const char* text)
{
#ifdef ECMASCRIPT_DEBUG
fprintf(stderr, "%s:%s %s %d\n", __FILE__, __FUNCTION__, text, interpreter->changed);
#endif
run_jobs(interpreter);
if (interpreter->changed && !program.testjs) {
struct document_view *doc_view = interpreter->vs->doc_view;
struct document *document = doc_view->document;

642
src/ecmascript/fetch.js Normal file
View File

@ -0,0 +1,642 @@
/* 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
}
export 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
}
export 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)
export 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}})
}
export var DOMException = g.DOMException
try {
new DOMException()
} catch (err) {
DOMException = function(message, name) {
this.message = message
this.name = name
var error = Error(message)
this.stack = error.stack
}
DOMException.prototype = Object.create(Error.prototype)
DOMException.prototype.constructor = DOMException
}
export 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 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 = 'responseText' in xhr ? xhr.responseText : xhr.response
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 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
}

View File

@ -1,5 +1,8 @@
#INCLUDES += $(SPIDERMONKEY_CFLAGS)
if conf_data.get('CONFIG_ECMASCRIPT_SMJS')
xxd = find_program('xxd')
run_command([xxd, '-i', 'fetch.js', 'fetch.h'])
srcs += files('ecmascript.cpp', 'ecmascript-c.cpp', 'localstorage-db.c', 'spidermonkey.cpp', 'timer.c')
subdir('spidermonkey')
endif

View File

@ -41,6 +41,7 @@ spidermonkey_runtime_addref(void)
JS_ShutDown();
return 0;
}
js::UseInternalJobQueues(main_ctx);
if (!JS::InitSelfHostedCode(main_ctx)) {
JS_DestroyContext(main_ctx);

View File

@ -65,13 +65,18 @@
#include "viewer/text/view.h"
#include "viewer/text/vs.h"
#include <jsapi.h>
#include <js/CompilationAndEvaluation.h>
#include <js/Printf.h>
#include <js/SourceText.h>
#include <js/Warnings.h>
#include <js/Modules.h>
#include <map>
/*** Global methods */
#include "ecmascript/fetch.h"
/* TODO? Are there any which need to be implemented? */
@ -145,6 +150,83 @@ spidermonkey_done(struct module *xxx)
spidermonkey_runtime_release();
}
static JSObject*
CompileExampleModule(JSContext* cx, const char* filename, const char* code, size_t len)
{
JS::CompileOptions options(cx);
options.setFileAndLine(filename, 1);
JS::SourceText<mozilla::Utf8Unit> source;
if (!len) {
len = strlen(code);
}
if (!source.init(cx, code, len, JS::SourceOwnership::Borrowed)) {
return nullptr;
}
// Compile the module source to bytecode.
//
// NOTE: This generates a JSObject instead of a JSScript. This contains
// additional metadata to resolve imports/exports. This object should not be
// exposed to other JS code or unexpected behaviour may occur.
return JS::CompileModule(cx, options, source);
}
// Maintain a registry of imported modules. The ResolveHook may be called
// multiple times for the same specifier and we need to return the same compiled
// module.
//
// NOTE: This example assumes only one JSContext/GlobalObject is used, but in
// general the registry needs to be distinct for each GlobalObject.
static std::map<std::u16string, JS::PersistentRootedObject> moduleRegistry;
// Callback for embedding to provide modules for import statements. This example
// hardcodes sources, but an embedding would normally load files here.
static JSObject*
ExampleResolveHook(JSContext* cx, JS::HandleValue modulePrivate, JS::HandleObject moduleRequest)
{
// Extract module specifier string.
JS::Rooted<JSString*> specifierString(cx, JS::GetModuleRequestSpecifier(cx, moduleRequest));
if (!specifierString) {
return nullptr;
}
// Convert specifier to a std::u16char for simplicity.
JS::UniqueTwoByteChars specChars(JS_CopyStringCharsZ(cx, specifierString));
if (!specChars) {
return nullptr;
}
std::u16string filename(specChars.get());
// If we already resolved before, return same module.
auto search = moduleRegistry.find(filename);
if (search != moduleRegistry.end()) {
return search->second;
}
JS::RootedObject mod(cx);
if (filename == u"a") {
mod = CompileExampleModule(cx, "a", (const char *)fetch_js, (size_t)fetch_js_len);
if (!mod) {
return nullptr;
}
}
// Register result in table.
if (mod) {
moduleRegistry.emplace(filename, JS::PersistentRootedObject(cx, mod));
return mod;
}
JS_ReportErrorASCII(cx, "Cannot resolve import specifier");
return nullptr;
}
void *
spidermonkey_get_interpreter(struct ecmascript_interpreter *interpreter)
@ -176,6 +258,10 @@ spidermonkey_get_interpreter(struct ecmascript_interpreter *interpreter)
JS_AddInterruptCallback(ctx, heartbeat_callback);
JS::RealmOptions options;
JS::RootedObject global(ctx);
JS::RootedObject mod(ctx);
JS::CompileOptions copt(ctx);
JS::SourceText<mozilla::Utf8Unit> srcBuf;
JS::RootedValue rval(ctx);
JS::Heap<JSObject*> *window_obj = new JS::Heap<JSObject*>(JS_NewGlobalObject(ctx, &window_class, NULL, JS::FireOnNewGlobalHook, options));
@ -354,10 +440,29 @@ spidermonkey_get_interpreter(struct ecmascript_interpreter *interpreter)
urlSearchParams_funcs,
NULL, NULL, "URLSearchParams");
if (!url_obj) {
if (!urlSearchParams_obj) {
goto release_and_fail;
}
#if 1
// Register a hook in order to provide modules
JS::SetModuleResolveHook(JS_GetRuntime(ctx), ExampleResolveHook);
mod = CompileExampleModule(ctx, "top", "import {fetch,Headers,Request,Response} from 'a';", 0);
if (!mod) {
goto release_and_fail;
}
// Resolve imports by loading and compiling additional scripts.
if (!JS::ModuleLink(ctx, mod)) {
goto release_and_fail;
}
// Result value, used for top-level await.
// Execute the module bytecode.
if (!JS::ModuleEvaluate(ctx, mod, &rval)) {
goto release_and_fail;
}
#endif
JS::SetRealmPrivate(js::GetContextRealm(ctx), interpreter);
return ctx;

View File

@ -868,6 +868,7 @@ xhr_loading_callback(struct download *download, struct xhr *xhr)
if (!fragment) {
return;
}
if (cached->head) {
std::istringstream headers(cached->head);
@ -920,6 +921,8 @@ xhr_loading_callback(struct download *download, struct xhr *xhr)
}
xhr->status = status;
mem_free_set(&xhr->statusText, stracpy(statusText.c_str()));
} else {
xhr->status = 200;
}
mem_free_set(&xhr->responseText, memacpy(fragment->data, fragment->length));
mem_free_set(&xhr->responseType, stracpy(""));

View File

@ -0,0 +1,22 @@
<script>
async function getData() {
const url = "hello.html";
try {
const response = await fetch(url);
if (!response.ok) {
console.error(`Response status: ${response.status}`);
}
const txt = await response.text();
return txt;
} catch (error) {
console.error(error.message);
}
}
console.error("fetch.html");
getData().then(text => {
console.assert(text === 'Hello', 'hello');
console.exit();
});
</script>

View File

@ -0,0 +1 @@
Hello

View File

@ -69,6 +69,7 @@ took = [
'element.textContent.html',
#'event.html',
'eventListener.html',
'fetch.html',
'keyboardEvent.html',
'navigator.appCodeName.html',
'nodelist.forEach.html',