31 Commits

Author SHA1 Message Date
Nicolas Lepage
fb62588b31 feat: adds Referer, Host, RequestURI and User-Agent on request 2025-11-21 10:16:13 +01:00
Nicolas Lepage
9ae9f3c443 feat: allows registering multiple wasm instances 2025-11-21 01:04:46 +01:00
Nicolas Lepage
9ff6ec615a docs: updates build tags and adds warning for FOSDEM video 2025-02-13 10:41:03 +01:00
Nicolas Lepage
ac1cfbfe8a chore: v2.2.1 2025-02-07 11:48:06 +01:00
Nicolas Lepage
f0a7bf8f2b docs: updates EliCDavis’ contribution 2025-02-07 11:46:33 +01:00
Nicolas Lepage
d7055fb5ea [BUGFIX] Writes default header when response close and no header already written 2025-02-07 11:45:18 +01:00
Nicolas Lepage
b9921d7d43 fix: writes default header when response closed and no header written 2025-02-07 11:42:55 +01:00
Nicolas Lepage
2f3e39bf97 chore: v2.2.0 2025-02-06 11:21:25 +01:00
Nicolas Lepage
065e91545b docs: adds FAQ mentionning websockets and tinygo 2025-02-06 11:17:08 +01:00
Nicolas Lepage
9be33ecb4d docs: updates examples doc and adds an index for examples 2025-02-06 10:15:25 +01:00
Nicolas Lepage
6ddad6298b docs: adds EliCDavis as a contributor 2025-02-06 10:08:56 +01:00
Nicolas Lepage
1f38d08145 feat: Provide ability to passthrough and hit network 2025-02-06 09:58:37 +01:00
Nicolas Lepage
5dc06a3dbb docs: updates documentation for registerWasmHTTPListener 2025-02-06 09:55:58 +01:00
Nicolas Lepage
38c1814f66 refactor: change signature for passthrough function 2025-02-06 09:55:56 +01:00
Eli Davis
268c971467 Provide ability to passthrough and hit network 2025-02-05 17:48:27 -08:00
Nicolas Lepage
57a311369e doc: credit contributors 2025-01-19 11:29:03 +01:00
Nicolas Lepage
5f2b342f3a chore: v2.1.0 2025-01-19 11:16:06 +01:00
Nicolas Lepage
8e787fbf29 Merge pull request #17 from jphastings/compile-with-tinygo
TinyGo compatible packages
2025-01-19 11:03:24 +01:00
JP Hastings-Spital
ce6765e72a feat: Demonstrates TinyGo compatibility
Adds an example that demonstrates TinyGo compatibility, as well as using a server-side HTTP handler as a fallback.
2025-01-19 11:01:15 +01:00
Nico
d12a255cff implements io.Closer for readablestream.Reader 2024-12-30 00:51:31 +01:00
JP Hastings-Spital
a16a847b26 fix: TinyGo compatible packages
The net/http/httptest package isn't implemented in tinygo — this (minor) change swaps to creating the necessary http.Request struct directly (which needs an extra step or two).

This *is not* enough to get TinyGo _working_ as a compiler for this repo, but the compilation succeeds now.
2024-12-14 10:12:55 +00:00
Nicolas Lepage
3cf36c41e2 chore: v2.0.5 2024-12-09 23:12:31 +01:00
Nicolas Lepage
30a6ef67f9 fix: avoids closing cancelled readablestream 2024-12-09 23:11:17 +01:00
Nicolas Lepage
669f82020d chore: rebuilds hello-sse example 2024-12-09 22:46:00 +01:00
Nicolas Lepage
c94dcd965d chore: v2.0.4 2024-11-27 22:33:50 +01:00
Nicolas Lepage
2d786bdb14 fix: listen for ReadableStream cancellation 2024-11-27 22:32:19 +01:00
Nicolas Lepage
c561826125 chore: v2.0.3 2024-10-17 01:12:34 +02:00
Nicolas Lepage
897626b7d1 fix: Firefox does not have request.body ReadableStream 2024-10-17 01:11:41 +02:00
Nicolas Lepage
c93d379f20 feat: improves examples 2024-10-16 14:01:11 +02:00
Nicolas Lepage
98257b470a feat: allow using cache for wasm binary 2024-10-16 14:01:11 +02:00
Nicolas Lepage
b2bd8679fd fix: error management in readablestream writer close method 2024-10-16 13:19:16 +02:00
39 changed files with 1498 additions and 156 deletions

37
.all-contributorsrc Normal file
View File

