vite-plugin-io1 0.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 +21 -0
- package/README.md +94 -0
- package/dist/announce.d.ts +38 -0
- package/dist/announce.d.ts.map +1 -0
- package/dist/announce.js +42 -0
- package/dist/announce.js.map +1 -0
- package/dist/binary.d.ts +50 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +137 -0
- package/dist/binary.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +127 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/src/announce.ts +54 -0
- package/src/binary.ts +175 -0
- package/src/index.ts +165 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bitmot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# vite-plugin-io1
|
|
2
|
+
|
|
3
|
+
Publish your Vite dev server to the public internet over a [bitmot io1](https://bitmot.com) relay: a
|
|
4
|
+
stable, optionally email-gated HTTPS URL that hot-reloads as you edit. The plugin manages the `bitmot`
|
|
5
|
+
CLI for you.
|
|
6
|
+
|
|
7
|
+
While `vite dev` (or `vite preview`) runs, the plugin starts a `bitmot io1 relay` pointed at the
|
|
8
|
+
server's real bound port and tears it down when the server stops. The public URL appears in the
|
|
9
|
+
startup output.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
- **Real HTTPS, real cert** so secure-context APIs (service workers, camera, WebCrypto) just work.
|
|
14
|
+
- **Working HMR through the tunnel** -- the shared URL is your live dev server, not a static copy.
|
|
15
|
+
- **Email-gated sharing** via `allowedEmails` makes a preview shared-but-not-public.
|
|
16
|
+
- **No `server.allowedHosts` fuss** -- the relay presents the upstream's own Host, so Vite's host
|
|
17
|
+
check never trips.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm i -D vite-plugin-io1
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You do **not** need to install the `bitmot` CLI first: the plugin downloads the current build for
|
|
26
|
+
your platform on demand (content-addressed and checksum-verified) into `~/.cache/bitmot`. If you
|
|
27
|
+
already have `bitmot` on your PATH, that copy is used instead.
|
|
28
|
+
|
|
29
|
+
One-time per machine, authorize the CLI so the relay can claim hosts on your account:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
bitmot io1 authorize
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// vite.config.ts
|
|
39
|
+
import { defineConfig } from 'vite'
|
|
40
|
+
import { io1 } from 'vite-plugin-io1'
|
|
41
|
+
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
plugins: [
|
|
44
|
+
io1({
|
|
45
|
+
host: process.env.IO1_DEV_HOST, // e.g. "🔥.io1.io" or "demo" (-> demo.io1.io)
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The plugin stays dormant unless a host is configured (via `host` or `$IO1_DEV_HOST`): publishing
|
|
52
|
+
every dev server by default would be a bit dicey, and a hardcoded shared host collides between
|
|
53
|
+
teammates. Point `IO1_DEV_HOST` at a per-developer host in your shell profile, or set `host`
|
|
54
|
+
explicitly for a shared demo host.
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
| Option | Type | Default | Description |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| `host` | `string \| string[]` | `$IO1_DEV_HOST` | Host(s) to claim. A bare name is completed to `.io1.io`. |
|
|
61
|
+
| `allowedEmails` | `string[]` | none | Restrict visitors to these verified emails. |
|
|
62
|
+
| `account` | `string` | inferred | Account claim, when your identity has several memberships. |
|
|
63
|
+
| `gateway` | `string` | production | Gateway WebSocket URL override (for a local/dev gateway). |
|
|
64
|
+
| `bitmotPath` | `string` | resolved | Explicit CLI path; skips PATH lookup and download. |
|
|
65
|
+
| `autoInstall` | `boolean` | `true` | Download the CLI when not found. `false` makes a miss an error. |
|
|
66
|
+
| `baseUrl` | `string` | `https://bitmot.com` | Download origin for the CLI. |
|
|
67
|
+
| `extraArgs` | `string[]` | none | Extra `bitmot io1 relay` flags, inserted before `--to`. |
|
|
68
|
+
| `enabled` | `boolean` | on, unless CI or no host | Master switch. |
|
|
69
|
+
|
|
70
|
+
## How the CLI stays current
|
|
71
|
+
|
|
72
|
+
The `bitmot` CLI carries a version (`bitmot version`), and the gateway advertises the latest one on
|
|
73
|
+
every relay handshake; run by hand, the CLI nudges you when you are out of date. That nudge is
|
|
74
|
+
deliberately silent when its output is piped or `CI` is set -- that is, whenever this plugin runs it.
|
|
75
|
+
So the plugin keeps the CLI current a different way: auto-download resolves the **currently deployed**
|
|
76
|
+
bytes by content hash, so a plugin-managed CLI is always up to date and the nudge is moot. A CLI you
|
|
77
|
+
installed yourself on your PATH is respected as-is and never replaced.
|
|
78
|
+
|
|
79
|
+
## Caveats
|
|
80
|
+
|
|
81
|
+
- **macOS and Linux** -- published binaries cover macOS (arm64/x86_64) and Linux (x86_64/aarch64).
|
|
82
|
+
Elsewhere (e.g. native Windows), install `bitmot` yourself (the plugin will use it) or set
|
|
83
|
+
`bitmotPath`.
|
|
84
|
+
- **Middleware mode** (e.g. Nuxt) exposes no `httpServer` for the plugin to hook; not yet supported.
|
|
85
|
+
- The relay is a child process tied to the dev server. A hard crash of the Vite process (SIGKILL) can
|
|
86
|
+
orphan it; a normal stop or Ctrl-C cleans it up.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
npm install
|
|
92
|
+
npm test # vitest: arg-building, announcement parsing, download+verify, spawn/kill lifecycle
|
|
93
|
+
npm run build # tsc -> dist
|
|
94
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure argument-building and output-parsing for the bitmot io1 relay. Kept free of I/O so the wire
|
|
3
|
+
* contract with the CLI -- which flags it takes, and how it announces a live host -- is unit-tested
|
|
4
|
+
* in isolation and lives in one place.
|
|
5
|
+
*/
|
|
6
|
+
export interface RelayInvocation {
|
|
7
|
+
/** Hosts to claim, one `--host` each (the CLI appends across repeats). At least one. */
|
|
8
|
+
readonly hosts: readonly string[];
|
|
9
|
+
/** Allow-list emails; the CLI takes a single comma-separated `--allowed-emails`, not repeats. */
|
|
10
|
+
readonly allowedEmails?: readonly string[];
|
|
11
|
+
/** Account claim, when the identity has more than one membership. */
|
|
12
|
+
readonly account?: string;
|
|
13
|
+
/** Gateway WebSocket URL; omitted to use the CLI's baked-in production default. */
|
|
14
|
+
readonly gateway?: string;
|
|
15
|
+
/** Host header presented to the gateway; omitted to derive it from the gateway URL. */
|
|
16
|
+
readonly gatewayHost?: string;
|
|
17
|
+
/** Escape hatch for flags this plugin does not model (e.g. `--credential`). Inserted verbatim. */
|
|
18
|
+
readonly extraArgs?: readonly string[];
|
|
19
|
+
/** The local dev server to proxy to, e.g. `http://127.0.0.1:5173`. */
|
|
20
|
+
readonly to: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The full `bitmot io1 relay ...` argv (without the leading program name). `--allowed-emails` is
|
|
24
|
+
* comma-joined into a single flag because the CLI parses it as one comma-separated value; emitting
|
|
25
|
+
* repeats would make the native CLI keep only the last. `--to` goes last so the line reads as a claim
|
|
26
|
+
* of hosts proxied to a target.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildRelayArgs(invocation: RelayInvocation): string[];
|
|
29
|
+
/**
|
|
30
|
+
* The public URL from a relay's per-host announcement line, or null for any other line. The relay
|
|
31
|
+
* prints one `<arrow> <url> live` line per claimed host once its link is up (uncolored when its
|
|
32
|
+
* stderr is piped, as it is under this plugin). Tolerant of the arrow glyph and surrounding spaces so
|
|
33
|
+
* both the native and JVM CLIs -- and any cached older build -- parse the same; the trailing `live`
|
|
34
|
+
* badge is what distinguishes it from the pending-requests and update-nudge lines, which also carry
|
|
35
|
+
* URLs but must not be mistaken for a live host.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseLiveUrl(line: string): string | null;
|
|
38
|
+
//# sourceMappingURL=announce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"announce.d.ts","sourceRoot":"","sources":["../src/announce.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,iGAAiG;IACjG,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,uFAAuF;IACvF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kGAAkG;IAClG,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,GAAG,MAAM,EAAE,CAWpE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGxD"}
|
package/dist/announce.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure argument-building and output-parsing for the bitmot io1 relay. Kept free of I/O so the wire
|
|
3
|
+
* contract with the CLI -- which flags it takes, and how it announces a live host -- is unit-tested
|
|
4
|
+
* in isolation and lives in one place.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The full `bitmot io1 relay ...` argv (without the leading program name). `--allowed-emails` is
|
|
8
|
+
* comma-joined into a single flag because the CLI parses it as one comma-separated value; emitting
|
|
9
|
+
* repeats would make the native CLI keep only the last. `--to` goes last so the line reads as a claim
|
|
10
|
+
* of hosts proxied to a target.
|
|
11
|
+
*/
|
|
12
|
+
export function buildRelayArgs(invocation) {
|
|
13
|
+
const args = ['io1', 'relay'];
|
|
14
|
+
for (const host of invocation.hosts)
|
|
15
|
+
args.push('--host', host);
|
|
16
|
+
const emails = invocation.allowedEmails ?? [];
|
|
17
|
+
if (emails.length > 0)
|
|
18
|
+
args.push('--allowed-emails', emails.join(','));
|
|
19
|
+
if (invocation.account)
|
|
20
|
+
args.push('--account', invocation.account);
|
|
21
|
+
if (invocation.gateway)
|
|
22
|
+
args.push('--gateway', invocation.gateway);
|
|
23
|
+
if (invocation.gatewayHost)
|
|
24
|
+
args.push('--gateway-host', invocation.gatewayHost);
|
|
25
|
+
if (invocation.extraArgs)
|
|
26
|
+
args.push(...invocation.extraArgs);
|
|
27
|
+
args.push('--to', invocation.to);
|
|
28
|
+
return args;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The public URL from a relay's per-host announcement line, or null for any other line. The relay
|
|
32
|
+
* prints one `<arrow> <url> live` line per claimed host once its link is up (uncolored when its
|
|
33
|
+
* stderr is piped, as it is under this plugin). Tolerant of the arrow glyph and surrounding spaces so
|
|
34
|
+
* both the native and JVM CLIs -- and any cached older build -- parse the same; the trailing `live`
|
|
35
|
+
* badge is what distinguishes it from the pending-requests and update-nudge lines, which also carry
|
|
36
|
+
* URLs but must not be mistaken for a live host.
|
|
37
|
+
*/
|
|
38
|
+
export function parseLiveUrl(line) {
|
|
39
|
+
const match = /(https?:\/\/\S+)\s+live\s*$/u.exec(line);
|
|
40
|
+
return match ? match[1] : null;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=announce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"announce.js","sourceRoot":"","sources":["../src/announce.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmBH;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,UAA2B;IACxD,MAAM,IAAI,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC7B,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,IAAI,EAAE,CAAA;IAC7C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACtE,IAAI,UAAU,CAAC,OAAO;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAClE,IAAI,UAAU,CAAC,OAAO;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAClE,IAAI,UAAU,CAAC,WAAW;QAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;IAC/E,IAAI,UAAU,CAAC,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;IAC5D,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;IAChC,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,8BAA8B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACjC,CAAC"}
|
package/dist/binary.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface ResolveOptions {
|
|
2
|
+
/** Explicit path to a bitmot binary; when set it is used as-is (no PATH search, no download). */
|
|
3
|
+
readonly bitmotPath?: string;
|
|
4
|
+
/** Download the CLI when not otherwise found. Default true; false makes a miss a hard error. */
|
|
5
|
+
readonly autoInstall?: boolean;
|
|
6
|
+
/** Download origin; mirrors the installer's BITMOT_BASE_URL. Default https://bitmot.com. */
|
|
7
|
+
readonly baseUrl?: string;
|
|
8
|
+
/** Where to cache downloaded binaries. Default ~/.cache/bitmot (or $XDG_CACHE_HOME/bitmot). */
|
|
9
|
+
readonly cacheDir?: string;
|
|
10
|
+
/** Sink for human-facing progress ("downloading ..."). */
|
|
11
|
+
readonly log?: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ResolvedBinary {
|
|
14
|
+
/** Path to invoke. */
|
|
15
|
+
readonly path: string;
|
|
16
|
+
/** How it was found, for logging. */
|
|
17
|
+
readonly source: 'explicit' | 'path' | 'download' | 'cache';
|
|
18
|
+
/** `bitmot version`'s reported semver (e.g. "0.2.1"), or null if it could not be read. */
|
|
19
|
+
readonly version: string | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The published artifact name for a platform/arch, or null when unsupported. Mirrors the server's
|
|
23
|
+
* download whitelist (io/io1/gateway/download/DownloadRoutesImpl.kt) and the installer's `uname`
|
|
24
|
+
* mapping: macOS and Linux, on x86_64 or 64-bit ARM. Node's arch is translated to the server's
|
|
25
|
+
* spelling -- 'x64' -> 'x86_64' on both, but 64-bit ARM is 'arm64' on macOS and 'aarch64' on Linux.
|
|
26
|
+
*/
|
|
27
|
+
export declare function artifactFor(platform: NodeJS.Platform, arch: string): string | null;
|
|
28
|
+
/** `bitmot version` -> "0.2.1", or null if the binary is missing or its output is unexpected. */
|
|
29
|
+
export declare function readVersion(binary: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Ensures the current published binary is present in the cache and returns its path. The stable
|
|
32
|
+
* download URL 302-redirects to the content-addressed URL of whatever bytes are deployed now, so the
|
|
33
|
+
* followed response's checksum header names the current version: if a file for that hash is already
|
|
34
|
+
* cached the body is dropped undownloaded, otherwise it is streamed, verified against the advertised
|
|
35
|
+
* checksum, and atomically installed. This is what makes a plugin-managed CLI track production
|
|
36
|
+
* automatically -- so the CLI's own "you're out of date" nudge (suppressed anyway when its stderr is
|
|
37
|
+
* piped, as it is here) does not matter for the auto-managed case.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureDownloaded(baseUrl: string, artifact: string, cacheDir: string, log?: (message: string) => void): Promise<{
|
|
40
|
+
path: string;
|
|
41
|
+
downloaded: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a bitmot binary to invoke: an explicit path wins; else a bitmot already on PATH is used
|
|
45
|
+
* (respecting a user- or installer-managed copy); else, when autoInstall is on, the current published
|
|
46
|
+
* binary is downloaded and cached. Throws with an actionable message when nothing is available and
|
|
47
|
+
* downloading is off or the platform has no published artifact.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveBitmot(options?: ResolveOptions): Promise<ResolvedBinary>;
|
|
50
|
+
//# sourceMappingURL=binary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary.d.ts","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,cAAc;IAC7B,iGAAiG;IACjG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,sBAAsB;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAA;IAC3D,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYlF;AAgBD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzD;AASD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC,CA6ChD;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwBzF"}
|
package/dist/binary.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { constants as FS, createWriteStream } from 'node:fs';
|
|
4
|
+
import { access, chmod, mkdir, rename, rm } from 'node:fs/promises';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { Readable, Transform } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
const DEFAULT_BASE_URL = 'https://bitmot.com';
|
|
10
|
+
/**
|
|
11
|
+
* The published artifact name for a platform/arch, or null when unsupported. Mirrors the server's
|
|
12
|
+
* download whitelist (io/io1/gateway/download/DownloadRoutesImpl.kt) and the installer's `uname`
|
|
13
|
+
* mapping: macOS and Linux, on x86_64 or 64-bit ARM. Node's arch is translated to the server's
|
|
14
|
+
* spelling -- 'x64' -> 'x86_64' on both, but 64-bit ARM is 'arm64' on macOS and 'aarch64' on Linux.
|
|
15
|
+
*/
|
|
16
|
+
export function artifactFor(platform, arch) {
|
|
17
|
+
if (platform === 'darwin') {
|
|
18
|
+
if (arch === 'arm64')
|
|
19
|
+
return 'bitmot-macos-arm64';
|
|
20
|
+
if (arch === 'x64')
|
|
21
|
+
return 'bitmot-macos-x86_64';
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (platform === 'linux') {
|
|
25
|
+
if (arch === 'x64')
|
|
26
|
+
return 'bitmot-linux-x86_64';
|
|
27
|
+
if (arch === 'arm64')
|
|
28
|
+
return 'bitmot-linux-aarch64';
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function defaultCacheDir() {
|
|
34
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
35
|
+
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), '.cache'), 'bitmot');
|
|
36
|
+
}
|
|
37
|
+
async function exists(path) {
|
|
38
|
+
try {
|
|
39
|
+
await access(path, FS.F_OK);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** `bitmot version` -> "0.2.1", or null if the binary is missing or its output is unexpected. */
|
|
47
|
+
export function readVersion(binary) {
|
|
48
|
+
const result = spawnSync(binary, ['version'], { encoding: 'utf8' });
|
|
49
|
+
if (result.status !== 0 || typeof result.stdout !== 'string')
|
|
50
|
+
return null;
|
|
51
|
+
const match = /\bbitmot\s+(\d+\.\d+\.\d+)\b/.exec(result.stdout);
|
|
52
|
+
return match ? match[1] : null;
|
|
53
|
+
}
|
|
54
|
+
function onPath() {
|
|
55
|
+
// A bare-name spawn resolves via PATH; a clean `version` exit both locates the binary and proves it
|
|
56
|
+
// runs on this machine (arch, permissions), which a filesystem check alone would not.
|
|
57
|
+
const result = spawnSync('bitmot', ['version'], { stdio: 'ignore' });
|
|
58
|
+
return result.status === 0 && result.error === undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Ensures the current published binary is present in the cache and returns its path. The stable
|
|
62
|
+
* download URL 302-redirects to the content-addressed URL of whatever bytes are deployed now, so the
|
|
63
|
+
* followed response's checksum header names the current version: if a file for that hash is already
|
|
64
|
+
* cached the body is dropped undownloaded, otherwise it is streamed, verified against the advertised
|
|
65
|
+
* checksum, and atomically installed. This is what makes a plugin-managed CLI track production
|
|
66
|
+
* automatically -- so the CLI's own "you're out of date" nudge (suppressed anyway when its stderr is
|
|
67
|
+
* piped, as it is here) does not matter for the auto-managed case.
|
|
68
|
+
*/
|
|
69
|
+
export async function ensureDownloaded(baseUrl, artifact, cacheDir, log) {
|
|
70
|
+
const response = await fetch(`${baseUrl}/download/${artifact}`);
|
|
71
|
+
if (!response.ok || !response.body) {
|
|
72
|
+
await response.body?.cancel();
|
|
73
|
+
throw new Error(`download failed: ${baseUrl}/download/${artifact} returned ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
const advertised = response.headers.get('x-checksum-sha256')?.toLowerCase();
|
|
76
|
+
if (!advertised || !/^[0-9a-f]{64}$/.test(advertised)) {
|
|
77
|
+
await response.body.cancel();
|
|
78
|
+
throw new Error('download failed: server did not advertise a sha256 checksum');
|
|
79
|
+
}
|
|
80
|
+
const dest = join(cacheDir, `bitmot-${advertised.slice(0, 8)}`);
|
|
81
|
+
if (await exists(dest)) {
|
|
82
|
+
await response.body.cancel(); // already have these exact bytes; do not re-download the body
|
|
83
|
+
return { path: dest, downloaded: false };
|
|
84
|
+
}
|
|
85
|
+
await mkdir(cacheDir, { recursive: true });
|
|
86
|
+
const temp = `${dest}.${randomBytes(6).toString('hex')}.tmp`;
|
|
87
|
+
const hash = createHash('sha256');
|
|
88
|
+
const tee = new Transform({
|
|
89
|
+
transform(chunk, _encoding, callback) {
|
|
90
|
+
hash.update(chunk);
|
|
91
|
+
callback(null, chunk);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
log?.(`downloading ${artifact} (${advertised.slice(0, 8)})`);
|
|
95
|
+
try {
|
|
96
|
+
await pipeline(Readable.fromWeb(response.body), tee, createWriteStream(temp, { mode: 0o755 }));
|
|
97
|
+
const actual = hash.digest('hex');
|
|
98
|
+
if (actual !== advertised) {
|
|
99
|
+
throw new Error(`checksum mismatch: downloaded ${actual}, server advertised ${advertised}`);
|
|
100
|
+
}
|
|
101
|
+
await chmod(temp, 0o755);
|
|
102
|
+
await rename(temp, dest);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
await rm(temp, { force: true });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
return { path: dest, downloaded: true };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolves a bitmot binary to invoke: an explicit path wins; else a bitmot already on PATH is used
|
|
112
|
+
* (respecting a user- or installer-managed copy); else, when autoInstall is on, the current published
|
|
113
|
+
* binary is downloaded and cached. Throws with an actionable message when nothing is available and
|
|
114
|
+
* downloading is off or the platform has no published artifact.
|
|
115
|
+
*/
|
|
116
|
+
export async function resolveBitmot(options = {}) {
|
|
117
|
+
if (options.bitmotPath) {
|
|
118
|
+
return { path: options.bitmotPath, source: 'explicit', version: readVersion(options.bitmotPath) };
|
|
119
|
+
}
|
|
120
|
+
if (onPath()) {
|
|
121
|
+
return { path: 'bitmot', source: 'path', version: readVersion('bitmot') };
|
|
122
|
+
}
|
|
123
|
+
if (options.autoInstall === false) {
|
|
124
|
+
throw new Error('bitmot CLI not found on PATH and autoInstall is disabled. ' +
|
|
125
|
+
'Install it with: curl -fsSL https://bitmot.com/install | sh');
|
|
126
|
+
}
|
|
127
|
+
const artifact = artifactFor(process.platform, process.arch);
|
|
128
|
+
if (!artifact) {
|
|
129
|
+
throw new Error(`bitmot CLI not found and no published binary for ${process.platform}/${process.arch}. ` +
|
|
130
|
+
'Install it manually: curl -fsSL https://bitmot.com/install | sh');
|
|
131
|
+
}
|
|
132
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
133
|
+
const cacheDir = options.cacheDir ?? defaultCacheDir();
|
|
134
|
+
const { path, downloaded } = await ensureDownloaded(baseUrl, artifact, cacheDir, options.log);
|
|
135
|
+
return { path, source: downloaded ? 'download' : 'cache', version: readVersion(path) };
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=binary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary.js","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
export type { RelayInvocation } from './announce.js';
|
|
3
|
+
export type { ResolveOptions, ResolvedBinary } from './binary.js';
|
|
4
|
+
export interface Io1PluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Host(s) to claim, e.g. "🔥.io1.io" or "demo" (completed to demo.io1.io). Falls back to
|
|
7
|
+
* $IO1_DEV_HOST. With none set the plugin stays dormant: publishing every dev server by default
|
|
8
|
+
* would be a footgun, and a hardcoded shared host collides between teammates.
|
|
9
|
+
*/
|
|
10
|
+
host?: string | string[];
|
|
11
|
+
/** Allow-list emails; when set, visitors must verify one of them to reach the host. */
|
|
12
|
+
allowedEmails?: string[];
|
|
13
|
+
/** Account claim, when your identity has more than one membership. */
|
|
14
|
+
account?: string;
|
|
15
|
+
/** Gateway WebSocket URL override; defaults to the CLI's production gateway. */
|
|
16
|
+
gateway?: string;
|
|
17
|
+
/** Explicit path to a bitmot binary; skips PATH lookup and auto-download. */
|
|
18
|
+
bitmotPath?: string;
|
|
19
|
+
/** Auto-download the CLI when not found. Default true. */
|
|
20
|
+
autoInstall?: boolean;
|
|
21
|
+
/** Download origin for the CLI; mirrors the installer's BITMOT_BASE_URL. */
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
/** Extra `bitmot io1 relay` flags this plugin does not model, inserted verbatim before `--to`. */
|
|
24
|
+
extraArgs?: string[];
|
|
25
|
+
/** Master switch. Default: on, unless running under CI or no host is configured. */
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Vite plugin: while the dev (or preview) server runs, publish it to the internet over a
|
|
30
|
+
* `bitmot io1 relay` child process, proxying to the server's real bound port. The relay's lifetime is
|
|
31
|
+
* tied to the server -- spawned once it is listening, killed when it closes -- so a shared URL never
|
|
32
|
+
* outlives the session. The CLI is located on PATH or auto-downloaded (content-addressed, so it tracks
|
|
33
|
+
* the deployed build). Reconnects are the relay's own concern; a non-zero exit is a definitive refusal
|
|
34
|
+
* (bad auth, host held by another account) and is surfaced, not retried.
|
|
35
|
+
*/
|
|
36
|
+
export declare function io1(options?: Io1PluginOptions): Plugin;
|
|
37
|
+
export default io1;
|
|
38
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAgC,MAAM,MAAM,CAAA;AAIhE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjE,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,oFAAoF;IACpF,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAUD;;;;;;;GAOG;AACH,wBAAgB,GAAG,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA0F1D;AAyBD,eAAe,GAAG,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { buildRelayArgs, parseLiveUrl } from './announce.js';
|
|
3
|
+
import { resolveBitmot } from './binary.js';
|
|
4
|
+
function normalizeHosts(host) {
|
|
5
|
+
const raw = host ?? process.env.IO1_DEV_HOST;
|
|
6
|
+
if (!raw)
|
|
7
|
+
return [];
|
|
8
|
+
return (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry.length > 0);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Vite plugin: while the dev (or preview) server runs, publish it to the internet over a
|
|
12
|
+
* `bitmot io1 relay` child process, proxying to the server's real bound port. The relay's lifetime is
|
|
13
|
+
* tied to the server -- spawned once it is listening, killed when it closes -- so a shared URL never
|
|
14
|
+
* outlives the session. The CLI is located on PATH or auto-downloaded (content-addressed, so it tracks
|
|
15
|
+
* the deployed build). Reconnects are the relay's own concern; a non-zero exit is a definitive refusal
|
|
16
|
+
* (bad auth, host held by another account) and is surfaced, not retried.
|
|
17
|
+
*/
|
|
18
|
+
export function io1(options = {}) {
|
|
19
|
+
const hosts = normalizeHosts(options.host);
|
|
20
|
+
const enabled = options.enabled ?? (!process.env.CI && hosts.length > 0);
|
|
21
|
+
let child;
|
|
22
|
+
let stopping = false;
|
|
23
|
+
const supervise = (server) => {
|
|
24
|
+
if (!enabled)
|
|
25
|
+
return;
|
|
26
|
+
const logger = server.config.logger;
|
|
27
|
+
if (hosts.length === 0) {
|
|
28
|
+
logger.warn('[io1] no host configured (set `host` or $IO1_DEV_HOST); relay not started');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const httpServer = server.httpServer;
|
|
32
|
+
if (!httpServer) {
|
|
33
|
+
logger.warn('[io1] server has no httpServer (middleware mode?); relay not started');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const onExit = () => {
|
|
37
|
+
stopping = true;
|
|
38
|
+
child?.kill('SIGTERM');
|
|
39
|
+
child = undefined;
|
|
40
|
+
process.removeListener('exit', onExit); // avoid piling up a listener per dev-server restart
|
|
41
|
+
};
|
|
42
|
+
httpServer.once('listening', () => {
|
|
43
|
+
void start(server).catch((error) => {
|
|
44
|
+
logger.error(`[io1] ${error.message}`);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
httpServer.once('close', onExit);
|
|
48
|
+
process.once('exit', onExit);
|
|
49
|
+
};
|
|
50
|
+
const start = async (server) => {
|
|
51
|
+
const logger = server.config.logger;
|
|
52
|
+
const address = server.httpServer?.address();
|
|
53
|
+
if (!address || typeof address === 'string') {
|
|
54
|
+
logger.error('[io1] could not determine the dev server port; relay not started');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const binary = await resolveBitmot({
|
|
58
|
+
bitmotPath: options.bitmotPath,
|
|
59
|
+
autoInstall: options.autoInstall,
|
|
60
|
+
baseUrl: options.baseUrl,
|
|
61
|
+
log: (message) => logger.info(`[io1] ${message}`),
|
|
62
|
+
});
|
|
63
|
+
if (binary.version)
|
|
64
|
+
logger.info(`[io1] using bitmot ${binary.version} (${binary.source})`);
|
|
65
|
+
const args = buildRelayArgs({
|
|
66
|
+
hosts,
|
|
67
|
+
allowedEmails: options.allowedEmails,
|
|
68
|
+
account: options.account,
|
|
69
|
+
gateway: options.gateway,
|
|
70
|
+
extraArgs: options.extraArgs,
|
|
71
|
+
to: `http://127.0.0.1:${address.port}`,
|
|
72
|
+
});
|
|
73
|
+
if (stopping)
|
|
74
|
+
return; // the server closed while we were resolving the binary
|
|
75
|
+
const relay = spawn(binary.path, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
76
|
+
child = relay;
|
|
77
|
+
relay.on('error', (error) => {
|
|
78
|
+
logger.error(`[io1] failed to start relay: ${error.message}`);
|
|
79
|
+
});
|
|
80
|
+
relay.on('exit', (code, signal) => {
|
|
81
|
+
if (child === relay)
|
|
82
|
+
child = undefined;
|
|
83
|
+
if (stopping || signal)
|
|
84
|
+
return;
|
|
85
|
+
if (code && code !== 0) {
|
|
86
|
+
logger.error(`[io1] relay exited (code ${code}). If this is an auth error, run: bitmot io1 authorize`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
forwardRelayOutput(relay, server);
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
name: 'vite-plugin-io1',
|
|
93
|
+
configureServer(server) {
|
|
94
|
+
supervise(server);
|
|
95
|
+
},
|
|
96
|
+
configurePreviewServer(server) {
|
|
97
|
+
supervise(server);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Forwards the relay's stderr to Vite's logger line by line: a per-host "live" announcement becomes a
|
|
103
|
+
* banner-style URL line, anything else (the link-established line, a pending-access-requests notice,
|
|
104
|
+
* a diagnostic) is passed through verbatim under an [io1] tag. The relay writes whole lines, but a
|
|
105
|
+
* chunk may split one, so partial lines are buffered until their newline arrives.
|
|
106
|
+
*/
|
|
107
|
+
function forwardRelayOutput(relay, server) {
|
|
108
|
+
const logger = server.config.logger;
|
|
109
|
+
let buffer = '';
|
|
110
|
+
relay.stderr?.setEncoding('utf8');
|
|
111
|
+
relay.stderr?.on('data', (chunk) => {
|
|
112
|
+
buffer += chunk;
|
|
113
|
+
for (let newline = buffer.indexOf('\n'); newline >= 0; newline = buffer.indexOf('\n')) {
|
|
114
|
+
const line = buffer.slice(0, newline).replace(/\r$/, '');
|
|
115
|
+
buffer = buffer.slice(newline + 1);
|
|
116
|
+
if (line.trim().length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
const url = parseLiveUrl(line);
|
|
119
|
+
if (url)
|
|
120
|
+
logger.info(` ➜ io1: ${url}`, { clear: false, timestamp: false });
|
|
121
|
+
else
|
|
122
|
+
logger.info(`[io1] ${line}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export default io1;
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-io1",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Publish your Vite dev server to the public internet over a bitmot io1 relay, with the CLI auto-managed.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vite",
|
|
7
|
+
"vite-plugin",
|
|
8
|
+
"tunnel",
|
|
9
|
+
"bitmot",
|
|
10
|
+
"io1",
|
|
11
|
+
"relay",
|
|
12
|
+
"https",
|
|
13
|
+
"share",
|
|
14
|
+
"dev-server"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Bitmot <shea@bitmot.com>",
|
|
19
|
+
"homepage": "https://io1.io/vite",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://gitlab.com/bitmot/vite-plugin-io1.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://gitlab.com/bitmot/vite-plugin-io1/-/issues"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"vite": ">=5"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.10.0",
|
|
52
|
+
"typescript": "^5.7.0",
|
|
53
|
+
"vite": "^6.0.0",
|
|
54
|
+
"vitest": "^2.1.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/announce.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure argument-building and output-parsing for the bitmot io1 relay. Kept free of I/O so the wire
|
|
3
|
+
* contract with the CLI -- which flags it takes, and how it announces a live host -- is unit-tested
|
|
4
|
+
* in isolation and lives in one place.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface RelayInvocation {
|
|
8
|
+
/** Hosts to claim, one `--host` each (the CLI appends across repeats). At least one. */
|
|
9
|
+
readonly hosts: readonly string[]
|
|
10
|
+
/** Allow-list emails; the CLI takes a single comma-separated `--allowed-emails`, not repeats. */
|
|
11
|
+
readonly allowedEmails?: readonly string[]
|
|
12
|
+
/** Account claim, when the identity has more than one membership. */
|
|
13
|
+
readonly account?: string
|
|
14
|
+
/** Gateway WebSocket URL; omitted to use the CLI's baked-in production default. */
|
|
15
|
+
readonly gateway?: string
|
|
16
|
+
/** Host header presented to the gateway; omitted to derive it from the gateway URL. */
|
|
17
|
+
readonly gatewayHost?: string
|
|
18
|
+
/** Escape hatch for flags this plugin does not model (e.g. `--credential`). Inserted verbatim. */
|
|
19
|
+
readonly extraArgs?: readonly string[]
|
|
20
|
+
/** The local dev server to proxy to, e.g. `http://127.0.0.1:5173`. */
|
|
21
|
+
readonly to: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The full `bitmot io1 relay ...` argv (without the leading program name). `--allowed-emails` is
|
|
26
|
+
* comma-joined into a single flag because the CLI parses it as one comma-separated value; emitting
|
|
27
|
+
* repeats would make the native CLI keep only the last. `--to` goes last so the line reads as a claim
|
|
28
|
+
* of hosts proxied to a target.
|
|
29
|
+
*/
|
|
30
|
+
export function buildRelayArgs(invocation: RelayInvocation): string[] {
|
|
31
|
+
const args = ['io1', 'relay']
|
|
32
|
+
for (const host of invocation.hosts) args.push('--host', host)
|
|
33
|
+
const emails = invocation.allowedEmails ?? []
|
|
34
|
+
if (emails.length > 0) args.push('--allowed-emails', emails.join(','))
|
|
35
|
+
if (invocation.account) args.push('--account', invocation.account)
|
|
36
|
+
if (invocation.gateway) args.push('--gateway', invocation.gateway)
|
|
37
|
+
if (invocation.gatewayHost) args.push('--gateway-host', invocation.gatewayHost)
|
|
38
|
+
if (invocation.extraArgs) args.push(...invocation.extraArgs)
|
|
39
|
+
args.push('--to', invocation.to)
|
|
40
|
+
return args
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The public URL from a relay's per-host announcement line, or null for any other line. The relay
|
|
45
|
+
* prints one `<arrow> <url> live` line per claimed host once its link is up (uncolored when its
|
|
46
|
+
* stderr is piped, as it is under this plugin). Tolerant of the arrow glyph and surrounding spaces so
|
|
47
|
+
* both the native and JVM CLIs -- and any cached older build -- parse the same; the trailing `live`
|
|
48
|
+
* badge is what distinguishes it from the pending-requests and update-nudge lines, which also carry
|
|
49
|
+
* URLs but must not be mistaken for a live host.
|
|
50
|
+
*/
|
|
51
|
+
export function parseLiveUrl(line: string): string | null {
|
|
52
|
+
const match = /(https?:\/\/\S+)\s+live\s*$/u.exec(line)
|
|
53
|
+
return match ? match[1]! : null
|
|
54
|
+
}
|
package/src/binary.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
import { constants as FS, createWriteStream } from 'node:fs'
|
|
4
|
+
import { access, chmod, mkdir, rename, rm } from 'node:fs/promises'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { Readable, Transform } from 'node:stream'
|
|
8
|
+
import { pipeline } from 'node:stream/promises'
|
|
9
|
+
import type { ReadableStream as WebReadableStream } from 'node:stream/web'
|
|
10
|
+
|
|
11
|
+
export interface ResolveOptions {
|
|
12
|
+
/** Explicit path to a bitmot binary; when set it is used as-is (no PATH search, no download). */
|
|
13
|
+
readonly bitmotPath?: string
|
|
14
|
+
/** Download the CLI when not otherwise found. Default true; false makes a miss a hard error. */
|
|
15
|
+
readonly autoInstall?: boolean
|
|
16
|
+
/** Download origin; mirrors the installer's BITMOT_BASE_URL. Default https://bitmot.com. */
|
|
17
|
+
readonly baseUrl?: string
|
|
18
|
+
/** Where to cache downloaded binaries. Default ~/.cache/bitmot (or $XDG_CACHE_HOME/bitmot). */
|
|
19
|
+
readonly cacheDir?: string
|
|
20
|
+
/** Sink for human-facing progress ("downloading ..."). */
|
|
21
|
+
readonly log?: (message: string) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResolvedBinary {
|
|
25
|
+
/** Path to invoke. */
|
|
26
|
+
readonly path: string
|
|
27
|
+
/** How it was found, for logging. */
|
|
28
|
+
readonly source: 'explicit' | 'path' | 'download' | 'cache'
|
|
29
|
+
/** `bitmot version`'s reported semver (e.g. "0.2.1"), or null if it could not be read. */
|
|
30
|
+
readonly version: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_BASE_URL = 'https://bitmot.com'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The published artifact name for a platform/arch, or null when unsupported. Mirrors the server's
|
|
37
|
+
* download whitelist (io/io1/gateway/download/DownloadRoutesImpl.kt) and the installer's `uname`
|
|
38
|
+
* mapping: macOS and Linux, on x86_64 or 64-bit ARM. Node's arch is translated to the server's
|
|
39
|
+
* spelling -- 'x64' -> 'x86_64' on both, but 64-bit ARM is 'arm64' on macOS and 'aarch64' on Linux.
|
|
40
|
+
*/
|
|
41
|
+
export function artifactFor(platform: NodeJS.Platform, arch: string): string | null {
|
|
42
|
+
if (platform === 'darwin') {
|
|
43
|
+
if (arch === 'arm64') return 'bitmot-macos-arm64'
|
|
44
|
+
if (arch === 'x64') return 'bitmot-macos-x86_64'
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
if (platform === 'linux') {
|
|
48
|
+
if (arch === 'x64') return 'bitmot-linux-x86_64'
|
|
49
|
+
if (arch === 'arm64') return 'bitmot-linux-aarch64'
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultCacheDir(): string {
|
|
56
|
+
const xdg = process.env.XDG_CACHE_HOME
|
|
57
|
+
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), '.cache'), 'bitmot')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function exists(path: string): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
await access(path, FS.F_OK)
|
|
63
|
+
return true
|
|
64
|
+
} catch {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `bitmot version` -> "0.2.1", or null if the binary is missing or its output is unexpected. */
|
|
70
|
+
export function readVersion(binary: string): string | null {
|
|
71
|
+
const result = spawnSync(binary, ['version'], { encoding: 'utf8' })
|
|
72
|
+
if (result.status !== 0 || typeof result.stdout !== 'string') return null
|
|
73
|
+
const match = /\bbitmot\s+(\d+\.\d+\.\d+)\b/.exec(result.stdout)
|
|
74
|
+
return match ? match[1]! : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onPath(): boolean {
|
|
78
|
+
// A bare-name spawn resolves via PATH; a clean `version` exit both locates the binary and proves it
|
|
79
|
+
// runs on this machine (arch, permissions), which a filesystem check alone would not.
|
|
80
|
+
const result = spawnSync('bitmot', ['version'], { stdio: 'ignore' })
|
|
81
|
+
return result.status === 0 && result.error === undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Ensures the current published binary is present in the cache and returns its path. The stable
|
|
86
|
+
* download URL 302-redirects to the content-addressed URL of whatever bytes are deployed now, so the
|
|
87
|
+
* followed response's checksum header names the current version: if a file for that hash is already
|
|
88
|
+
* cached the body is dropped undownloaded, otherwise it is streamed, verified against the advertised
|
|
89
|
+
* checksum, and atomically installed. This is what makes a plugin-managed CLI track production
|
|
90
|
+
* automatically -- so the CLI's own "you're out of date" nudge (suppressed anyway when its stderr is
|
|
91
|
+
* piped, as it is here) does not matter for the auto-managed case.
|
|
92
|
+
*/
|
|
93
|
+
export async function ensureDownloaded(
|
|
94
|
+
baseUrl: string,
|
|
95
|
+
artifact: string,
|
|
96
|
+
cacheDir: string,
|
|
97
|
+
log?: (message: string) => void,
|
|
98
|
+
): Promise<{ path: string; downloaded: boolean }> {
|
|
99
|
+
const response = await fetch(`${baseUrl}/download/${artifact}`)
|
|
100
|
+
if (!response.ok || !response.body) {
|
|
101
|
+
await response.body?.cancel()
|
|
102
|
+
throw new Error(`download failed: ${baseUrl}/download/${artifact} returned ${response.status}`)
|
|
103
|
+
}
|
|
104
|
+
const advertised = response.headers.get('x-checksum-sha256')?.toLowerCase()
|
|
105
|
+
if (!advertised || !/^[0-9a-f]{64}$/.test(advertised)) {
|
|
106
|
+
await response.body.cancel()
|
|
107
|
+
throw new Error('download failed: server did not advertise a sha256 checksum')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const dest = join(cacheDir, `bitmot-${advertised.slice(0, 8)}`)
|
|
111
|
+
if (await exists(dest)) {
|
|
112
|
+
await response.body.cancel() // already have these exact bytes; do not re-download the body
|
|
113
|
+
return { path: dest, downloaded: false }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await mkdir(cacheDir, { recursive: true })
|
|
117
|
+
const temp = `${dest}.${randomBytes(6).toString('hex')}.tmp`
|
|
118
|
+
const hash = createHash('sha256')
|
|
119
|
+
const tee = new Transform({
|
|
120
|
+
transform(chunk, _encoding, callback) {
|
|
121
|
+
hash.update(chunk)
|
|
122
|
+
callback(null, chunk)
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
log?.(`downloading ${artifact} (${advertised.slice(0, 8)})`)
|
|
126
|
+
try {
|
|
127
|
+
await pipeline(
|
|
128
|
+
Readable.fromWeb(response.body as unknown as WebReadableStream<Uint8Array>),
|
|
129
|
+
tee,
|
|
130
|
+
createWriteStream(temp, { mode: 0o755 }),
|
|
131
|
+
)
|
|
132
|
+
const actual = hash.digest('hex')
|
|
133
|
+
if (actual !== advertised) {
|
|
134
|
+
throw new Error(`checksum mismatch: downloaded ${actual}, server advertised ${advertised}`)
|
|
135
|
+
}
|
|
136
|
+
await chmod(temp, 0o755)
|
|
137
|
+
await rename(temp, dest)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await rm(temp, { force: true })
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
return { path: dest, downloaded: true }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolves a bitmot binary to invoke: an explicit path wins; else a bitmot already on PATH is used
|
|
147
|
+
* (respecting a user- or installer-managed copy); else, when autoInstall is on, the current published
|
|
148
|
+
* binary is downloaded and cached. Throws with an actionable message when nothing is available and
|
|
149
|
+
* downloading is off or the platform has no published artifact.
|
|
150
|
+
*/
|
|
151
|
+
export async function resolveBitmot(options: ResolveOptions = {}): Promise<ResolvedBinary> {
|
|
152
|
+
if (options.bitmotPath) {
|
|
153
|
+
return { path: options.bitmotPath, source: 'explicit', version: readVersion(options.bitmotPath) }
|
|
154
|
+
}
|
|
155
|
+
if (onPath()) {
|
|
156
|
+
return { path: 'bitmot', source: 'path', version: readVersion('bitmot') }
|
|
157
|
+
}
|
|
158
|
+
if (options.autoInstall === false) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'bitmot CLI not found on PATH and autoInstall is disabled. ' +
|
|
161
|
+
'Install it with: curl -fsSL https://bitmot.com/install | sh',
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
const artifact = artifactFor(process.platform, process.arch)
|
|
165
|
+
if (!artifact) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`bitmot CLI not found and no published binary for ${process.platform}/${process.arch}. ` +
|
|
168
|
+
'Install it manually: curl -fsSL https://bitmot.com/install | sh',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL
|
|
172
|
+
const cacheDir = options.cacheDir ?? defaultCacheDir()
|
|
173
|
+
const { path, downloaded } = await ensureDownloaded(baseUrl, artifact, cacheDir, options.log)
|
|
174
|
+
return { path, source: downloaded ? 'download' : 'cache', version: readVersion(path) }
|
|
175
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
2
|
+
import type { Plugin, PreviewServer, ViteDevServer } from 'vite'
|
|
3
|
+
import { buildRelayArgs, parseLiveUrl } from './announce.js'
|
|
4
|
+
import { resolveBitmot } from './binary.js'
|
|
5
|
+
|
|
6
|
+
export type { RelayInvocation } from './announce.js'
|
|
7
|
+
export type { ResolveOptions, ResolvedBinary } from './binary.js'
|
|
8
|
+
|
|
9
|
+
export interface Io1PluginOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Host(s) to claim, e.g. "🔥.io1.io" or "demo" (completed to demo.io1.io). Falls back to
|
|
12
|
+
* $IO1_DEV_HOST. With none set the plugin stays dormant: publishing every dev server by default
|
|
13
|
+
* would be a footgun, and a hardcoded shared host collides between teammates.
|
|
14
|
+
*/
|
|
15
|
+
host?: string | string[]
|
|
16
|
+
/** Allow-list emails; when set, visitors must verify one of them to reach the host. */
|
|
17
|
+
allowedEmails?: string[]
|
|
18
|
+
/** Account claim, when your identity has more than one membership. */
|
|
19
|
+
account?: string
|
|
20
|
+
/** Gateway WebSocket URL override; defaults to the CLI's production gateway. */
|
|
21
|
+
gateway?: string
|
|
22
|
+
/** Explicit path to a bitmot binary; skips PATH lookup and auto-download. */
|
|
23
|
+
bitmotPath?: string
|
|
24
|
+
/** Auto-download the CLI when not found. Default true. */
|
|
25
|
+
autoInstall?: boolean
|
|
26
|
+
/** Download origin for the CLI; mirrors the installer's BITMOT_BASE_URL. */
|
|
27
|
+
baseUrl?: string
|
|
28
|
+
/** Extra `bitmot io1 relay` flags this plugin does not model, inserted verbatim before `--to`. */
|
|
29
|
+
extraArgs?: string[]
|
|
30
|
+
/** Master switch. Default: on, unless running under CI or no host is configured. */
|
|
31
|
+
enabled?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeHosts(host: Io1PluginOptions['host']): string[] {
|
|
35
|
+
const raw = host ?? process.env.IO1_DEV_HOST
|
|
36
|
+
if (!raw) return []
|
|
37
|
+
return (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry.length > 0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type ServeServer = ViteDevServer | PreviewServer
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Vite plugin: while the dev (or preview) server runs, publish it to the internet over a
|
|
44
|
+
* `bitmot io1 relay` child process, proxying to the server's real bound port. The relay's lifetime is
|
|
45
|
+
* tied to the server -- spawned once it is listening, killed when it closes -- so a shared URL never
|
|
46
|
+
* outlives the session. The CLI is located on PATH or auto-downloaded (content-addressed, so it tracks
|
|
47
|
+
* the deployed build). Reconnects are the relay's own concern; a non-zero exit is a definitive refusal
|
|
48
|
+
* (bad auth, host held by another account) and is surfaced, not retried.
|
|
49
|
+
*/
|
|
50
|
+
export function io1(options: Io1PluginOptions = {}): Plugin {
|
|
51
|
+
const hosts = normalizeHosts(options.host)
|
|
52
|
+
const enabled = options.enabled ?? (!process.env.CI && hosts.length > 0)
|
|
53
|
+
|
|
54
|
+
let child: ChildProcess | undefined
|
|
55
|
+
let stopping = false
|
|
56
|
+
|
|
57
|
+
const supervise = (server: ServeServer): void => {
|
|
58
|
+
if (!enabled) return
|
|
59
|
+
const logger = server.config.logger
|
|
60
|
+
if (hosts.length === 0) {
|
|
61
|
+
logger.warn('[io1] no host configured (set `host` or $IO1_DEV_HOST); relay not started')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
const httpServer = server.httpServer
|
|
65
|
+
if (!httpServer) {
|
|
66
|
+
logger.warn('[io1] server has no httpServer (middleware mode?); relay not started')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const onExit = (): void => {
|
|
71
|
+
stopping = true
|
|
72
|
+
child?.kill('SIGTERM')
|
|
73
|
+
child = undefined
|
|
74
|
+
process.removeListener('exit', onExit) // avoid piling up a listener per dev-server restart
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
httpServer.once('listening', () => {
|
|
78
|
+
void start(server).catch((error: unknown) => {
|
|
79
|
+
logger.error(`[io1] ${(error as Error).message}`)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
httpServer.once('close', onExit)
|
|
83
|
+
process.once('exit', onExit)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const start = async (server: ServeServer): Promise<void> => {
|
|
87
|
+
const logger = server.config.logger
|
|
88
|
+
const address = server.httpServer?.address()
|
|
89
|
+
if (!address || typeof address === 'string') {
|
|
90
|
+
logger.error('[io1] could not determine the dev server port; relay not started')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const binary = await resolveBitmot({
|
|
95
|
+
bitmotPath: options.bitmotPath,
|
|
96
|
+
autoInstall: options.autoInstall,
|
|
97
|
+
baseUrl: options.baseUrl,
|
|
98
|
+
log: (message) => logger.info(`[io1] ${message}`),
|
|
99
|
+
})
|
|
100
|
+
if (binary.version) logger.info(`[io1] using bitmot ${binary.version} (${binary.source})`)
|
|
101
|
+
|
|
102
|
+
const args = buildRelayArgs({
|
|
103
|
+
hosts,
|
|
104
|
+
allowedEmails: options.allowedEmails,
|
|
105
|
+
account: options.account,
|
|
106
|
+
gateway: options.gateway,
|
|
107
|
+
extraArgs: options.extraArgs,
|
|
108
|
+
to: `http://127.0.0.1:${address.port}`,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (stopping) return // the server closed while we were resolving the binary
|
|
112
|
+
const relay = spawn(binary.path, args, { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
113
|
+
child = relay
|
|
114
|
+
|
|
115
|
+
relay.on('error', (error) => {
|
|
116
|
+
logger.error(`[io1] failed to start relay: ${error.message}`)
|
|
117
|
+
})
|
|
118
|
+
relay.on('exit', (code, signal) => {
|
|
119
|
+
if (child === relay) child = undefined
|
|
120
|
+
if (stopping || signal) return
|
|
121
|
+
if (code && code !== 0) {
|
|
122
|
+
logger.error(
|
|
123
|
+
`[io1] relay exited (code ${code}). If this is an auth error, run: bitmot io1 authorize`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
forwardRelayOutput(relay, server)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
name: 'vite-plugin-io1',
|
|
133
|
+
configureServer(server) {
|
|
134
|
+
supervise(server)
|
|
135
|
+
},
|
|
136
|
+
configurePreviewServer(server) {
|
|
137
|
+
supervise(server)
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Forwards the relay's stderr to Vite's logger line by line: a per-host "live" announcement becomes a
|
|
144
|
+
* banner-style URL line, anything else (the link-established line, a pending-access-requests notice,
|
|
145
|
+
* a diagnostic) is passed through verbatim under an [io1] tag. The relay writes whole lines, but a
|
|
146
|
+
* chunk may split one, so partial lines are buffered until their newline arrives.
|
|
147
|
+
*/
|
|
148
|
+
function forwardRelayOutput(relay: ChildProcess, server: ServeServer): void {
|
|
149
|
+
const logger = server.config.logger
|
|
150
|
+
let buffer = ''
|
|
151
|
+
relay.stderr?.setEncoding('utf8')
|
|
152
|
+
relay.stderr?.on('data', (chunk: string) => {
|
|
153
|
+
buffer += chunk
|
|
154
|
+
for (let newline = buffer.indexOf('\n'); newline >= 0; newline = buffer.indexOf('\n')) {
|
|
155
|
+
const line = buffer.slice(0, newline).replace(/\r$/, '')
|
|
156
|
+
buffer = buffer.slice(newline + 1)
|
|
157
|
+
if (line.trim().length === 0) continue
|
|
158
|
+
const url = parseLiveUrl(line)
|
|
159
|
+
if (url) logger.info(` ➜ io1: ${url}`, { clear: false, timestamp: false })
|
|
160
|
+
else logger.info(`[io1] ${line}`)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default io1
|