@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.
@@ -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
+ }
@@ -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
+ })