@@ -0,0 +1,37 @@
{
"projectName": "go-wasm-http-server",
"projectOwner": "nlepage",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitConvention": "gitmoji",
"contributors": [
{
"login": "jphastings",
"name": "JP Hastings-Edrei",
"avatar_url": "https://avatars.githubusercontent.com/u/42999?v=4",
"profile": "https://byjp.me/",
"contributions": [
"code",
"doc",
"example"
]
},
{
"login": "EliCDavis",
"name": "Eli Davis",
"avatar_url": "https://avatars.githubusercontent.com/u/9094977?v=4",
"profile": "https://recolude.com/",
"contributions": [
"code",
"bug"
]
}
],
"contributorsPerLine": 7,
"linkToUsage": false
}

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2021 Nicolas Lepage Copyright 2025 Nicolas Lepage
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -6,9 +6,6 @@
<a href="https://github.com/nlepage/go-wasm-http-server/blob/master/LICENSE" target="_blank"> <a href="https://github.com/nlepage/go-wasm-http-server/blob/master/LICENSE" target="_blank">
<img alt="License: Apache 2.0" src="https://img.shields.io/badge/License-Apache--2.0-yellow.svg" /> <img alt="License: Apache 2.0" src="https://img.shields.io/badge/License-Apache--2.0-yellow.svg" />
</a> </a>
<a href="https://twitter.com/njblepage" target="_blank">
<img alt="Twitter: njblepage" src="https://img.shields.io/twitter/follow/njblepage.svg?style=social" />
</a>
</p> </p>
> Embed your Go HTTP handlers in a ServiceWorker (using [WebAssembly](https://mdn.io/WebAssembly/)) and emulate an HTTP server! > Embed your Go HTTP handlers in a ServiceWorker (using [WebAssembly](https://mdn.io/WebAssembly/)) and emulate an HTTP server!
@@ -18,13 +15,18 @@
- [Hello example](https://nlepage.github.io/go-wasm-http-server/hello) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello)) - [Hello example](https://nlepage.github.io/go-wasm-http-server/hello) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello))
- [Hello example with state](https://nlepage.github.io/go-wasm-http-server/hello-state) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state)) - [Hello example with state](https://nlepage.github.io/go-wasm-http-server/hello-state) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state))
- [Hello example with state and keepalive](https://nlepage.github.io/go-wasm-http-server/hello-state-keepalive) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state-keepalive)) - [Hello example with state and keepalive](https://nlepage.github.io/go-wasm-http-server/hello-state-keepalive) ([sources](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state-keepalive))
- [Hello example with Server Sent Events](https://nlepage.github.io/go-wasm-http-server/hello-sse/) ([sources](https://nlepage.github.io/go-wasm-http-server/hello-sse/))
- [😺 Catption generator example](https://nlepage.github.io/catption/wasm) ([sources](https://github.com/nlepage/catption/tree/wasm)) - [😺 Catption generator example](https://nlepage.github.io/catption/wasm) ([sources](https://github.com/nlepage/catption/tree/wasm))
- [Random password generator web server](https://nlepage.github.io/random-password-please/) ([sources](https://github.com/nlepage/random-password-please) forked from [jbarham/random-password-please](https://github.com/jbarham/random-password-please)) - [Random password generator web server](https://nlepage.github.io/random-password-please/) ([sources](https://github.com/nlepage/random-password-please) forked from [jbarham/random-password-please](https://github.com/jbarham/random-password-please))
- [Server fallbacks, and compiling with TinyGo](https://nlepage.github.io/go-wasm-http-server/tinygo/) (runs locally; see [sources & readme](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/tinygo#readme) for how to run this example)
## How? ## How?
Talk given at the Go devroom of FOSDEM 2021 explaining how `go-wasm-http-server` works: Below is a talk given at the Go devroom of FOSDEM 2021 explaining how `go-wasm-http-server` works.
> [!WARNING]
> `go-wasm-http-server` has suffered major changes since this talk, be aware that it is not accurate anymore on several aspects.
> Please refer to the documentation below for up to date usage of `go-wasm-http-server`.
[![Deploy a Go HTTP server in your browser Youtube link](https://raw.githubusercontent.com/nlepage/go-wasm-http-talk/main/youtube.png)](https://youtu.be/O2RB_8ircdE) [![Deploy a Go HTTP server in your browser Youtube link](https://raw.githubusercontent.com/nlepage/go-wasm-http-talk/main/youtube.png)](https://youtu.be/O2RB_8ircdE)
@@ -39,6 +41,7 @@ The slides are available [here](https://nlepage.github.io/go-wasm-http-talk/).
`go-wasm-http-server` requires you to build your Go application to WebAssembly, so you need to make sure your code is compatible: `go-wasm-http-server` requires you to build your Go application to WebAssembly, so you need to make sure your code is compatible:
- no C bindings - no C bindings
- no System dependencies such as file system or network (database server for example) - no System dependencies such as file system or network (database server for example)
- For smaller WASM blobs, your code may also benefit from being compatible with, and compiled by, [TinyGo](https://tinygo.org/docs/reference/lang-support/stdlib/). See the TinyGo specific details below.
## Usage ## Usage
@@ -48,7 +51,7 @@ In your Go code, replace [`http.ListenAndServe()`](https://pkg.go.dev/net/http#L
📄 `server.go` 📄 `server.go`
```go ```go
// +build !js,!wasm //go:build !js && !wasm
package main package main
@@ -67,7 +70,7 @@ becomes:
📄 `server_js_wasm.go` 📄 `server_js_wasm.go`
```go ```go
// +build js,wasm //go:build js && wasm
package main package main
@@ -87,17 +90,37 @@ You may want to use build tags as shown above (or file name suffixes) in order t
Then build your WebAssembly binary: Then build your WebAssembly binary:
```sh ```sh
# To compile with Go
GOOS=js GOARCH=wasm go build -o server.wasm . GOOS=js GOARCH=wasm go build -o server.wasm .
# To compile with TinyGo, if your code is compatible
GOOS=js GOARCH=wasm tinygo build -o server.wasm .
``` ```
### Step 2: Create ServiceWorker file ### Step 2: Create ServiceWorker file
First, check the version of Go/TinyGo you compiled your wasm with:
```sh
$ go version
go version go1.23.4 darwin/arm64
# ^------^
$ tinygo version
tinygo version 0.35.0 darwin/arm64 (using go version go1.23.4 and LLVM version 18.1.2)
# ^----^
```
Create a ServiceWorker file with the following code: Create a ServiceWorker file with the following code:
📄 `sw.js` 📄 `sw.js`
```js ```js
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.18.4/misc/wasm/wasm_exec.js') // Note the 'go.1.23.4' below, that matches the version you just found:
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.2/sw.js') importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.4/misc/wasm/wasm_exec.js')
// If you compiled with TinyGo then, similarly, use:
importScripts('https://cdn.jsdelivr.net/gh/tinygo-org/tinygo@0.35.0/targets/wasm_exec.js')
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.2.1/sw.js')
registerWasmHTTPListener('path/to/server.wasm') registerWasmHTTPListener('path/to/server.wasm')
``` ```
@@ -163,15 +186,44 @@ URL string of the WebAssembly module, example: `"path/to/my-module.wasm"`.
An optional object containing: An optional object containing:
- `base` (`string`): Base path of the server, relative to the ServiceWorker's scope. - `base` (`string`): Base path of the server, relative to the ServiceWorker's scope.
- `cacheName` (`string`): Name of the [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to store the WebAssembly binary.
- `args` (`string[]`): Arguments for the WebAssembly module. - `args` (`string[]`): Arguments for the WebAssembly module.
- `passthrough` (`(request: Request): boolean`): Optional callback to allow passing the request through to network.
## Author ## <abbr title="Frequently Asked Questions">FAQ</abbr> ❓
👤 **Nicolas Lepage** ### Are WebSockets supported?
* Website: https://nicolas.lepage.dev/ No, WebSockets arent and wont be supported, because Service Workers cannot intercept websocket connections.
* Twitter: [@njblepage](https://twitter.com/njblepage)
* Github: [@nlepage](https://github.com/nlepage) However [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), which is an alternative to WebSockets, are supported, you can find the code for an example [here](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-sse) and the demo [here](https://nlepage.github.io/go-wasm-http-server/hello-sse/).
### Is it compatible with TinyGo?
Yes, an example and some specific information is available [here](https://github.com/nlepage/go-wasm-http-server/tree/master/docs/tinygo).
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://byjp.me/"><img src="https://avatars.githubusercontent.com/u/42999?v=4?s=100" width="100px;" alt="JP Hastings-Edrei"/><br /><sub><b>JP Hastings-Edrei</b></sub></a><br /><a href="https://github.com/nlepage/go-wasm-http-server/commits?author=jphastings" title="Code">💻</a> <a href="https://github.com/nlepage/go-wasm-http-server/commits?author=jphastings" title="Documentation">📖</a> <a href="#example-jphastings" title="Examples">💡</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://recolude.com/"><img src="https://avatars.githubusercontent.com/u/9094977?v=4?s=100" width="100px;" alt="Eli Davis"/><br /><sub><b>Eli Davis</b></sub></a><br /><a href="https://github.com/nlepage/go-wasm-http-server/commits?author=EliCDavis" title="Code">💻</a> <a href="https://github.com/nlepage/go-wasm-http-server/issues?q=author%3AEliCDavis" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
## 🤝 Contributing ## 🤝 Contributing
@@ -183,7 +235,7 @@ Give a ⭐️ if this project helped you!
## 📝 License ## 📝 License
Copyright © 2021 [Nicolas Lepage](https://github.com/nlepage).<br /> Copyright © 2025 [Nicolas Lepage](https://github.com/nlepage).<br />
This project is [Apache 2.0](https://github.com/nlepage/go-wasm-http-server/blob/master/LICENSE) licensed. This project is [Apache 2.0](https://github.com/nlepage/go-wasm-http-server/blob/master/LICENSE) licensed.
*** ***

View File

@@ -0,0 +1,27 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
)
var binaryName = ""
func main() {
http.HandleFunc("/hello", func(res http.ResponseWriter, req *http.Request) {
res.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(res).Encode(map[string]string{
"message": fmt.Sprintf("Hello from %s at path %s", binaryName, os.Getenv("WASM_HTTP_PATH")),
}); err != nil {
panic(err)
}
})
wasmhttp.Serve(nil)
select {}
}

BIN
docs/hello-multiple/api1.wasm Executable file

Binary file not shown.

BIN
docs/hello-multiple/api2.wasm Executable file

Binary file not shown.

3
docs/hello-multiple/build.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
GOOS=js GOARCH=wasm go build -o api1.wasm --ldflags="-X 'main.binaryName=api1.wasm'" .
GOOS=js GOARCH=wasm go build -o api2.wasm --ldflags="-X 'main.binaryName=api2.wasm'" .

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>go-wasm-http-server hello demo</title>
<script>
navigator.serviceWorker.register('sw.js');
async function hello() {
const api = document.querySelector('#api').value;
const res = await fetch(`${api}/hello`);
const { message } = await res.json();
alert(message);
}
</script>
</head>
<body>
<label for="api">API: </label
><select id="api">
<option value="api1/1">api1.wasm first instance</option>
<option value="api1/2">api1.wasm second instance</option>
<option value="api2/1">api2.wasm first instance</option>
<option value="api2/2">api2.wasm second instance</option>
</select>
<button onclick="hello()">Hello</button>
</body>
</html>

15
docs/hello-multiple/sw.js Normal file
View File

@@ -0,0 +1,15 @@
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.25.1/lib/wasm/wasm_exec.js');
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@master/sw.js');
addEventListener('install', (event) => {
event.waitUntil(caches.open('examples').then((cache) => cache.addAll(['api1.wasm', 'api2.wasm'])));
});
addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
registerWasmHTTPListener('api1.wasm', { base: 'api1/1/' });
registerWasmHTTPListener('api1.wasm', { base: 'api1/2/' });
registerWasmHTTPListener('api2.wasm', { base: 'api2/1/' });
registerWasmHTTPListener('api2.wasm', { base: 'api2/2/' });

Binary file not shown.

View File

@@ -1,32 +1,56 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>go-wasm-http-server hello demo</title> <title>go-wasm-http-server hello sse demo</title>
<script> <script>
navigator.serviceWorker.register('sw.js') navigator.serviceWorker.register('sw.js').then((registration) => {
.then(registration => { const serviceWorker = registration.installing ?? registration.waiting ?? registration.active;
const serviceWorker = registration.installing ?? registration.waiting ?? registration.active
if (serviceWorker.state === 'activated') { if (serviceWorker.state === 'activated') {
startEventSource() document.querySelector('#open-button').disabled = false;
} else { } else {
serviceWorker.addEventListener('statechange', e => { serviceWorker.addEventListener('statechange', (e) => {
if (e.target.state === 'activated') startEventSource() if (e.target.state === 'activated') {
}) document.querySelector('#open-button').disabled = false;
} }
}) });
}
});
function startEventSource() { window.addEventListener('DOMContentLoaded', () => {
const es = new EventSource('/api/events') let es;
document.querySelector('#open-button').addEventListener('click', () => {
if (es && es.readyState === es.OPEN) return;
es = new EventSource('api/events');
es.addEventListener('ping', (e) => { es.addEventListener('ping', (e) => {
const p = document.createElement('p') const li = document.createElement('li');
p.textContent = `ping: data=${e.data}` li.textContent = `ping: data=${e.data}`;
document.body.append(p) document.querySelector('ul').append(li);
}) });
});
document.querySelector('#close-button').addEventListener('click', () => {
if (!es) return;
es.close();
});
document.querySelector('#clear-button').addEventListener('click', () => {
document.querySelector('ul').innerHTML = '';
});
window.addEventListener('unload', () => { window.addEventListener('unload', () => {
es.close() if (!es) return;
}) es.close();
} });
});
</script> </script>
</head> </head>
<body></body> <body>
<p>
<button id="open-button" disabled>Open event source</button>
<button id="close-button">Close event source</button>
<button id="clear-button">Clear events</button>
</p>
<ul></ul>
</body>
</html> </html>

View File

@@ -1,12 +1,14 @@
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js') importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.25.1/lib/wasm/wasm_exec.js');
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.2/sw.js') importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@master/sw.js');
const wasm = 'api.wasm';
addEventListener('install', (event) => { addEventListener('install', (event) => {
event.waitUntil(skipWaiting()) event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)));
}) });
addEventListener('activate', event => { addEventListener('activate', (event) => {
event.waitUntil(clients.claim()) event.waitUntil(clients.claim());
}) });
registerWasmHTTPListener('api.wasm', { base: 'api' }) registerWasmHTTPListener(wasm, { base: 'api' });

View File

@@ -1,32 +1,32 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>go-wasm-http-server hello with state demo</title> <title>go-wasm-http-server hello with state and keepalive demo</title>
<script> <script>
navigator.serviceWorker.register('sw.js').then(registration => { navigator.serviceWorker.register('sw.js').then((registration) => {
const sw = registration.active ?? registration.installing ?? registration.waiting const sw = registration.active ?? registration.installing ?? registration.waiting;
setInterval(() => sw.postMessage(null), 15000) setInterval(() => sw.postMessage(null), 15000);
}) });
async function hello() { async function hello() {
const name = document.querySelector("#name").value const name = document.querySelector('#name').value;
const res = await fetch('api/hello', { const res = await fetch('api/hello', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ name }) body: JSON.stringify({ name }),
}) });
const { message } = await res.json() const { message } = await res.json();
alert(message) alert(message);
} }
</script> </script>
</head> </head>
<body> <body>
<label for="name">Name: </label><input id="name" value="World"> <label for="name">Name: </label><input id="name" value="World" />
<button onclick="hello()">Hello</button> <button onclick="hello()">Hello</button>
</body> </body>
</html> </html>

View File

@@ -1,14 +1,16 @@
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js') importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.25.1/lib/wasm/wasm_exec.js');
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.2/sw.js') importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@master/sw.js');
addEventListener('install', event => { const wasm = '../hello-state/api.wasm';
event.waitUntil(skipWaiting())
})
addEventListener('activate', event => { addEventListener('install', (event) => {
event.waitUntil(clients.claim()) event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)));
}) });
addEventListener('message', () => {}) addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
registerWasmHTTPListener('../hello-state/api.wasm', { base: 'api' }) addEventListener('message', () => {});
registerWasmHTTPListener(wasm, { base: 'api' });

Binary file not shown.

View File

@@ -3,27 +3,27 @@
<head> <head>
<title>go-wasm-http-server hello with state demo</title> <title>go-wasm-http-server hello with state demo</title>
<script> <script>
navigator.serviceWorker.register('sw.js') navigator.serviceWorker.register('sw.js');
async function hello() { async function hello() {
const name = document.querySelector("#name").value const name = document.querySelector('#name').value;
const res = await fetch('api/hello', { const res = await fetch('api/hello', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ name }) body: JSON.stringify({ name }),
}) });
const { message } = await res.json() const { message } = await res.json();
alert(message) alert(message);
} }
</script> </script>
</head> </head>
<body> <body>
<label for="name">Name: </label><input id="name" value="World"> <label for="name">Name: </label><input id="name" value="World" />
<button onclick="hello()">Hello</button> <button onclick="hello()">Hello</button>
</body> </body>
</html> </html>

View File

@@ -1,12 +1,14 @@
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js') importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.25.1/lib/wasm/wasm_exec.js');
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.2/sw.js') importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@master/sw.js');
const wasm = 'api.wasm';
addEventListener('install', (event) => { addEventListener('install', (event) => {
event.waitUntil(skipWaiting()) event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)));
}) });
addEventListener('activate', event => { addEventListener('activate', (event) => {
event.waitUntil(clients.claim()) event.waitUntil(clients.claim());
}) });
registerWasmHTTPListener('api.wasm', { base: 'api' }) registerWasmHTTPListener(wasm, { base: 'api' });

