diff --git a/contributed/httpbridge/.gitignore b/contributed/httpbridge/.gitignore new file mode 100644 index 0000000000..b512c09d47 --- /dev/null +++ b/contributed/httpbridge/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/contributed/httpbridge/README.md b/contributed/httpbridge/README.md new file mode 100644 index 0000000000..580e238d4b --- /dev/null +++ b/contributed/httpbridge/README.md @@ -0,0 +1,33 @@ +# Moddable http bridge example + +This example shows how to use the http bridge components. + +It starts a bi-directional websocket between the Moddable server and a browser. If you start another browser instance, changes on one browser reflect in the other. + +### build the zip file +``` +cd site +npm install +npm run build +``` + +### build moddable +From the site folder: +``` +npm run mcconfig +``` + +This will build the `site.zip` and launch the simulator + +open browser `http://localhost` + +### front end development +``` +`npm run dev` +or +`wmr` + +open browser `http://localhost:8080` + +Edit any ts or css file, and on save the browser will auto-update with changes, and the `websocket` is connected to the simulator Modable server + diff --git a/contributed/httpbridge/main.js b/contributed/httpbridge/main.js new file mode 100644 index 0000000000..fff1bf7a2e --- /dev/null +++ b/contributed/httpbridge/main.js @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ +import { WebServer as HTTPServer } from "bridge/webserver"; +import { BridgeWebsocket } from "bridge/websocket"; +import { BridgeHttpZip } from "bridge/httpzip"; +import Preference from "preference"; + +const http = new HTTPServer({ + port: 80, +}); + +let ws = http.use(new BridgeWebsocket("/api")); +http.use(new BridgeHttpZip("site.zip")); + +class App { + #preference_domain = "bridge"; + #model; + + constructor(m) { + this.#model = m; + } + + get model() { + return this.#model; + } + + set model(m) { + this.#model = m; + } + + minus(value) { + this.model.satisfaction = Math.max(0, this.model.satisfaction - 1); + return this.model; + } + plus(value) { + this.model.satisfaction = Math.min(10, this.model.satisfaction + 1); + return this.model; + } + shutdown() { + ws.close(); + http.close(); + } + language() { + this.model.language = value.language; + return this.model; + } + restore() { + let keys = Preference.keys(this.#preference_domain); + for (let key of keys) { + let pref_settings = Preference.get(this.#preference_domain, key); + if (pref_settings) { + Object.assign(this.model, JSON.parse(pref_settings)); + } + } + return this.model; + } + save() { + Preference.set( + this.#preference_domain, + "settings", + JSON.stringify(this.model) + ); + } +} + +import { _model } from "model"; +const app = new App({ ..._model }); +app.restore(); + +ws.callback = function cb(websock, message, value) { + switch (message) { + case BridgeWebsocket.connect: + break; + + case BridgeWebsocket.handshake: + websock.broadcast(app.model); + break; + + case BridgeWebsocket.receive: + try { + trace(`Main WebSocket receive: ${value}\n`); + value = JSON.parse(value); + + let action = value?.action; + + if (typeof app[action] === "function") { + value = app[action](value); + } else { + if (value.hasOwnProperty('language')) { + Object.assign(app.model,value) + } else { + trace("No matching action found\n"); + } + } + if (value) websock.broadcast(value); + } catch (e) { + trace(`WebSocket parse received data error: ${e}\n`); + } + } +}; diff --git a/contributed/httpbridge/manifest.json b/contributed/httpbridge/manifest.json new file mode 100644 index 0000000000..2b2d93c7b8 --- /dev/null +++ b/contributed/httpbridge/manifest.json @@ -0,0 +1,24 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json", + "$(MODDABLE)/examples/manifest_net.json", + "$(MODULES)/network/http/manifest.json", + "$(MODULES)/network/websocket/manifest.json", + "$(MODULES)/files/zip/manifest.json", + "$(MODULES)/files/preference/manifest.json" + ], + "modules": { + "*": [ + "./main" + ], + "bridge/*": "$(MODDABLE)/contributed/httpbridge/modules/*", + "model":"site/public/model" + }, + "preload": [ + "model", + "bridge/httpzip" + ], + "data": { + "*": "./site/dist/site" + } +} diff --git a/contributed/httpbridge/modules/hotspot.js b/contributed/httpbridge/modules/hotspot.js new file mode 100644 index 0000000000..be9a33b5b9 --- /dev/null +++ b/contributed/httpbridge/modules/hotspot.js @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import { Bridge, HTTPServer } from "bridge/webserver"; +import Net from "net" + +const hotspot = new Map; + +// iOS 8/9 +hotspot.set("/library/test/success.html",{status: 302,body: "Success"}); +hotspot.set("/hotspot-detect.html",{status: 302,body: "Success"}); + +// Windows +hotspot.set("/ncsi.txt",{status: 302,body: "Microsoft NCSI"}); +hotspot.set("/connecttest.txt",{status: 302,body: "Microsoft Connect Test"}); +hotspot.set("/redirect",{status: 302,body: ""}); // Win 10 + +// Android +hotspot.set("/mobile/status.php", {status:302}); // Android 8.0 (Samsung s9+) +hotspot.set("/generate_204", {status:302}); // Android actual redirect +hotspot.set("/gen_204", {status:204}); // Android 9.0 + +export class BridgeHotspot extends Bridge { + constructor() { + super(); + } + handler(req, message, value, etc) { + switch (message) { + case HTTPServer.status: + + req.redirect=hotspot.get(value); // value is path + if ( req.redirect) return; // Hotspot url match + delete req.redirect; + return this.next?.handler(req, message, value, etc); + case HTTPServer.header: { + if ( "host" === value ) { + req.host=etc; + trace(`BridgeHotspot: http://${req.host}${req.path}\n`); + } + return this.next?.handler(req, message, value, etc); + } + case HTTPServer.prepareResponse: + + if( req.redirect) { + let apIP=Net.get("IP", "ap"); + let redirect={ + headers: [ "Content-type", "text/plain", "Location",`http://${apIP}`], + ...req.redirect + }; + trace(`Hotspot match: http://${req.host}${req.path}\n`); + trace(JSON.stringify(redirect),'\n'); + + return redirect; + } + } + return this.next?.handler(req, message, value, etc); + } +} +Object.freeze(hotspot); + +/* TO DO +add dns constructor flag. then becomes self contained. +*/ diff --git a/contributed/httpbridge/modules/httpzip.js b/contributed/httpbridge/modules/httpzip.js new file mode 100644 index 0000000000..c3dd2ca7f8 --- /dev/null +++ b/contributed/httpbridge/modules/httpzip.js @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import { Bridge, HTTPServer } from "bridge/webserver"; +import Resource from "Resource"; +import {ZIP} from "zip" + +const mime = new Map; +mime.set("js", "application/javascript"); +mime.set("css", "text/css"); +mime.set("ico", "image/vnd.microsoft.icon"); +mime.set("txt", "text/plain"); +mime.set("htm", "text/html"); +mime.set("html", "text/html"); +mime.set("svg", "image/svg+xml"); +mime.set("png", "image/png"); +mime.set("gif", "image/gif"); +mime.set("webp", "image/webp"); +mime.set("jpg", "image/jpeg"); +mime.set("jpeg", "image/jpeg"); + +export class BridgeHttpZip extends Bridge { + constructor(resource) { + super(); + + this.archive = new ZIP(new Resource(resource)); + } + + handler(req, message, value, etc) { + switch (message) { + case HTTPServer.status: + // redirect home page + if (value === '/') value='/index.html'; + req.path = value; + try { + req.data = this.archive.file(req.path.slice(1)); // drop leading / to match zip content + req.etag = "mod-" + req.data.crc.toString(16); + } + catch { + delete req.data; + delete req.etag; + return this.next?.handler(req, message, value, etc); + } + break; + + case HTTPServer.header: + req.match ||= ("if-none-match" === value) && (req.etag === etc); + return this.next?.handler(req, message, value, etc); + + case HTTPServer.prepareResponse: + if (req.match) { + return { + status: 304, + headers: [ + "ETag", req.etag, + ] + }; + } + if (!req.data) { + trace(`prepareResponse: missing file ${req.path}\n`); + + return this.next?.handler(req, message, value, etc); + } + + req.data.current = 0; + const result = { + headers: [ + "Content-type", mime.get(req.path.split('.').pop()) ?? "text/plain", + "Content-length", req.data.length, + "ETag", req.etag, + "Cache-Control", "max-age=60" + ], + body: true + } + if (8 === req.data.method) // Compression Method + result.headers.push("Content-Encoding", "deflate"); + return result; + + case HTTPServer.responseFragment: + if (req.data.current >= req.data.length) + return; + + const chunk = req.data.read(ArrayBuffer, (value > 1536) ? 1536 : value); + req.data.current += chunk.byteLength; + return chunk; + } + } +} + +Object.freeze(mime); \ No newline at end of file diff --git a/contributed/httpbridge/modules/manifest.json b/contributed/httpbridge/modules/manifest.json new file mode 100644 index 0000000000..a57aa7423c --- /dev/null +++ b/contributed/httpbridge/modules/manifest.json @@ -0,0 +1,25 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json", + "$(MODDABLE)/examples/manifest_net.json", + "$(MODULES)/network/http/manifest.json", + "$(MODULES)/network/websocket/manifest.json", + "$(MODULES)/files/zip/manifest.json" + ], + "modules": { + "*": [ + "$(MODULES)/files/resource/*" + ], + "dns/server": "$(MODULES)/network/dns/dnsserver" + }, + "preload": [ + "http", + "dns/server", + "websocket/websocket", + "base64", + "hex", + "logical", + "resource", + "zip" + ] +} diff --git a/contributed/httpbridge/modules/rewritespa.js b/contributed/httpbridge/modules/rewritespa.js new file mode 100644 index 0000000000..4fdda7c152 --- /dev/null +++ b/contributed/httpbridge/modules/rewritespa.js @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import { Bridge, HTTPServer } from "bridge/webserver"; + +// Single Page application. Map page requests back to / + +export class BridgeRewriteSPA extends Bridge { + #routes + constructor(routes) { + super(); + this.#routes=routes + } + + handler(req, message, value, etc) { + + switch (message) { + case HTTPServer.status: + if ( this.#routes.includes(value) ) { // To do: possibly use Set? + trace(`Rewrite: ${value}\n`); + value='/'; + trace(`Rewrite now: ${value}\n`); + } + break; + } + return this.next?.handler(req, message, value, etc); + } +} diff --git a/contributed/httpbridge/modules/status.js b/contributed/httpbridge/modules/status.js new file mode 100644 index 0000000000..f119b87436 --- /dev/null +++ b/contributed/httpbridge/modules/status.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import { Bridge, HTTPServer } from "bridge/webserver"; + +export class BridgeStatus extends Bridge { + #status; + constructor(status=404) { + super(); + this.#status=status + } + handler(req, message, value, etc) { + switch (message) { + + case HTTPServer.prepareResponse: + return { + status: this.#status + }; + } + } +} diff --git a/contributed/httpbridge/modules/webserver.js b/contributed/httpbridge/modules/webserver.js new file mode 100644 index 0000000000..0945e80c75 --- /dev/null +++ b/contributed/httpbridge/modules/webserver.js @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import {Server as HTTPServer} from "http" + +class Bridge { + #next; + constructor() { + this.#next = null + } + get next() { + return this.#next; + } + set next(n) { + this.#next = n; + } +} + +class WebServer extends HTTPServer { + #last; + #first; + + constructor(options) { + super(options); + + this.#first = null; + this.#last = null; + } + + use( handler ) { + // chaining here, join new to previous + if( this.#first === null ) { + this.#first=this.#last=handler; + } else { + this.#last.next=handler; + this.#last=handler; + } + // Advise Bridge of http parent - needed by websocket for connection management + if ( 'parent' in handler ) + handler.parent=this; + return handler; + } + + callback(message, value, etc) { + switch (message) { + case HTTPServer.status: + this.path=value; + this.method = etc; + break; + case HTTPServer.header: + value=value.toLowerCase(); // Header field names are case-insensitive - force lower for easy compare + } + return this.server.#first?.handler(this, message, value, etc); + } + + close() { + if ( this.connections ) + super.close(); // Close any http connections + } +} + +Object.freeze(WebServer.prototype); + +export { WebServer, HTTPServer, Bridge }; diff --git a/contributed/httpbridge/modules/websocket.js b/contributed/httpbridge/modules/websocket.js new file mode 100644 index 0000000000..1f5594e8e0 --- /dev/null +++ b/contributed/httpbridge/modules/websocket.js @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import {Bridge, HTTPServer} from "bridge/webserver"; +import {Server as WebSocketServer} from "websocket"; + +class WebSocketUpgrade extends WebSocketServer { + static connections = []; + #bridge; + + constructor(opts) { + super(opts); + } + set bridge(value) { + this.#bridge = value; + } + + get bridge() { + return this.#bridge; + } + + callback(message, value) { + const ws = this.ws; + + switch (message) { + case WebSocketServer.connect: + this.ws = value; + break; + + case WebSocketServer.handshake: + WebSocketUpgrade.connections.push(this); + trace(`WebSocket connected: ${WebSocketUpgrade.connections.length}\n`); + break; + + case WebSocketServer.disconnect: + case WebSocketServer.error: + value = WebSocketUpgrade.connections.findIndex(value => value === this); + if (-1 === value) + return; + + WebSocketUpgrade.connections.splice(value, 1); + trace(`WebSocket disconnected: ${WebSocketUpgrade.connections.length}\n`); + break; + + case WebSocketServer.receive: + // App handles.. + break; + } + WebSocketUpgrade.bridge?.callback(ws,message, value ); // call app callback; + } + + broadcast(message, except) { + message = JSON.stringify(message); + trace(`WebSocket send: ${message}\n`); + for (let i = 0, connections = WebSocketUpgrade.connections; i < connections.length; i++) { + const connection = connections[i]; + + if (except === connection) // Don't broadcast to self + continue; + try { + //connection.send(message); + connection.write(message); + } catch { + connections.splice(i, 1); + } + } + } +} + +class BridgeWebsocket extends Bridge { + #path; + #webserver; + constructor( path='/') { + super(); + this.#path=path; + } + + get parent() { + return this.#webserver; + } + set parent(p) { + + this.#webserver = p; + } + + close() { + for (let i = 0, connections = WebSocketUpgrade.connections; i < connections.length; i++) { + try { + connections[i].close() + } catch { + // Silently ignore + } + } + } + + handler(req, message, value, etc) { + switch (message) { + case HTTPServer.status: + if ( value === this.#path ) { + WebSocketUpgrade.bridge=this; + const socket = this.parent.detach(req); + const websocket = new WebSocketUpgrade({port:null}); + websocket.attach(socket); + return; + } + break; + } + return this.next?.handler(req, message, value, etc); + } +} + +BridgeWebsocket.connect = 1; +BridgeWebsocket.handshake = 2; +BridgeWebsocket.receive = 3; +BridgeWebsocket.disconnect = 4; +BridgeWebsocket.subprotocol = 5; +BridgeWebsocket.encode = 6; // Websocket encode packet +BridgeWebsocket.decode = 7; // Websocket decode packet +Object.freeze(BridgeWebsocket.prototype); + +export { BridgeWebsocket }; diff --git a/contributed/httpbridge/site/dist/site.zip b/contributed/httpbridge/site/dist/site.zip new file mode 100644 index 0000000000..b621025eaf Binary files /dev/null and b/contributed/httpbridge/site/dist/site.zip differ diff --git a/contributed/httpbridge/site/package.json b/contributed/httpbridge/site/package.json new file mode 100644 index 0000000000..561cc84710 --- /dev/null +++ b/contributed/httpbridge/site/package.json @@ -0,0 +1,18 @@ +{ + "name": "moddable", + "version": "1.0.0", + "description": "", + "main": "app.ts", + "scripts": { + "dev": "wmr", + "build": "wmr build", + "serve": "wmr serve", + "mcconfig": "wmr build && cd .. && mcconfig -d -m" + }, + "author": "", + "license": "MIT", + "dependencies": { + "rollup-plugin-zip": "^1.0.3", + "wmr": "^3.7.2" + } +} diff --git a/contributed/httpbridge/site/public/app.ts b/contributed/httpbridge/site/public/app.ts new file mode 100644 index 0000000000..cc576ab47e --- /dev/null +++ b/contributed/httpbridge/site/public/app.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2021-2022 Moddable Tech, Inc. + * Copyright (c) Wilberforce + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import { _model } from "./model.js"; + +let observer = { + set: function (obj, prop, value) { + console.log(`set prop: ${prop} ${ JSON.stringify(value)}\n`) + obj[prop] = value + return true; + } + }; + +const model = new Proxy(_model, observer); + +class WebConnect extends WebSocket { + constructor(url) { + super(url); + this.onopen = this.onOpen.bind(this) + this.onmessage = this.onMessage.bind(this) + this.onclose = this.onClose.bind(this) + this.onerror = this.onError.bind(this) + } + + onOpen() { + this.trace("ws open", "e") + this.send({ hello: "world" }) + } + + onMessage(event) { + this.trace(`ws recv: ${event.data}`, "w") + const data = JSON.parse(event.data) + this.model( data ) + } + + model(data) { + if ( data.hasOwnProperty('satisfaction') ) { + model.satisfaction = data.satisfaction + document.getElementById('satisfaction').value = data.satisfaction + } + if ( data.hasOwnProperty('language') ) { + model.language = data.language; + document.getElementById(`language-${data.language}`).checked=true + } + } + + onClose(event: CloseEvent) { + this.trace(`ws close: ${event.code}`, "e") + } + + onError(event: Event) { + this.trace(`ws error: ${event.code}`, "e") + } + + send(msg) { + if (typeof msg === "object") msg = JSON.stringify(msg) + this.trace(`ws sent: ${msg}`); + try { + super.send(msg); + } + catch (e) { + this.trace(`ws error: ${e}`, "e"); + } + } + + trace(msg, cls = "i") { + if (typeof msg === "object") msg = JSON.stringify(msg); + console.log(msg); + msg = new Date().toString().substring(16, 24) + ": " + msg; + document.getElementById("log").innerHTML = `${msg}\n${log.innerHTML}`; + } + + action(act) { + this.send({ action: act }) + } +} + +class App { + ws: WebConnect = null + + constructor() { + const watch = document.querySelectorAll('.e-watch') + watch.forEach(el => { + el.addEventListener("change", (e) => { + let t=e.target + let packet = {} + packet[t.name] = t.value + this.ws.send(packet) + }) + }) + + const actionElements = document.querySelectorAll('.e-action'); + actionElements.forEach(el => { + el.addEventListener('click', (e) => { + + if ( e.target.id === "connect" ) { + this.connect(); + } else { + this.ws.action(e.target.id) + } + }) + }) + this.connect(); + } + + connect() { + if ( this.ws ) { + try { + this.ws.trace(`disconnecting... ${this.ws.readyState}`) + if ( this.ws.readyState === WebSocket.OPEN ) + this.ws.close() + } + catch (e) { + } + delete this.ws; + } + const url = "ws://" + window.location.hostname + "/api" + this.ws = new WebConnect(url) + this.ws.trace("connecting...") + this.ws.model(model) + } +} + +let app = new App(); diff --git a/contributed/httpbridge/site/public/index.html b/contributed/httpbridge/site/public/index.html new file mode 100644 index 0000000000..30f22346cb --- /dev/null +++ b/contributed/httpbridge/site/public/index.html @@ -0,0 +1,42 @@ + + + + + + + +Moddable + + + +
+Moddable +
+ +
+ +
+ +Javascript +Typescript +
+
+ + + +
+
+

