@statewalker/webrun-http-browser 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,9 @@ After that the same code can be deployed in Deno / Deno Deploy / Cloudflare / No
6
6
 
7
7
  ## Demo
8
8
 
9
- * [Demo 1](./demo/demo-1.html) - a dynamic web site with a server-side API, a HTML page and a CSS file
10
9
  * [https://observablehq.com/@kotelnikov/webrun-http-service](https://observablehq.com/@kotelnikov/webrun-http-service) - an Observable page demonstrating how it works. You can play with the code here.
10
+ * [Demo 1](./demo/demo-1.html) - a dynamic web site with a server-side API, a HTML page and a CSS file
11
+ * [Demo 2](./demo/demo-2.html) - a virtual file server exposing your local disk content
11
12
 
12
13
  ## Features
13
14
 
@@ -0,0 +1,199 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ </head>
9
+
10
+ <body>
11
+ <h1>Dynamic Site</h1>
12
+ <p>This demo shows how to launch a dynamic site containing the following parts:</p>
13
+ <ul>
14
+ <li><code>/index.html</code> - the main page referencing a CSS and a java script files. Note that the javascript is
15
+ imported using the standard ESM <code>import</code> notation</li>
16
+ <li><code>/index.css</code> - styles for the HTML page.</li>
17
+ <li><code>/index.js</code> - the main application code. This script listens the input box and sent its content to
18
+ the server-side API.</li>
19
+ <li><code>/api/:name</code> - dynamic server-side endpoint running responding to GET HTTP requests from the client.
20
+ This endpoint returns JSON object with a greeting message using the transmitted name with the current time.
21
+ The name of the user is loaded from the URL.
22
+
23
+ <p>Example of the server response:</p>
24
+ <pre>
25
+ {
26
+ "name" : "John",
27
+ "time" : "11:02:59",
28
+ "milliseconds" : "785"
29
+ }
30
+ </pre>
31
+ </li>
32
+ </ul>
33
+ <div id="view"></div>
34
+
35
+ <script type="module">
36
+ import * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
37
+ import { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
38
+ import mime from "https://esm.sh/mime-types";
39
+
40
+ Promise.resolve().then(main).catch(console.error);
41
+ async function main() {
42
+ // Load a Hono request router:
43
+ const router = getRouter();
44
+
45
+ // Create an HTTP Server with the specified name.
46
+ // It uses the given Hono router to dispatch requests
47
+ // between endpoints.
48
+ const serviceBaseUrl = await startHttpService({
49
+ serviceKey: "MY_SITE",
50
+ router
51
+ })
52
+ const siteUrl = `${serviceBaseUrl}/`;
53
+
54
+ // Now we can visualize the site in an iframe iframe:
55
+ const container = document.querySelector("#view");
56
+ const iframe = document.createElement("iframe");
57
+ iframe.width = "100%";
58
+ iframe.height = "500px";
59
+ iframe.src = siteUrl;
60
+ container.appendChild(iframe);
61
+
62
+ // We also append a reference to this site.
63
+ // It allows to open the site in a separate window:
64
+ const a = document.createElement("a");
65
+ a.target = "_blank";
66
+ a.href = siteUrl;
67
+ a.innerText = `Open "~MY_SITE/index.html" in a new window.`;
68
+ container.insertBefore(a, iframe);
69
+ }
70
+
71
+ async function startHttpService({
72
+ serviceKey,
73
+ router
74
+ }) {
75
+ // Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
76
+ const connection = await WebRunHttp.newRemoteRelayChannel();
77
+
78
+ // Step 2: Define an HTTP handler
79
+ const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
80
+ const app = new Hono({
81
+ getPath: (req) => req.url.substring(serviceBaseUrl.length)
82
+ });
83
+ // Add the router
84
+ app.route("/", router);
85
+ const httpHandler = (request) => app.fetch(request);
86
+
87
+ // Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
88
+ const close = await WebRunHttp.initHttpService(httpHandler, {
89
+ key: serviceKey, // Service prefix
90
+ port: connection.port
91
+ });
92
+
93
+ return serviceBaseUrl;
94
+ }
95
+
96
+ function getRouter() {
97
+ const router = new Hono();
98
+
99
+ // Server-side REST API handler:
100
+ router.get("/api/:name", (c) => {
101
+ const { name } = c.req.param()
102
+ const date = new Date();
103
+ const response = {
104
+ message: `Hello ${name}!`,
105
+ time: date.toLocaleTimeString(),
106
+ milliseconds: date.getMilliseconds()
107
+ }
108
+ return c.json(response);
109
+ });
110
+
111
+ // Static files: an object with path names and the corresponding content
112
+ const files = getStaticFiles();
113
+ // Register these files with a basic dispatcher
114
+ // returning the file content to the caller:
115
+ router.get("/*", newStaticFilesDispatcher(files))
116
+
117
+ return router;
118
+ }
119
+
120
+ function newStaticFilesDispatcher(files) {
121
+ return function showStaticFiles(context) {
122
+ let { url, path } = context.req;
123
+ path = path.replace(/#.*$/, "");
124
+ if (path.match(/\/$/)) {
125
+ path = path + "index.html";
126
+ }
127
+ const result = files[path];
128
+ if (!result) {
129
+ return context.notFound();
130
+ }
131
+ const mimeType = mime.lookup(path) || "application/octet-stream";
132
+ context.header("Content-Type", mimeType);
133
+ context.status(201);
134
+ if (typeof result === "object") {
135
+ return context.json(result);
136
+ } else {
137
+ return context.body(result);
138
+ }
139
+ };
140
+ }
141
+
142
+ function getStaticFiles() {
143
+ return {
144
+ "/index.css": `
145
+ html, body {
146
+ padding: 0;
147
+ margin: 0;
148
+ }
149
+ h1 {
150
+ color : navy;
151
+ }
152
+ `,
153
+
154
+
155
+ "/index.js": `
156
+ document.addEventListener("DOMContentLoaded", () => {
157
+ const input = document.querySelector("#name");
158
+ input.addEventListener("input", (ev) => {
159
+ ev.preventDefault();
160
+ ev.stopPropagation();
161
+ updateName();
162
+ })
163
+ updateName();
164
+ async function updateName() {
165
+ const name = input.value;
166
+ const res = await fetch("./api/" + encodeURIComponent(name));
167
+ const { message, time, milliseconds } = await res.json();
168
+ const view = document.querySelector("#messages");
169
+ view.innerHTML = "<div>" + message + "</div><div>Current time is " + time + " (and " + milliseconds + " milliseconds).</div>";
170
+ }
171
+ })
172
+ `,
173
+ "/index.html": `<!DOCTYPE HTML>
174
+ <html>
175
+ <head>
176
+ <meta charset="utf-8" />
177
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
178
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
179
+ <link rel="stylesheet" href="./index.css" />
180
+ </head>
181
+ <body>
182
+ <h1>In-Browser Dynamic Site</h1>
183
+ <div>
184
+ Please enter your name:
185
+ <input type="text" id="name" value="John">
186
+ </div>
187
+ <p>Message from the server:</p>
188
+ <div id="messages">
189
+ </div>
190
+ <script type="module" src="./index.js"><\/script>
191
+ </body>
192
+ </html>
193
+ `
194
+ }
195
+ }
196
+ </script>
197
+ </body>
198
+
199
+ </html>