Binary file not shown.

View File

@@ -3,27 +3,27 @@
<head> <head>
<title>go-wasm-http-server hello demo</title> <title>go-wasm-http-server hello demo</title>
<script> <script>
navigator.serviceWorker.register('sw.js') navigator.serviceWorker.register('sw.js');
async function hello() { async function hello() {
const name = document.querySelector("#name").value const name = document.querySelector('#name').value;
const res = await fetch('api/hello', { const res = await fetch('api/hello', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ name }) body: JSON.stringify({ name }),
}) });
const { message } = await res.json() const { message } = await res.json();
alert(message) alert(message);
} }
</script> </script>
</head> </head>
<body> <body>
<label for="name">Name: </label><input id="name" value="World"> <label for="name">Name: </label><input id="name" value="World" />
<button onclick="hello()">Hello</button> <button onclick="hello()">Hello</button>
</body> </body>
</html> </html>

View File

@@ -1,12 +1,14 @@
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.1/misc/wasm/wasm_exec.js') importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.25.1/lib/wasm/wasm_exec.js');
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.0.2/sw.js') importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@master/sw.js');
const wasm = 'api.wasm';
addEventListener('install', (event) => { addEventListener('install', (event) => {
event.waitUntil(skipWaiting()) event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)));
}) });
addEventListener('activate', event => { addEventListener('activate', (event) => {
event.waitUntil(clients.claim()) event.waitUntil(clients.claim());
}) });
registerWasmHTTPListener('api.wasm', { base: 'api' }) registerWasmHTTPListener(wasm, { base: 'api' });