Debug Log

+
+

+
+ + + + + +
+ + + + diff --git a/contributed/httpbridge/site/public/moddable.svg b/contributed/httpbridge/site/public/moddable.svg new file mode 100644 index 0000000000..bec1eced59 --- /dev/null +++ b/contributed/httpbridge/site/public/moddable.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/contributed/httpbridge/site/public/model.js b/contributed/httpbridge/site/public/model.js new file mode 100644 index 0000000000..d47baa4a47 --- /dev/null +++ b/contributed/httpbridge/site/public/model.js @@ -0,0 +1,6 @@ +let _model= { + satisfaction: 5, + language: 'javascript' +} + +export { _model }; \ No newline at end of file diff --git a/contributed/httpbridge/site/public/stylesheet.css b/contributed/httpbridge/site/public/stylesheet.css new file mode 100644 index 0000000000..5901aabfb2 --- /dev/null +++ b/contributed/httpbridge/site/public/stylesheet.css @@ -0,0 +1,75 @@ +* { + box-sizing: inherit; +} + +div, +input { + font-size: 1em; + vertical-align:middle; + padding: 4px +} + +body { + font-family: sans-serif; +} + +h1 { + text-align: center; +} + +#log { + padding:0.5em; + text-align: left; + color: wheat; + background-color: #1c1c1c; + overflow-x: scroll; +} + +#log pre { + overflow-x: scroll +} + +#log .v { + color: #888888; +} + +#log .d { + color: #00DDDD; +} + +#log .c { + color: magenta; +} + +#log .i { + color: limegreen; +} + +#log .w { + color: yellow; +} + +#log .e { + color: red; + font-weight: bold; +} + +.flow-x { + overflow-x:auto +} + +.right { + float: right; +} + +button { + border-radius: 1rem; + border-width: 1px; + min-width: 0.8rem; + font-size: 1.2rem; +} + +label { + min-width: 6rem; + float:left; +} \ No newline at end of file diff --git a/contributed/httpbridge/site/wmr.config.mjs b/contributed/httpbridge/site/wmr.config.mjs new file mode 100644 index 0000000000..8776c89f1e --- /dev/null +++ b/contributed/httpbridge/site/wmr.config.mjs @@ -0,0 +1,7 @@ +import zip from 'rollup-plugin-zip'; + +export function build({ plugins }) { + plugins.push( + zip({file: 'site.zip'}) + ); +} diff --git a/modules/network/http/http.js b/modules/network/http/http.js index f58a5dfc53..8d3bd3b591 100644 --- a/modules/network/http/http.js +++ b/modules/network/http/http.js @@ -452,7 +452,7 @@ function done(error = false, data) { state: 0 - connecting - 1 - receieving request status + 1 - receiving request status 2 - receiving request headers