@nivalos/lithium.js 1.1.0
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 +661 -0
- package/README.md +317 -0
- package/bin/lithium.js +40 -0
- package/client/backends/scramjet.js +53 -0
- package/client/backends/ultraviolet.js +30 -0
- package/client/index.js +359 -0
- package/client/net.js +114 -0
- package/client/sw.js +33 -0
- package/package.json +69 -0
- package/server/color.js +16 -0
- package/server/doctor.js +132 -0
- package/server/errors.js +21 -0
- package/server/index.js +238 -0
- package/server/meta.js +55 -0
- package/server/pkgs.js +91 -0
- package/server/registry.js +262 -0
- package/server/start.js +14 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
import { dirname } from "node:path"
|
|
3
|
+
|
|
4
|
+
import { uvPath } from "@titaniumnetwork-dev/ultraviolet"
|
|
5
|
+
import { scramjetPath } from "@mercuryworkshop/scramjet/path"
|
|
6
|
+
import { baremuxPath } from "@mercuryworkshop/bare-mux/node"
|
|
7
|
+
// npm aliases for the older (bare-mux generation) transports, see meta.js
|
|
8
|
+
import { epoxyPath as epoxy_bm_path } from "epoxy-transport-bm"
|
|
9
|
+
import { libcurlPath as libcurl_bm_path } from "libcurl-transport-bm"
|
|
10
|
+
|
|
11
|
+
import { PROXY_META, TRANSPORT_META } from "./meta.js"
|
|
12
|
+
import { LithiumError } from "./errors.js"
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url)
|
|
15
|
+
// browser files for the 2.x packages live next to their entry point
|
|
16
|
+
const dir_of = (specifier) => dirname(require.resolve(specifier))
|
|
17
|
+
|
|
18
|
+
// Ultraviolet config, served at /uv/uv.config.js. We generate it because the
|
|
19
|
+
// one shipped in the package points at root-relative files (/uv.sw.js, ...)
|
|
20
|
+
// that don't exist when everything is served under /uv/. UV injects these
|
|
21
|
+
// paths into every proxied page, so they all have to resolve. `prefix` is
|
|
22
|
+
// where proxied pages live, deliberately outside /uv/ (the static files).
|
|
23
|
+
const UV_CONFIG = `self.__uv$config = {
|
|
24
|
+
prefix: "/service/",
|
|
25
|
+
encodeUrl: Ultraviolet.codec.xor.encode,
|
|
26
|
+
decodeUrl: Ultraviolet.codec.xor.decode,
|
|
27
|
+
handler: "/uv/uv.handler.js",
|
|
28
|
+
client: "/uv/uv.client.js",
|
|
29
|
+
bundle: "/uv/uv.bundle.js",
|
|
30
|
+
config: "/uv/uv.config.js",
|
|
31
|
+
sw: "/uv/uv.sw.js",
|
|
32
|
+
};
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
const proxy_defs = new Map()
|
|
36
|
+
const transport_defs = new Map()
|
|
37
|
+
|
|
38
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/
|
|
39
|
+
|
|
40
|
+
// --------------------------------------------------------------- built-ins --
|
|
41
|
+
// A proxy definition:
|
|
42
|
+
// name, interface, isolation, packages
|
|
43
|
+
// routes(app) optional, runs BEFORE mounts (so it can shadow a file)
|
|
44
|
+
// mounts() [{ label, url, dir, files }] static folders to serve
|
|
45
|
+
// clientModule URL of the browser-side module (custom proxies only,
|
|
46
|
+
// built-ins are bundled with the client)
|
|
47
|
+
// serviceWorker JS source for the worker (custom proxies only,
|
|
48
|
+
// built-ins use client/sw.js)
|
|
49
|
+
proxy_defs.set("ultraviolet", {
|
|
50
|
+
name: "ultraviolet",
|
|
51
|
+
...PROXY_META.ultraviolet,
|
|
52
|
+
builtin: true,
|
|
53
|
+
routes(app) {
|
|
54
|
+
app.get("/uv/uv.config.js", (_req, res) => {
|
|
55
|
+
res.type("js").send(UV_CONFIG)
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
mounts: () => [
|
|
59
|
+
{ label: "ultraviolet", url: "/uv/", dir: uvPath, files: ["uv.bundle.js", "uv.sw.js", "uv.handler.js", "uv.client.js"] },
|
|
60
|
+
{ label: "bare-mux", url: "/baremux/", dir: baremuxPath, files: ["worker.js"] },
|
|
61
|
+
],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
proxy_defs.set("scramjet", {
|
|
65
|
+
name: "scramjet",
|
|
66
|
+
...PROXY_META.scramjet,
|
|
67
|
+
builtin: true,
|
|
68
|
+
mounts: () => [
|
|
69
|
+
{ label: "scramjet", url: "/scram/", dir: scramjetPath, files: ["scramjet.js", "scramjet.wasm"] },
|
|
70
|
+
{ label: "scramjet-controller", url: "/controller/", dir: dir_of("@mercuryworkshop/scramjet-controller"), files: ["controller.sw.js", "controller.api.js", "controller.inject.js"] },
|
|
71
|
+
{ label: "scramjet-utils", url: "/utils/", dir: dir_of("@mercuryworkshop/scramjet-utils"), files: ["scramjet-utils.js"] },
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// A transport definition:
|
|
76
|
+
// name
|
|
77
|
+
// interfaces: { [interface]: { package?, major?, mount, dir, entry? } }
|
|
78
|
+
// mount URL folder it's served under (e.g. "/epoxy/")
|
|
79
|
+
// dir folder on disk, or a function returning it
|
|
80
|
+
// entry file the browser imports (default "index.mjs")
|
|
81
|
+
transport_defs.set("epoxy", {
|
|
82
|
+
name: "epoxy",
|
|
83
|
+
builtin: true,
|
|
84
|
+
interfaces: {
|
|
85
|
+
"bare-mux": { ...TRANSPORT_META.epoxy["bare-mux"], mount: "/bm/epoxy/", dir: () => epoxy_bm_path },
|
|
86
|
+
"proxy-transports": { ...TRANSPORT_META.epoxy["proxy-transports"], mount: "/epoxy/", dir: () => dir_of("@mercuryworkshop/epoxy-transport") },
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
transport_defs.set("libcurl", {
|
|
91
|
+
name: "libcurl",
|
|
92
|
+
builtin: true,
|
|
93
|
+
interfaces: {
|
|
94
|
+
"bare-mux": { ...TRANSPORT_META.libcurl["bare-mux"], mount: "/bm/libcurl/", dir: () => libcurl_bm_path },
|
|
95
|
+
"proxy-transports": { ...TRANSPORT_META.libcurl["proxy-transports"], mount: "/libcurl/", dir: () => dir_of("@mercuryworkshop/libcurl-transport") },
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// ------------------------------------------------------------ compatibility --
|
|
100
|
+
export function supports(proxy, transport) {
|
|
101
|
+
const p = proxy_defs.get(proxy)
|
|
102
|
+
const t = transport_defs.get(transport)
|
|
103
|
+
return Boolean(p && t && t.interfaces[p.interface])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// -> { supported, interface, reason }
|
|
107
|
+
export function get_compatibility(proxy, transport) {
|
|
108
|
+
const p = proxy_defs.get(proxy)
|
|
109
|
+
const t = transport_defs.get(transport)
|
|
110
|
+
if (!p) return { supported: false, interface: null, reason: `unknown proxy "${proxy}"` }
|
|
111
|
+
if (!t) return { supported: false, interface: p.interface, reason: `unknown transport "${transport}"` }
|
|
112
|
+
if (!t.interfaces[p.interface]) {
|
|
113
|
+
return {
|
|
114
|
+
supported: false,
|
|
115
|
+
interface: p.interface,
|
|
116
|
+
reason: `${proxy} uses the ${p.interface} interface, ${transport} only provides: ${Object.keys(t.interfaces).join(", ")}`,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { supported: true, interface: p.interface, reason: null }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------- public api --
|
|
123
|
+
export const proxies = {
|
|
124
|
+
list: () => [...proxy_defs.keys()],
|
|
125
|
+
has: (name) => proxy_defs.has(name),
|
|
126
|
+
|
|
127
|
+
// plain-data description (safe to log / JSON.stringify)
|
|
128
|
+
get(name) {
|
|
129
|
+
const p = proxy_defs.get(name)
|
|
130
|
+
if (!p) return null
|
|
131
|
+
return {
|
|
132
|
+
name: p.name,
|
|
133
|
+
interface: p.interface,
|
|
134
|
+
isolation: Boolean(p.isolation),
|
|
135
|
+
builtin: Boolean(p.builtin),
|
|
136
|
+
transports: transports.list().filter((t) => supports(name, t)),
|
|
137
|
+
packages: (p.packages ?? []).map((x) => x.name),
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Register your own proxy engine. You provide the server side (what to
|
|
142
|
+
// serve), the service worker source and a browser module (see README,
|
|
143
|
+
// "Custom backends").
|
|
144
|
+
register(name, def = {}) {
|
|
145
|
+
if (typeof name !== "string" || !NAME_RE.test(name)) {
|
|
146
|
+
throw new LithiumError("INVALID_BACKEND", `proxy name must match ${NAME_RE}`, { name })
|
|
147
|
+
}
|
|
148
|
+
if (proxy_defs.has(name)) throw new LithiumError("BACKEND_EXISTS", `proxy "${name}" is already registered`, { name })
|
|
149
|
+
|
|
150
|
+
const missing = ["interface", "serviceWorker", "clientModule"].filter((k) => typeof def[k] !== "string" || !def[k])
|
|
151
|
+
if (missing.length) {
|
|
152
|
+
throw new LithiumError("INVALID_BACKEND", `proxy "${name}" is missing: ${missing.join(", ")}`, { name, missing })
|
|
153
|
+
}
|
|
154
|
+
if (def.mounts != null && typeof def.mounts !== "function") {
|
|
155
|
+
throw new LithiumError("INVALID_BACKEND", `proxy "${name}": mounts must be a function returning [{ label, url, dir, files }]`, { name })
|
|
156
|
+
}
|
|
157
|
+
if (def.routes != null && typeof def.routes !== "function") {
|
|
158
|
+
throw new LithiumError("INVALID_BACKEND", `proxy "${name}": routes must be a function (app) => void`, { name })
|
|
159
|
+
}
|
|
160
|
+
proxy_defs.set(name, {
|
|
161
|
+
name,
|
|
162
|
+
interface: def.interface,
|
|
163
|
+
isolation: Boolean(def.isolation),
|
|
164
|
+
packages: def.packages ?? [],
|
|
165
|
+
routes: def.routes,
|
|
166
|
+
mounts: def.mounts ?? (() => []),
|
|
167
|
+
serviceWorker: def.serviceWorker,
|
|
168
|
+
clientModule: def.clientModule,
|
|
169
|
+
builtin: false,
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const transports = {
|
|
175
|
+
list: () => [...transport_defs.keys()],
|
|
176
|
+
has: (name) => transport_defs.has(name),
|
|
177
|
+
|
|
178
|
+
get(name) {
|
|
179
|
+
const t = transport_defs.get(name)
|
|
180
|
+
if (!t) return null
|
|
181
|
+
return {
|
|
182
|
+
name: t.name,
|
|
183
|
+
builtin: Boolean(t.builtin),
|
|
184
|
+
interfaces: Object.keys(t.interfaces),
|
|
185
|
+
proxies: proxies.list().filter((p) => supports(p, name)),
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
register(name, def = {}) {
|
|
190
|
+
if (typeof name !== "string" || !NAME_RE.test(name)) {
|
|
191
|
+
throw new LithiumError("INVALID_BACKEND", `transport name must match ${NAME_RE}`, { name })
|
|
192
|
+
}
|
|
193
|
+
if (transport_defs.has(name)) throw new LithiumError("BACKEND_EXISTS", `transport "${name}" is already registered`, { name })
|
|
194
|
+
|
|
195
|
+
const entries = Object.entries(def.interfaces ?? {})
|
|
196
|
+
if (!entries.length) {
|
|
197
|
+
throw new LithiumError("INVALID_BACKEND", `transport "${name}" must provide at least one interface`, { name })
|
|
198
|
+
}
|
|
199
|
+
for (const [iface, impl] of entries) {
|
|
200
|
+
const ok = impl && typeof impl.mount === "string" && impl.mount.startsWith("/") && impl.mount.endsWith("/") && impl.dir
|
|
201
|
+
if (!ok) {
|
|
202
|
+
throw new LithiumError("INVALID_BACKEND", `transport "${name}", interface "${iface}": needs mount ("/some/path/") and dir`, { name, interface: iface })
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
transport_defs.set(name, { name, builtin: false, interfaces: Object.fromEntries(entries) })
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ------------------------------------------------------------- for the server --
|
|
210
|
+
// Turns names into the full definitions, or throws a LithiumError that says
|
|
211
|
+
// what's wrong and what would work instead.
|
|
212
|
+
export function resolve_backend(proxy, transport) {
|
|
213
|
+
const p = proxy_defs.get(proxy)
|
|
214
|
+
if (!p) {
|
|
215
|
+
throw new LithiumError("UNKNOWN_PROXY", `unknown proxy "${proxy}" (expected: ${proxies.list().join(", ")})`, {
|
|
216
|
+
proxy, available: proxies.list(),
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
const t = transport_defs.get(transport)
|
|
220
|
+
if (!t) {
|
|
221
|
+
throw new LithiumError("UNKNOWN_TRANSPORT", `unknown transport "${transport}" (expected: ${transports.list().join(", ")})`, {
|
|
222
|
+
transport, available: transports.list(),
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
const impl = t.interfaces[p.interface]
|
|
226
|
+
if (!impl) {
|
|
227
|
+
throw new LithiumError(
|
|
228
|
+
"INCOMPATIBLE_TRANSPORT",
|
|
229
|
+
`transport "${transport}" can't be used with ${proxy}: ${proxy} uses the ${p.interface} interface, ${transport} only provides ${Object.keys(t.interfaces).join(", ")}`,
|
|
230
|
+
{
|
|
231
|
+
proxy, transport, proxyInterface: p.interface, transportInterfaces: Object.keys(t.interfaces),
|
|
232
|
+
compatibleTransports: transports.list().filter((x) => supports(proxy, x)),
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
return { proxy: p, transport: t, interface: p.interface, impl }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// every static folder the backend needs: the proxy's own + the transport's
|
|
240
|
+
export function mounts_for(backend) {
|
|
241
|
+
const list = backend.proxy.mounts()
|
|
242
|
+
const dir = typeof backend.impl.dir === "function" ? backend.impl.dir() : backend.impl.dir
|
|
243
|
+
list.push({
|
|
244
|
+
label: `${backend.transport.name} (${backend.interface} interface)`,
|
|
245
|
+
url: backend.impl.mount,
|
|
246
|
+
dir,
|
|
247
|
+
files: [backend.impl.entry ?? "index.mjs"],
|
|
248
|
+
})
|
|
249
|
+
return list
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// what check_packages() needs to verify this combination
|
|
253
|
+
export function packages_for(backend) {
|
|
254
|
+
const list = (backend.proxy.packages ?? []).map((p) => ({ ...p, role: "proxy" }))
|
|
255
|
+
if (backend.impl.package) {
|
|
256
|
+
list.push({
|
|
257
|
+
name: backend.impl.package, major: backend.impl.major, role: "transport",
|
|
258
|
+
transport: backend.transport.name, interface: backend.interface,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
return list
|
|
262
|
+
}
|
package/server/start.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// `npm start`: quick way to try lithium without writing your own server file.
|
|
2
|
+
// PROXY=scramjet TRANSPORT=libcurl PORT=3000 STATIC_DIR=public npm start
|
|
3
|
+
import { create_lithium_server } from "./index.js"
|
|
4
|
+
|
|
5
|
+
const { server, port } = create_lithium_server({
|
|
6
|
+
staticDir: process.env.STATIC_DIR || "public",
|
|
7
|
+
port: Number(process.env.PORT) || 8080,
|
|
8
|
+
proxy: process.env.PROXY || "ultraviolet",
|
|
9
|
+
transport: process.env.TRANSPORT || "epoxy",
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
server.listen(port, () => {
|
|
13
|
+
console.log(`Lithium server running on http://localhost:${port}`)
|
|
14
|
+
})
|