45
docs/index.html Normal file
View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>go-wasm-http-server examples</title>
</head>
<body>
<h1>go-wasm-http-server examples</h1>
<ul>
<li>
<a href="hello/">Hello example</a> (<a
href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello"
>Sources</a
>)
</li>
<li>
<a href="hello-multiple/">Hello example with multiple wasm instances</a> (<a
href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-multiple"
>Sources</a
>)
</li>
<li>
<a href="hello-state/">Hello example with state</a> (<a
href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state"
>Sources</a
>)
</li>
<li>
<a href="hello-state-keepalive/">Hello example with state and keepalive</a> (<a
href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-state-keepalive"
>Sources</a
>)
</li>
<li>
<a href="hello-sse/">Hello example with Server Sent Events</a> (<a
href="https://github.com/nlepage/go-wasm-http-server/tree/master/docs/hello-sse"
>Sources</a
>)
</li>
</ul>
<p>
See <a href="https://github.com/nlepage/go-wasm-http-server?tab=readme-ov-file#readme">README</a> for more
information.
</p>
</body>
</html>

42
docs/tinygo/README.md Normal file
View File

@@ -0,0 +1,42 @@
# Compiling with TinyGo
This example demonstrates that go-wasm-http-server can also be compiled with [TinyGo](https://www.tinygo.org), producing significantly smaller WASM blobs, though at the expense of [at least one known bug](https://github.com/tinygo-org/tinygo/issues/1140) and a [reduced standard library](https://tinygo.org/docs/reference/lang-support/stdlib/).
This example also demonstrates how the same code can be used for both server-side execution, and client-side execution in WASM (providing support for clients that cannot interpret WASM).
## Prerequisites
You'll need a version of [TinyGo installed](https://tinygo.org/getting-started/install/). (eg. `brew install tinygo-org/tools/tinygo`)
You'll need to make sure the first line of `sw.js` here has the same tinygo version number as your TinyGo version (this was v0.35.0 at time of writing).
## Build & run
Compile the WASM blob with TinyGo (this has been done for you for this example):
```bash
GOOS=js GOARCH=wasm tinygo build -o api.wasm .
```
Run the server (with Go, not TinyGo):
```bash
$ go run .
Server starting on http://127.0.0.1:<port>
```
## Important notes
You **must** use the TinyGo `wasm_exec.js`, specific to the version of TinyGo used to compile the WASM, in your `sw.js`. For example, if using the JSDelivr CDN:
```js
importScripts('https://cdn.jsdelivr.net/gh/tinygo-org/tinygo@0.35.0/targets/wasm_exec.js')
```
Note that the `0.35.0` within the path matches the TinyGo version used:
```sh
$ tinygo version
tinygo version 0.35.0 darwin/arm64 (using go version go1.23.4 and LLVM version 18.1.2)
# ^----^
```

BIN
docs/tinygo/api.wasm Normal file

Binary file not shown.

19
docs/tinygo/handlers.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"encoding/json"
"net/http"
"runtime"
)
func goRuntimeHandler(res http.ResponseWriter, req *http.Request) {
res.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(res).Encode(map[string]string{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"compiler": runtime.Compiler,
"version": runtime.Version(),
}); err != nil {
panic(err)
}
}

