@statewalker/webrun-http-browser 0.3.2 → 0.3.4
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/LICENSE +1 -1
- package/README.md +321 -50
- package/demo/demo-1.html +109 -194
- package/demo/demo-2.html +119 -580
- package/dist/core/data-calls.d.ts +5 -0
- package/dist/core/data-calls.d.ts.map +1 -0
- package/dist/core/data-channels.d.ts +17 -0
- package/dist/core/data-channels.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/message-target.d.ts +16 -0
- package/dist/core/message-target.d.ts.map +1 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/http/http-send-recieve.d.ts +23 -0
- package/dist/http/http-send-recieve.d.ts.map +1 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2541 -807
- package/dist/relay/index-sw.d.ts +7 -0
- package/dist/relay/index-sw.d.ts.map +1 -0
- package/dist/relay/index.d.ts +69 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/split-service-url.d.ts +13 -0
- package/dist/relay/split-service-url.d.ts.map +1 -0
- package/dist/relay-sw.d.ts +2 -0
- package/dist/relay-sw.d.ts.map +1 -0
- package/dist/relay-sw.js +878 -0
- package/dist/sw/http-sw-dispatcher.d.ts +28 -0
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
- package/dist/sw/index.d.ts +3 -0
- package/dist/sw/index.d.ts.map +1 -0
- package/dist/sw/sw-dispatcher.d.ts +74 -0
- package/dist/sw/sw-dispatcher.d.ts.map +1 -0
- package/dist/sw-worker.d.ts +2 -0
- package/dist/sw-worker.d.ts.map +1 -0
- package/dist/sw-worker.js +813 -0
- package/dist/sw.d.ts +2 -0
- package/dist/sw.d.ts.map +1 -0
- package/dist/sw.js +1076 -0
- package/package.json +50 -37
- package/public/index.html +61 -97
- package/public/index.js +7 -4
- package/public/sw-worker.js +4 -0
- package/public-relay/heartbeat.js +4 -3
- package/public-relay/relay-sw.js +4 -6
- package/public-relay/relay.html +16 -11
- package/src/core/data-calls.ts +46 -0
- package/src/core/data-channels.ts +224 -0
- package/src/core/index.ts +9 -0
- package/src/core/message-target.ts +18 -0
- package/src/core/registry.ts +54 -0
- package/src/http/http-send-recieve.ts +81 -0
- package/src/http/index.ts +7 -0
- package/src/index.ts +3 -0
- package/src/relay/index-sw.ts +145 -0
- package/src/relay/index.ts +261 -0
- package/src/relay/split-service-url.ts +30 -0
- package/src/relay-sw.ts +7 -0
- package/src/sw/http-sw-dispatcher.ts +122 -0
- package/src/sw/index.ts +2 -0
- package/src/sw/sw-dispatcher.ts +307 -0
- package/src/sw-worker.ts +7 -0
- package/src/sw.ts +1 -0
- package/CHANGELOG.md +0 -1
- package/demo/demo-3.html +0 -167
- package/dist/index-sw-umd.js +0 -798
- package/dist/index-sw-umd.min.js +0 -2
- package/dist/index-sw.js +0 -791
- package/dist/index-sw.min.js +0 -2
- package/dist/index-umd.js +0 -853
- package/dist/index-umd.min.js +0 -2
- package/dist/index.min.js +0 -2
- package/index.js +0 -1
- package/src/core/data-calls.js +0 -36
- package/src/core/data-channels.js +0 -192
- package/src/core/data-send-recieve.js +0 -28
- package/src/core/errors.js +0 -13
- package/src/core/index.js +0 -4
- package/src/http/HttpError.js +0 -59
- package/src/http/http-send-recieve.js +0 -40
- package/src/http/http-stubs.js +0 -135
- package/src/http/index.js +0 -3
- package/src/http/readable-streams.js +0 -33
- package/src/index.js +0 -7
- package/src/relay/index-sw.js +0 -216
- package/src/relay/index.js +0 -263
- package/src/relay/splitServiceUrl.js +0 -24
- package/src/sw/http-sw-dispatcher.js +0 -128
- package/src/sw/sw-dispatcher.js +0 -264
package/demo/demo-2.html
CHANGED
|
@@ -1,598 +1,137 @@
|
|
|
1
|
-
<!DOCTYPE
|
|
2
|
-
<html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>webrun-http-browser — local file server demo (relay)</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 60rem; }
|
|
9
|
+
.panel { display: flex; gap: 1rem; min-height: 22rem; }
|
|
10
|
+
.files { width: 16rem; border: 1px solid #e5e5e5; padding: 0.5rem; overflow: auto; }
|
|
11
|
+
.preview { flex: 1; border: 1px solid #e5e5e5; }
|
|
12
|
+
iframe { width: 100%; height: 100%; border: none; }
|
|
13
|
+
.tree-node { padding-left: 1rem; }
|
|
14
|
+
.entry { display: flex; gap: 0.25rem; cursor: pointer; font-family: monospace; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<h1>Local disk → in-browser file server</h1>
|
|
19
|
+
<p>
|
|
20
|
+
Click <strong>Open folder</strong> and pick a directory. A relay
|
|
21
|
+
ServiceWorker will serve the folder's contents from an in-browser URL
|
|
22
|
+
rooted at <code>~FS/</code>.
|
|
23
|
+
</p>
|
|
24
|
+
<button type="button" id="open">Open folder</button>
|
|
25
|
+
<button type="button" id="reload" style="display: none;">Reload tree</button>
|
|
26
|
+
<div class="panel">
|
|
27
|
+
<div class="files" id="files"></div>
|
|
28
|
+
<div class="preview">
|
|
29
|
+
<iframe title="File preview" id="preview" name="preview" src=""></iframe>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<script type="module">
|
|
34
|
+
import * as WebRunHttp from "../dist/index.js";
|
|
35
|
+
|
|
36
|
+
const openBtn = document.querySelector("#open");
|
|
37
|
+
const reloadBtn = document.querySelector("#reload");
|
|
38
|
+
const filesView = document.querySelector("#files");
|
|
3
39
|
|
|
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
|
-
<div id="view"></div>
|
|
12
|
-
|
|
13
|
-
<script type="module">
|
|
14
|
-
import * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
|
|
15
|
-
import { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
|
|
16
|
-
import mime from "https://esm.sh/mime-types";
|
|
17
|
-
import {
|
|
18
|
-
BrowserFilesApi,
|
|
19
|
-
FilesApi,
|
|
20
|
-
isHandlerAccessible,
|
|
21
|
-
MemFilesApi,
|
|
22
|
-
verifyPermission,
|
|
23
|
-
} from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
|
|
24
|
-
|
|
25
|
-
Promise.resolve().then(main).catch(console.error);
|
|
26
|
-
async function main() {
|
|
27
|
-
// Load a Hono request router:
|
|
28
|
-
const router = getRouter();
|
|
29
|
-
|
|
30
|
-
// Create an HTTP Server with the specified name.
|
|
31
|
-
// It uses the given Hono router to dispatch requests
|
|
32
|
-
// between endpoints.
|
|
33
|
-
const serviceBaseUrl = await startHttpService({
|
|
34
|
-
serviceKey: "MY_STATIC_SITE_LAUNCHER",
|
|
35
|
-
router
|
|
36
|
-
})
|
|
37
|
-
const siteUrl = `${serviceBaseUrl}/fs/index.html`;
|
|
38
|
-
|
|
39
|
-
// Now we can visualize the site in an iframe iframe:
|
|
40
|
-
const container = document.querySelector("#view");
|
|
41
|
-
|
|
42
|
-
// We also append a reference to this site.
|
|
43
|
-
// It allows to open the site in a separate window:
|
|
44
|
-
const a = document.createElement("a");
|
|
45
|
-
a.target = "_blank";
|
|
46
|
-
a.href = siteUrl;
|
|
47
|
-
a.innerText = siteUrl;
|
|
48
|
-
container.appendChild(a);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function startHttpService({
|
|
52
|
-
serviceKey,
|
|
53
|
-
router
|
|
54
|
-
}) {
|
|
55
|
-
// Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
|
|
56
|
-
const connection = await WebRunHttp.newRemoteRelayChannel();
|
|
57
|
-
|
|
58
|
-
// Step 2: Define an HTTP handler
|
|
59
|
-
const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
|
|
60
|
-
const app = new Hono({
|
|
61
|
-
getPath: (req) => req.url.substring(serviceBaseUrl.length)
|
|
62
|
-
});
|
|
63
|
-
// Add the router
|
|
64
|
-
app.route("/", router);
|
|
65
|
-
const httpHandler = (request) => app.fetch(request);
|
|
66
|
-
|
|
67
|
-
// Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
|
|
68
|
-
const close = await WebRunHttp.initHttpService(httpHandler, {
|
|
69
|
-
key: serviceKey, // Service prefix
|
|
70
|
-
port: connection.port
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return serviceBaseUrl;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getRouter() {
|
|
77
|
-
const router = new Hono();
|
|
78
|
-
const files = [...document.querySelectorAll("[data-file]")].reduce((index, elm) => {
|
|
79
|
-
const fileName = elm.getAttribute("data-file");
|
|
80
|
-
const value = elm.innerText.replaceAll("<\\/script>", "</" + "script>");
|
|
81
|
-
index[fileName] = value;
|
|
82
|
-
return index;
|
|
83
|
-
}, {})
|
|
84
|
-
router.get("/*", newStaticFilesDispatcher(files))
|
|
85
|
-
return router;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function newStaticFilesDispatcher(files) {
|
|
89
|
-
return function showStaticFiles(context) {
|
|
90
|
-
let { url, path } = context.req;
|
|
91
|
-
path = path.replace(/#.*$/, "");
|
|
92
|
-
if (path.match(/\/$/)) {
|
|
93
|
-
path = path + "index.html";
|
|
94
|
-
}
|
|
95
|
-
const result = files[path];
|
|
96
|
-
if (!result) {
|
|
97
|
-
return context.notFound();
|
|
98
|
-
}
|
|
99
|
-
const mimeType = mime.lookup(path) || "application/octet-stream";
|
|
100
|
-
context.header("Content-Type", mimeType);
|
|
101
|
-
context.status(201);
|
|
102
|
-
if (typeof result === "object") {
|
|
103
|
-
return context.json(result);
|
|
104
|
-
} else {
|
|
105
|
-
return context.body(result);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<script type="template/file" data-file="/fs/files-tree.css">
|
|
112
|
-
.tree {
|
|
113
|
-
--tree-line-width: 1px;
|
|
114
|
-
--tree-line-color: silver;
|
|
115
|
-
--tree-connector-radius: 0.1em;
|
|
116
|
-
--tree-connector-length: 0.5em;
|
|
117
|
-
--tree-connector-height: 0.6em;
|
|
118
|
-
--tree-content-padding: 0.5em;
|
|
119
|
-
position: relative;
|
|
120
|
-
}
|
|
121
|
-
.tree .tree-node {
|
|
122
|
-
position: relative;
|
|
123
|
-
padding-inline-start: var(--tree-connector-length);
|
|
124
|
-
border-inline-start: var(--tree-line-width) solid var(--tree-line-color);
|
|
125
|
-
}
|
|
126
|
-
.tree .tree-node:before {
|
|
127
|
-
content: "";
|
|
128
|
-
position: absolute;
|
|
129
|
-
top: 0;
|
|
130
|
-
left: 0;
|
|
131
|
-
right: 0;
|
|
132
|
-
width: var(--tree-connector-length);
|
|
133
|
-
height: var(--tree-connector-height);
|
|
134
|
-
max-height: 100%;
|
|
135
|
-
margin-inline-start: calc(0em - var(--tree-line-width));
|
|
136
|
-
border-bottom: var(--tree-line-width) solid var(--tree-line-color);
|
|
137
|
-
border-inline-start: var(--tree-line-width) solid var(--tree-line-color);
|
|
138
|
-
border-end-start-radius: var(--tree-connector-radius);
|
|
139
|
-
}
|
|
140
|
-
.tree .tree-node:last-child {
|
|
141
|
-
border-inline-start: var(--tree-line-width) solid transparent;
|
|
142
|
-
}
|
|
143
|
-
.tree .tree-node > .tree-node-content {
|
|
144
|
-
position: relative;
|
|
145
|
-
padding-inline-start: var(--tree-content-padding);
|
|
146
|
-
}
|
|
147
|
-
.tree .tree-node > .tree-node-content:before {
|
|
148
|
-
content: "";
|
|
149
|
-
position: absolute;
|
|
150
|
-
top: var(--tree-connector-height);
|
|
151
|
-
left: 0;
|
|
152
|
-
right: 0;
|
|
153
|
-
width: var(--tree-line-width);
|
|
154
|
-
height: calc(100% - var(--tree-connector-height));
|
|
155
|
-
border-inline-start: var(--tree-line-width) solid var(--tree-line-color);
|
|
156
|
-
}
|
|
157
|
-
.tree .tree-node > .tree-node-content:last-child:before {
|
|
158
|
-
border-inline-start: var(--tree-line-width) solid transparent;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.tree {
|
|
162
|
-
--tree-content-padding: 0.1em;
|
|
163
|
-
padding: 1em 1em 1em 2em;
|
|
164
|
-
}
|
|
165
|
-
.tree .tree-node > .tree-node-content {
|
|
166
|
-
display: flex;
|
|
167
|
-
gap: 0.2em;
|
|
168
|
-
}
|
|
169
|
-
.tree .tree-node > .tree-node-content > .icon-dir,
|
|
170
|
-
.tree .tree-node > .tree-node-content > .icon-file {
|
|
171
|
-
display: block;
|
|
172
|
-
width: 1.5em;
|
|
173
|
-
color: gray;
|
|
174
|
-
}
|
|
175
|
-
</script>
|
|
176
|
-
<script type="template/file" data-file="/fs/index.html">
|
|
177
|
-
<!DOCTYPE HTML>
|
|
178
|
-
<html>
|
|
179
|
-
<head>
|
|
180
|
-
<meta charset="utf-8" />
|
|
181
|
-
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
|
182
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
183
|
-
<link crossorigin="anonymous" media="all" rel="stylesheet" href="./files-tree.css" />
|
|
184
|
-
</head>
|
|
185
|
-
<body>
|
|
186
|
-
<h3>Local File System</h3>
|
|
187
|
-
<p>
|
|
188
|
-
This page allows you to get read/write access to your local file system and develop the whole site directly in the browser without an HTTP server.
|
|
189
|
-
</p>
|
|
190
|
-
<div>
|
|
191
|
-
<button id="fs-open">Open Workspace</button>
|
|
192
|
-
<button id="fs-redraw-tree" style="display: none;">Redraw Files Tree</button>
|
|
193
|
-
</div>
|
|
194
|
-
<div id="fs-view"></div>
|
|
195
|
-
|
|
196
|
-
<script type="module" src="./js/index.js"><\/script>
|
|
197
|
-
</body>
|
|
198
|
-
</html>
|
|
199
|
-
</script>
|
|
200
|
-
|
|
201
|
-
<script data-file="/fs/js/deps.js" type="template/file">
|
|
202
|
-
export * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
|
|
203
|
-
export { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
|
|
204
|
-
export {
|
|
205
|
-
BrowserFilesApi,
|
|
206
|
-
verifyPermission,
|
|
207
|
-
} from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
|
|
208
|
-
export * as mime from "https://esm.sh/mime-types";
|
|
209
|
-
</script>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<script data-file="/fs/js/openRepository.js" type="template/file">
|
|
213
|
-
import { BrowserFilesApi, verifyPermission } from "./deps.js";
|
|
214
|
-
export async function openRepository(readwrite = true) {
|
|
215
|
-
const handler = await window.showDirectoryPicker();
|
|
216
|
-
if (!(await verifyPermission(handler, readwrite))) {
|
|
217
|
-
throw new Error("Access was not granted");
|
|
218
|
-
}
|
|
219
|
-
const fs = new BrowserFilesApi({ rootHandler : handler });
|
|
220
|
-
return fs;
|
|
221
|
-
}
|
|
222
|
-
</script>
|
|
223
|
-
|
|
224
|
-
<script data-file="/fs/js/startHttpService.js" type="template/file">
|
|
225
|
-
import { WebRunHttp, Hono } from "./deps.js";
|
|
226
|
-
|
|
227
|
-
export async function startHttpService({
|
|
228
|
-
serviceKey,
|
|
229
|
-
router
|
|
230
|
-
}) {
|
|
231
|
-
// Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
|
|
232
|
-
const connection = await WebRunHttp.newRemoteRelayChannel();
|
|
233
|
-
|
|
234
|
-
// Step 2: Define an HTTP handler
|
|
235
|
-
const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
|
|
236
|
-
const app = new Hono({
|
|
237
|
-
getPath: (req) => req.url.substring(serviceBaseUrl.length)
|
|
238
|
-
});
|
|
239
|
-
// Add the router
|
|
240
|
-
app.route("/", router);
|
|
241
|
-
const httpHandler = (request) => app.fetch(request);
|
|
242
|
-
|
|
243
|
-
// Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
|
|
244
|
-
const close = await WebRunHttp.initHttpService(httpHandler, {
|
|
245
|
-
key: serviceKey, // Service prefix
|
|
246
|
-
port: connection.port
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
return serviceBaseUrl;
|
|
250
|
-
}
|
|
251
|
-
</script>
|
|
252
|
-
|
|
253
|
-
<script data-file="/fs/js/newFilesRouter.js" type="template/file">
|
|
254
|
-
import { Hono, WebRunHttp } from "./deps.js";
|
|
255
|
-
|
|
256
|
-
export function newFilesRouter(fs) {
|
|
257
|
-
const router = new Hono();
|
|
258
|
-
router.get("/*", async (context) => {
|
|
259
|
-
let { url, path } = context.req;
|
|
260
|
-
path = path.replace(/#.*$/, "");
|
|
261
|
-
if (path.match(/\/$/)) {
|
|
262
|
-
path = path + "index.html";
|
|
263
|
-
}
|
|
264
|
-
const fileInfo = await fs.stats(path);
|
|
265
|
-
if (!fileInfo || (fileInfo.kind !== "file")) {
|
|
266
|
-
return context.notFound();
|
|
267
|
-
}
|
|
268
|
-
const content = await fs.read(fileInfo);
|
|
269
|
-
const responseBody = await WebRunHttp.toReadableStream(content);
|
|
270
|
-
return new Response(responseBody, {
|
|
271
|
-
status: 200,
|
|
272
|
-
headers: {
|
|
273
|
-
"Content-Type": fileInfo.type,
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
return router;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
</script>
|
|
281
|
-
|
|
282
|
-
<script data-file="/fs/js/index.js" type="template/file">
|
|
283
|
-
console.log("Hell-o!");
|
|
284
|
-
const SERVICE_KEY = "FS"
|
|
285
|
-
import { Hono, WebRunHttp } from "./deps.js";
|
|
286
|
-
import { startHttpService } from "./startHttpService.js";
|
|
287
|
-
import { openRepository } from "./openRepository.js";
|
|
288
|
-
import { newFilesRouter } from "./newFilesRouter.js";
|
|
289
|
-
|
|
290
|
-
Promise.resolve().then(main).catch(console.error);
|
|
291
|
-
|
|
292
|
-
async function main() {
|
|
293
|
-
const openBtn = document.querySelector("#fs-open");
|
|
294
|
-
const refreshBtn = document.querySelector("#fs-redraw-tree");
|
|
295
|
-
let close;
|
|
296
40
|
openBtn.addEventListener("click", async () => {
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
serviceKey : SERVICE_KEY,
|
|
302
|
-
router
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
openBtn.parentElement.style.display = "none";
|
|
306
|
-
refreshBtn.style.display = "block";
|
|
307
|
-
renderFilesExplorer(fs, serviceBaseUrl);
|
|
308
|
-
refreshBtn.addEventListener("click", (event) => {
|
|
309
|
-
renderFilesExplorer(fs, serviceBaseUrl);
|
|
310
|
-
});
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async function renderFilesExplorer(fs, baseUrl) {
|
|
315
|
-
const container = document.querySelector("#fs-view");
|
|
316
|
-
container.innerHTML = "";
|
|
317
|
-
container.classList.add("tree");
|
|
318
|
-
for await (let file of fs.list("/", { recursive: false })) {
|
|
319
|
-
renderFile({
|
|
320
|
-
fs,
|
|
321
|
-
file,
|
|
322
|
-
baseUrl,
|
|
323
|
-
container
|
|
324
|
-
})
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function renderFile({
|
|
329
|
-
fs,
|
|
330
|
-
file,
|
|
331
|
-
container,
|
|
332
|
-
baseUrl
|
|
333
|
-
}) {
|
|
334
|
-
let path = file.path;
|
|
335
|
-
const isDirectory = file.kind === "directory";
|
|
336
|
-
if (isDirectory) {
|
|
337
|
-
if (path[path.length - 1] !== '/') {
|
|
338
|
-
path += '/';
|
|
41
|
+
const rootHandle = await window.showDirectoryPicker();
|
|
42
|
+
const permission = await rootHandle.requestPermission?.({ mode: "read" });
|
|
43
|
+
if (permission && permission !== "granted") {
|
|
44
|
+
throw new Error("Permission denied");
|
|
339
45
|
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const treeNodeElm = newElement("tree-node", container);
|
|
343
|
-
const treeNodeContentElm = newElement("tree-node-content", treeNodeElm);
|
|
344
|
-
const iconElm = newElement("tree-node-icon", treeNodeContentElm);
|
|
345
|
-
const iconCls = isDirectory ? "icon-dir" : "icon-file";
|
|
346
|
-
iconElm.classList.add(iconCls);
|
|
347
|
-
iconElm.innerText = isDirectory ? 'dir' : 'file';
|
|
348
46
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
})
|
|
369
|
-
}
|
|
370
|
-
} else {
|
|
371
|
-
let elm = treeNodeElm.querySelector(".tree-node-content");
|
|
372
|
-
elm = elm?.nextElementSibling;
|
|
373
|
-
while (elm) {
|
|
374
|
-
const next = elm.nextElementSibling;
|
|
375
|
-
treeNodeElm.removeChild(elm);
|
|
376
|
-
elm = next;
|
|
47
|
+
const SERVICE_KEY = "FS";
|
|
48
|
+
const connection = await WebRunHttp.newRemoteRelayChannel();
|
|
49
|
+
const serviceBaseUrl = `${connection.baseUrl}~${SERVICE_KEY}`;
|
|
50
|
+
|
|
51
|
+
await WebRunHttp.initHttpService(
|
|
52
|
+
async (request) => {
|
|
53
|
+
const url = new URL(request.url);
|
|
54
|
+
const rel = url.pathname.substring(
|
|
55
|
+
new URL(serviceBaseUrl).pathname.length,
|
|
56
|
+
);
|
|
57
|
+
const path = rel.replace(/^\/+/, "").replace(/\/$/, "");
|
|
58
|
+
try {
|
|
59
|
+
const file = await resolveFile(rootHandle, path);
|
|
60
|
+
if (!file) return new Response(null, { status: 404 });
|
|
61
|
+
const body = await file.stream();
|
|
62
|
+
return new Response(body, {
|
|
63
|
+
headers: { "Content-Type": file.type || guessMime(file.name) },
|
|
64
|
+
});
|
|
65
|
+
} catch (_err) {
|
|
66
|
+
return new Response(null, { status: 404 });
|
|
377
67
|
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
function newElement(className, parent, name = "div") {
|
|
383
|
-
const div = document.createElement(name);
|
|
384
|
-
className
|
|
385
|
-
.split(/\\s+/)
|
|
386
|
-
.map(s => s.trim())
|
|
387
|
-
.forEach(cls => div.classList.add(cls));
|
|
388
|
-
if (parent) {
|
|
389
|
-
parent.appendChild(div);
|
|
390
|
-
}
|
|
391
|
-
return div;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
</script>
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
<script type="template/file" data-file="/fs/index-1.html">
|
|
399
|
-
<!DOCTYPE HTML>
|
|
400
|
-
<html>
|
|
401
|
-
<head>
|
|
402
|
-
<meta charset="utf-8" />
|
|
403
|
-
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
|
404
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
405
|
-
<link crossorigin="anonymous" media="all" rel="stylesheet" href="./files-tree.css" />
|
|
406
|
-
</head>
|
|
407
|
-
<body>
|
|
408
|
-
<h3>Local File System</h3>
|
|
409
|
-
<p>
|
|
410
|
-
This page allows you to get read/write access to your local file system and develop the whole site directly in the browser without an HTTP server.
|
|
411
|
-
</p>
|
|
412
|
-
|
|
413
|
-
<div>
|
|
414
|
-
<button id="fs-open">Open File System</button>
|
|
415
|
-
<div><small>When you click on this button the browser will ask you to give access to the directory you want to explore in your browser.</small></div>
|
|
416
|
-
</div>
|
|
417
|
-
|
|
418
|
-
<div>
|
|
419
|
-
<button id="fs-redraw-tree" style="display: none;">Refresh</button>
|
|
420
|
-
<div id="fs-view"></div>
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
<script type="module">
|
|
424
|
-
const SERVICE_KEY = "FS"
|
|
425
|
-
const ENDPOINT_BASE_URL = "https://unpkg.com/@statewalker/webrun-http-browser@0.2.1/public-relay"
|
|
426
|
-
import {
|
|
427
|
-
BrowserFilesApi,
|
|
428
|
-
FilesApi,
|
|
429
|
-
isHandlerAccessible,
|
|
430
|
-
MemFilesApi,
|
|
431
|
-
verifyPermission,
|
|
432
|
-
} from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
|
|
433
|
-
import * as relayLib from "https://unpkg.com/@statewalker/webrun-http-browser@0.2.1/dist/index.js";
|
|
434
|
-
import { lookup } from "https://esm.sh/mime-types";
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
Promise.resolve().then(main).catch(console.error);
|
|
438
|
-
|
|
439
|
-
async function main() {
|
|
440
|
-
const openBtn = document.querySelector("#fs-open");
|
|
441
|
-
const refreshBtn = document.querySelector("#fs-redraw-tree");
|
|
442
|
-
let close;
|
|
443
|
-
openBtn.addEventListener("click", async () => {
|
|
444
|
-
if (close) close();
|
|
445
|
-
const fs = await openRepository();
|
|
446
|
-
const channel = await relayLib.newRemoteRelayChannel();
|
|
447
|
-
close = () => channel.close();
|
|
448
|
-
await startFileServer(SERVICE_KEY, fs, channel.port);
|
|
449
|
-
openBtn.parentElement.style.display = "none";
|
|
450
|
-
refreshBtn.style.display = "block";
|
|
451
|
-
renderFilesExplorer(fs);
|
|
452
|
-
refreshBtn.addEventListener("click", (event) => {
|
|
453
|
-
renderFilesExplorer(fs);
|
|
454
|
-
});
|
|
455
|
-
})
|
|
456
|
-
}
|
|
457
|
-
|
|
68
|
+
},
|
|
69
|
+
{ key: SERVICE_KEY, port: connection.port },
|
|
70
|
+
);
|
|
458
71
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
for await (let file of fs.list("/", { recursive: false })) {
|
|
465
|
-
renderFile({
|
|
466
|
-
fs,
|
|
467
|
-
file,
|
|
468
|
-
baseUrl,
|
|
469
|
-
container
|
|
470
|
-
})
|
|
471
|
-
}
|
|
472
|
-
}
|
|
72
|
+
openBtn.style.display = "none";
|
|
73
|
+
reloadBtn.style.display = "";
|
|
74
|
+
reloadBtn.addEventListener("click", () => renderTree(rootHandle, serviceBaseUrl));
|
|
75
|
+
renderTree(rootHandle, serviceBaseUrl);
|
|
76
|
+
});
|
|
473
77
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}) {
|
|
480
|
-
let path = file.path;
|
|
481
|
-
const isDirectory = file.kind === "directory";
|
|
482
|
-
if (isDirectory) {
|
|
483
|
-
if (path[path.length - 1] !== '/') {
|
|
484
|
-
path += '/';
|
|
78
|
+
async function resolveFile(root, path) {
|
|
79
|
+
const parts = path.split("/").filter(Boolean);
|
|
80
|
+
let dir = root;
|
|
81
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
82
|
+
dir = await dir.getDirectoryHandle(parts[i]);
|
|
485
83
|
}
|
|
84
|
+
const handle = await dir.getFileHandle(parts[parts.length - 1]);
|
|
85
|
+
return handle.getFile();
|
|
486
86
|
}
|
|
487
87
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
iconElm.classList.add(iconCls);
|
|
493
|
-
iconElm.innerText = isDirectory ? 'dir' : 'file';
|
|
494
|
-
|
|
495
|
-
const linkElm = newElement("file-name", treeNodeContentElm, "a");
|
|
496
|
-
const fileUrl = baseUrl + path;
|
|
497
|
-
linkElm.setAttribute("href", fileUrl);
|
|
498
|
-
linkElm.setAttribute("target", "_blank");
|
|
499
|
-
linkElm.innerText = file.name;
|
|
88
|
+
async function renderTree(rootHandle, baseUrl) {
|
|
89
|
+
filesView.innerHTML = "";
|
|
90
|
+
await renderDir(rootHandle, "", baseUrl, filesView);
|
|
91
|
+
}
|
|
500
92
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
93
|
+
async function renderDir(handle, prefix, baseUrl, parent) {
|
|
94
|
+
for await (const [name, entry] of handle.entries()) {
|
|
95
|
+
const node = document.createElement("div");
|
|
96
|
+
node.className = "tree-node";
|
|
97
|
+
const row = document.createElement("div");
|
|
98
|
+
row.className = "entry";
|
|
99
|
+
const path = `${prefix}${name}`;
|
|
100
|
+
if (entry.kind === "directory") {
|
|
101
|
+
row.textContent = `📁 ${name}`;
|
|
102
|
+
let expanded = false;
|
|
103
|
+
row.addEventListener("click", async () => {
|
|
104
|
+
expanded = !expanded;
|
|
105
|
+
if (expanded) await renderDir(entry, `${path}/`, baseUrl, node);
|
|
106
|
+
else for (const c of [...node.children].slice(1)) node.removeChild(c);
|
|
107
|
+
});
|
|
516
108
|
} else {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
elm = next;
|
|
523
|
-
}
|
|
109
|
+
const link = document.createElement("a");
|
|
110
|
+
link.href = `${baseUrl}/${path}`;
|
|
111
|
+
link.target = "preview";
|
|
112
|
+
link.textContent = `📄 ${name}`;
|
|
113
|
+
row.appendChild(link);
|
|
524
114
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
function newElement(className, parent, name = "div") {
|
|
529
|
-
const div = document.createElement(name);
|
|
530
|
-
className
|
|
531
|
-
.split(/\\s+/)
|
|
532
|
-
.map(s => s.trim())
|
|
533
|
-
.forEach(cls => div.classList.add(cls));
|
|
534
|
-
if (parent) {
|
|
535
|
-
parent.appendChild(div);
|
|
115
|
+
node.appendChild(row);
|
|
116
|
+
parent.appendChild(node);
|
|
536
117
|
}
|
|
537
|
-
return div;
|
|
538
118
|
}
|
|
539
|
-
}
|
|
540
119
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
120
|
+
function guessMime(name) {
|
|
121
|
+
const ext = name.toLowerCase().split(".").pop();
|
|
122
|
+
return ({
|
|
123
|
+
html: "text/html",
|
|
124
|
+
css: "text/css",
|
|
125
|
+
js: "text/javascript",
|
|
126
|
+
json: "application/json",
|
|
127
|
+
png: "image/png",
|
|
128
|
+
jpg: "image/jpeg",
|
|
129
|
+
jpeg: "image/jpeg",
|
|
130
|
+
svg: "image/svg+xml",
|
|
131
|
+
txt: "text/plain",
|
|
132
|
+
md: "text/markdown",
|
|
133
|
+
}[ext] ?? "application/octet-stream");
|
|
545
134
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async function startFileServer(serviceKey, fs, port) {
|
|
551
|
-
await relayLib.initHttpService(async (req) => {
|
|
552
|
-
try {
|
|
553
|
-
let path = new URL(req.url).pathname;
|
|
554
|
-
path = path.replace(/^[^~]+~[^\\/]+?(\\/.*)$/, '$1');
|
|
555
|
-
if (path[path.length - 1] === '/') {
|
|
556
|
-
path += 'index.html';
|
|
557
|
-
}
|
|
558
|
-
const mimeType = lookup(path.toLowerCase()) || "application/octet-stream";
|
|
559
|
-
const fileInfo = await fs.stats(path);
|
|
560
|
-
if (!fileInfo || (fileInfo.kind !== "file")) {
|
|
561
|
-
return new Response(JSON.stringify({
|
|
562
|
-
status: 404,
|
|
563
|
-
statusText: "File not found. Path: '" + path + "'.",
|
|
564
|
-
}, null, 2), {
|
|
565
|
-
status: 404,
|
|
566
|
-
statusText : "File not found. Path: '" + path + "'.",
|
|
567
|
-
headers : {
|
|
568
|
-
"Content-Type": "application/json"
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
const content = await fs.read(fileInfo);
|
|
573
|
-
const responseBody = await relayLib.toReadableStream(content);
|
|
574
|
-
return new Response(responseBody, {
|
|
575
|
-
status: 200,
|
|
576
|
-
headers: {
|
|
577
|
-
"Content-Type": fileInfo.type,
|
|
578
|
-
},
|
|
579
|
-
});
|
|
580
|
-
} catch (error) {
|
|
581
|
-
console.error(error);
|
|
582
|
-
return new Response(JSON.stringify(error), {
|
|
583
|
-
status: 500,
|
|
584
|
-
statusText: error.message,
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
}, {
|
|
588
|
-
key: serviceKey,
|
|
589
|
-
port,
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
<\/script>
|
|
593
|
-
</body>
|
|
135
|
+
</script>
|
|
136
|
+
</body>
|
|
594
137
|
</html>
|
|
595
|
-
</script>
|
|
596
|
-
</body>
|
|
597
|
-
|
|
598
|
-
</html>
|