44
docs/tinygo/index.html Normal file
View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>go-wasm-http-server tinygo demo</title>
<script>
const sw = navigator.serviceWorker
function attachServiceWorker() {
sw.register('sw.js')
.then(() => {
document.getElementById('wasm-status').innerText = "⚡️ Loaded - Will execute WASM locally"
})
.catch((err) => {
document.getElementById('wasm-status').innerText = "🛑 Error loading service worker — Check console"
console.error(err)
})
}
async function makeQuery() {
const res = await fetch('api/tiny', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
document.getElementById('output').innerText = await res.text()
}
</script>
</head>
<body>
<p>This example demonstrates that go-wasm-http-server can be compiled with <a href="https://www.tinygo.org">TinyGo</em>, producing significantly smaller WASM blobs, at the expense of <a href="https://github.com/tinygo-org/tinygo/issues/1140">at least one known bug</a>, and a <a href="https://tinygo.org/docs/reference/lang-support/stdlib/">reduced standard library</a>.</p>
<dl><dt>WASM HTTP Service Worker:</dt><dd id="wasm-status">☁️ Not loaded — will call server</dd></dl>
<ol>
<li><button onclick="makeQuery()">Call API</button></li>
<li><button onclick="attachServiceWorker()">Attach the service worker</button></li>
<li><span>Call the API again (Step 1)</span></li>
</ol>
<h3>Response:</h3>
<pre id="output"></pre>
</body>
</html>

34
docs/tinygo/server.go Normal file
View File

@@ -0,0 +1,34 @@
//go:build !wasm
// +build !wasm
package main
import (
"embed"
"fmt"
"log"
"net"
"net/http"
)
//go:embed *.html *.js *.wasm
var thisDir embed.FS
func main() {
// Serve all files in this directory statically
http.Handle("/", http.FileServer(http.FS(thisDir)))
// Note that this needs to be mounted at /api/tiny, rather than just /tiny (like in wasm.go)
// because the service worker mounts the WASM server at /api (at the end of sw.js)
http.HandleFunc("/api/tiny", goRuntimeHandler)
// Pick any available port. Note that ServiceWorkers _require_ localhost for non-SSL serving (so other LAN/WAN IPs will prevent the service worker from loading)
listener, err := net.Listen("tcp", ":0")
if err != nil {
log.Fatalf("Unable to claim a port to start server on: %v", err)
}
// Share the port being used & start
fmt.Printf("Server starting on http://127.0.0.1:%d\n", listener.Addr().(*net.TCPAddr).Port)
panic(http.Serve(listener, nil))
}

14
docs/tinygo/sw.js Normal file
View File

@@ -0,0 +1,14 @@
importScripts('https://cdn.jsdelivr.net/gh/tinygo-org/tinygo@0.35.0/targets/wasm_exec.js')
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.2.1/sw.js')
const wasm = 'api.wasm'
addEventListener('install', (event) => {
event.waitUntil(caches.open('examples').then((cache) => cache.add(wasm)))
})
addEventListener('activate', (event) => {
event.waitUntil(clients.claim())
})
registerWasmHTTPListener(wasm, { base: 'api' })

18
docs/tinygo/wasm.go Normal file
View File

@@ -0,0 +1,18 @@
//go:build wasm
// +build wasm
package main
import (
"net/http"
wasmhttp "github.com/nlepage/go-wasm-http-server/v2"
)
func main() {
http.HandleFunc("/tiny", goRuntimeHandler)
wasmhttp.Serve(nil)
select {}
}

6
go.mod
View File

@@ -1,9 +1,9 @@
module github.com/nlepage/go-wasm-http-server/v2 module github.com/nlepage/go-wasm-http-server/v2
go 1.18 go 1.22
require ( require (
github.com/hack-pad/safejs v0.1.1 github.com/hack-pad/safejs v0.1.1
github.com/nlepage/go-js-promise v1.0.0 github.com/nlepage/go-js-promise v1.1.0
github.com/tmaxmax/go-sse v0.8.0 github.com/tmaxmax/go-sse v0.11.0
) )

8
go.sum
View File

@@ -1,6 +1,6 @@
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/nlepage/go-js-promise v1.0.0 h1:K7OmJ3+0BgWJ2LfXchg2sI6RDr7AW/KWR8182epFwGQ= github.com/nlepage/go-js-promise v1.1.0 h1:BfvywsIMo4cpNOKyoReBWkxEW8f9HMwXqGc45wEKPRs=
github.com/nlepage/go-js-promise v1.0.0/go.mod h1:bdOP0wObXu34euibyK39K1hoBCtlgTKXGc56AGflaRo= github.com/nlepage/go-js-promise v1.1.0/go.mod h1:bdOP0wObXu34euibyK39K1hoBCtlgTKXGc56AGflaRo=
github.com/tmaxmax/go-sse v0.8.0 h1:pPpTgyyi1r7vG2o6icebnpGEh3ebcnBXqDWkb7aTofs= github.com/tmaxmax/go-sse v0.11.0 h1:nogmJM6rJUoOLoAwEKeQe5XlVpt9l7N82SS1jI7lWFg=
github.com/tmaxmax/go-sse v0.8.0/go.mod h1:HLoxqxdH+7oSUItjtnpxjzJedfr/+Rrm/dNWBcTxJFM= github.com/tmaxmax/go-sse v0.11.0/go.mod h1:u/2kZQR1tyngo1lKaNCj1mJmhXGZWS1Zs5yiSOD+Eg8=

View File

@@ -14,7 +14,7 @@ type Reader struct {
off int off int
} }
var _ io.Reader = (*Reader)(nil) var _ io.ReadCloser = (*Reader)(nil)
func NewReader(r safejs.Value) *Reader { func NewReader(r safejs.Value) *Reader {
return &Reader{ return &Reader{
@@ -83,3 +83,14 @@ func (r *Reader) Read(p []byte) (int, error) {
return n, nil return n, nil
} }
func (r *Reader) Close() error {
p, err := r.value.Call("cancel")
if err != nil {
return err
}
_, err = promise.Await(safejs.Unsafe(p))
return err
}

View File

@@ -12,11 +12,14 @@ type Writer struct {
Value safejs.Value Value safejs.Value
controller safejs.Value controller safejs.Value
ctx context.Context ctx context.Context
cancelled bool
} }
var _ io.WriteCloser = (*Writer)(nil) var _ io.WriteCloser = (*Writer)(nil)
func NewWriter() (*Writer, error) { func NewWriter() (*Writer, error) {
var rs *Writer
var start safejs.Func var start safejs.Func
var controller safejs.Value var controller safejs.Value
@@ -34,6 +37,7 @@ func NewWriter() (*Writer, error) {
cancel, err = safejs.FuncOf(func(_ safejs.Value, _ []safejs.Value) any { cancel, err = safejs.FuncOf(func(_ safejs.Value, _ []safejs.Value) any {
defer cancel.Release() defer cancel.Release()
rs.cancelled = true
cancelCtx() cancelCtx()
return nil return nil
}) })
@@ -54,14 +58,20 @@ func NewWriter() (*Writer, error) {
return nil, err return nil, err
} }
return &Writer{ rs = &Writer{
Value: value, Value: value,
controller: controller, controller: controller,
ctx: ctx, ctx: ctx,
}, nil }
return rs, nil
} }
func (rs *Writer) Write(b []byte) (int, error) { func (rs *Writer) Write(b []byte) (int, error) {
if rs.cancelled {
return 0, nil
}
chunk, err := jstype.Uint8Array.New(len(b)) // FIXME reuse same Uint8Array? chunk, err := jstype.Uint8Array.New(len(b)) // FIXME reuse same Uint8Array?
if err != nil { if err != nil {
return 0, err return 0, err
@@ -78,8 +88,12 @@ func (rs *Writer) Write(b []byte) (int, error) {
} }
func (rs *Writer) Close() error { func (rs *Writer) Close() error {
rs.controller.Call("close") if rs.cancelled {
return nil return nil
}
_, err := rs.controller.Call("close")
return err
} }
func (rs *Writer) Context() context.Context { func (rs *Writer) Context() context.Context {

787
package-lock.json generated Normal file
View File

@@ -0,0 +1,787 @@
{
"name": "go-wasm-http-server",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "go-wasm-http-server",
"version": "0.1.0",
"license": "Apache-2.0",
"devDependencies": {
"all-contributors-cli": "^6.26.1"
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/all-contributors-cli": {
"version": "6.26.1",
"resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz",
"integrity": "sha512-Ymgo3FJACRBEd1eE653FD1J/+uD0kqpUNYfr9zNC1Qby0LgbhDBzB3EF6uvkAbYpycStkk41J+0oo37Lc02yEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.7.6",
"async": "^3.1.0",
"chalk": "^4.0.0",
"didyoumean": "^1.2.1",
"inquirer": "^7.3.3",
"json-fixer": "^1.6.8",
"lodash": "^4.11.2",
"node-fetch": "^2.6.0",
"pify": "^5.0.0",
"yargs": "^15.0.1"
},
"bin": {
"all-contributors": "dist/cli.js"
},
"engines": {
"node": ">=4"
},
"optionalDependencies": {
"prettier": "^2"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.21.3"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"dev": true,
"license": "MIT"
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true,
"license": "MIT"
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"restore-cursor": "^3.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cli-width": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 10"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
"tmp": "^0.0.33"
},
"engines": {
"node": ">=4"
}
},
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^1.0.5"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inquirer": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
"integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-escapes": "^4.2.1",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-width": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.19",
"mute-stream": "0.0.8",
"run-async": "^2.4.0",
"rxjs": "^6.6.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/json-fixer": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/json-fixer/-/json-fixer-1.6.15.tgz",
"integrity": "sha512-TuDuZ5KrgyjoCIppdPXBMqiGfota55+odM+j2cQ5rt/XKyKmqGB3Whz1F8SN8+60yYGy/Nu5lbRZ+rx8kBIvBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.9",
"chalk": "^4.1.2",
"pegjs": "^0.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true,
"license": "ISC"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pegjs": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz",
"integrity": "sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow==",
"dev": true,
"license": "MIT",
"bin": {
"pegjs": "bin/pegjs"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/pify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"license": "MIT",
"optional": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true,
"license": "ISC"
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
"integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC"
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true,
"license": "ISC"
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"dev": true,
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true,
"license": "MIT"
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"dev": true,
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
}
}
}

View File

@@ -5,5 +5,8 @@
"main": "index.js", "main": "index.js",
"repository": "https://github.com/nlepage/go-wasm-http-server", "repository": "https://github.com/nlepage/go-wasm-http-server",
"author": "Nicolas Lepage <19571875+nlepage@users.noreply.github.com>", "author": "Nicolas Lepage <19571875+nlepage@users.noreply.github.com>",
"license": "Apache-2.0" "license": "Apache-2.0",
"devDependencies": {
"all-contributors-cli": "^6.26.1"
}
} }

View File

@@ -3,13 +3,18 @@ package wasmhttp
import ( import (
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/url"
"syscall/js" "syscall/js"
promise "github.com/nlepage/go-js-promise"
"github.com/nlepage/go-wasm-http-server/v2/internal/readablestream" "github.com/nlepage/go-wasm-http-server/v2/internal/readablestream"
"github.com/nlepage/go-wasm-http-server/v2/internal/safejs" "github.com/nlepage/go-wasm-http-server/v2/internal/safejs"
) )
var (
navigator = safejs.Safe(js.Global().Get("navigator"))
)
// Request builds and returns the equivalent http.Request // Request builds and returns the equivalent http.Request
func Request(uvalue js.Value) (*http.Request, error) { func Request(uvalue js.Value) (*http.Request, error) {
value := safejs.Safe(uvalue) value := safejs.Safe(uvalue)
@@ -19,7 +24,11 @@ func Request(uvalue js.Value) (*http.Request, error) {
return nil, err return nil, err
} }
url, err := value.GetString("url") rawURL, err := value.GetString("url")
if err != nil {
return nil, err
}
u, err := url.Parse(rawURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -29,8 +38,27 @@ func Request(uvalue js.Value) (*http.Request, error) {
return nil, err return nil, err
} }
var bodyReader io.Reader var bodyReader io.ReadCloser
if !body.IsUndefined() && !body.IsNull() {
if !body.IsNull() {
// WORKAROUND: Firefox does not have request.body ReadableStream
if body.IsUndefined() {
blobp, err := value.Call("blob")
if err != nil {
return nil, err
}
blob, err := promise.Await(safejs.Unsafe(blobp))
if err != nil {
return nil, err
}
body, err = safejs.Safe(blob).Call("stream")
if err != nil {
return nil, err
}
}
r, err := body.Call("getReader") r, err := body.Call("getReader")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -39,11 +67,23 @@ func Request(uvalue js.Value) (*http.Request, error) {
bodyReader = readablestream.NewReader(r) bodyReader = readablestream.NewReader(r)
} }
req := httptest.NewRequest( req := &http.Request{
method, Method: method,
url, URL: u,
bodyReader, Host: u.Host,
) Body: bodyReader,
Header: make(http.Header),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
RequestURI: rawURL,
}
referer, err := value.GetString("referrer")
if err != nil {
return nil, err
}
req.Header.Set("Referer", referer)
headers, err := value.Get("headers") headers, err := value.Get("headers")
if err != nil { if err != nil {
@@ -87,5 +127,13 @@ func Request(uvalue js.Value) (*http.Request, error) {
req.Header.Set(key, value) req.Header.Set(key, value)
} }
if req.UserAgent() == "" {
userAgent, err := navigator.GetString("userAgent")
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
}
return req, nil return req, nil
} }

View File

@@ -123,6 +123,9 @@ func (r *response) Flush() {
// Close implements [io.Closer] // Close implements [io.Closer]
func (r *response) Close() error { func (r *response) Close() error {
if !r.wroteHeader {
r.WriteHeader(200)
}
if err := r.body.Flush(); err != nil { if err := r.body.Flush(); err != nil {
return err return err
} }
@@ -136,6 +139,7 @@ func (r *response) Context() context.Context {
func (r *response) WriteError(str string) { func (r *response) WriteError(str string) {
slog.Error(str) slog.Error(str)
if !r.wroteHeader { if !r.wroteHeader {
r.Header().Set("Content-Type", "text/plain")
r.WriteHeader(500) r.WriteHeader(500)
_, _ = r.WriteString(str) _, _ = r.WriteString(str)
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"os"
"strings" "strings"
"syscall/js" "syscall/js"
@@ -21,10 +22,8 @@ func Serve(handler http.Handler) (func(), error) {
h = http.DefaultServeMux h = http.DefaultServeMux
} }
prefix, err := wasmhttp.GetString("path") path := os.Getenv("WASM_HTTP_PATH")
if err != nil { prefix := path
return nil, err
}
for strings.HasSuffix(prefix, "/") { for strings.HasSuffix(prefix, "/") {
prefix = strings.TrimSuffix(prefix, "/") prefix = strings.TrimSuffix(prefix, "/")
@@ -78,7 +77,7 @@ func Serve(handler http.Handler) (func(), error) {
return nil, err return nil, err
} }
if _, err = wasmhttp.Call("setHandler", handlerValue); err != nil { if _, err = wasmhttp.Call("setHandler", path, handlerValue); err != nil {
return nil, err return nil, err
} }

110
sw.js
View File

@@ -1,34 +1,94 @@
function registerWasmHTTPListener(wasm, { base, args = [] } = {}) { class WasmHTTP {
let path = new URL(registration.scope).pathname #listeners = new Map();
if (base && base !== '') path = `${trimEnd(path, '/')}/${trimStart(base, '/')}`
const handlerPromise = new Promise(setHandler => { setHandler(path, handler) {
self.wasmhttp = { const listener = this.#listeners.get(path);
path, if (!listener) {
setHandler, throw new Error(`no listener for path "${path}"`);
} }
})
const go = new Go() listener.setHandler(handler);
go.argv = [wasm, ...args] }
WebAssembly.instantiateStreaming(fetch(wasm), go.importObject).then(({ instance }) => go.run(instance))
addEventListener('fetch', e => { handle(e) {
const { pathname } = new URL(e.request.url) const { pathname } = new URL(e.request.url);
if (!pathname.startsWith(path)) return
e.respondWith(handlerPromise.then(handler => handler(e.request))) for (const [path, listener] of this.#listeners) {
}) if (pathname.startsWith(path)) {
listener.handle(e);
return;
}
}
}
addListener({ wasm, base = '', cacheName, passthrough, args = [], env = {} }) {
const path = new URL(trimStart(base, '/'), registration.scope).pathname;
if (this.#listeners.has(path)) {
throw new Error(`a listener is already registered for path "${path}"`);
}
this.#listeners.set(path, new WasmHTTPListener({ wasm, path, cacheName, passthrough, args, env }));
}
}
class WasmHTTPListener {
#handlerPromise;
#resolveHandler;
#passthrough;
constructor({ wasm, path, cacheName, passthrough, args, env }) {
this.#handlerPromise = new Promise((resolve) => {
this.#resolveHandler = resolve;
});
this.#passthrough = passthrough;
this.#run({ wasm, path, cacheName, passthrough, args, env });
}
async #run({ wasm, path, cacheName, args, env }) {
try {
const go = new Go();
go.argv = [wasm, ...args];
go.env = { ...env, WASM_HTTP_PATH: path };
const cache = cacheName ? await caches.open(cacheName) : caches;
const source = (await cache.match(wasm)) ?? (await fetch(wasm));
const { instance } = await WebAssembly.instantiateStreaming(source, go.importObject);
await go.run(instance);
} catch (err) {
console.error(`error while running ${wasm} for path "${path}"`, err);
}
}
setHandler(handler) {
this.#resolveHandler(handler);
}
handle(e) {
if (this.#passthrough?.(e.request)) return;
// FIXME return 500 if run has thrown
e.respondWith(this.#handlerPromise.then((handler) => handler(e.request)));
}
}
self.wasmhttp = new WasmHTTP();
addEventListener('fetch', (e) => {
self.wasmhttp.handle(e);
});
function registerWasmHTTPListener(wasm, { base, cacheName, passthrough, args, env } = {}) {
self.wasmhttp.addListener({ wasm, base, cacheName, passthrough, args, env });
} }
function trimStart(s, c) { function trimStart(s, c) {
let r = s let r = s;
while (r.startsWith(c)) r = r.slice(c.length) while (r.startsWith(c)) r = r.slice(c.length);
return r return r;
}
function trimEnd(s, c) {
let r = s
while (r.endsWith(c)) r = r.slice(0, -c.length)
return r
} }