@tokamakdev/tok 0.1.0-beta.37 → 0.1.1-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -8
- package/bin/tok.js +9 -2
- package/dist/dev-socket.d.mts +23 -0
- package/dist/dev-socket.mjs +241 -0
- package/dist/events.d.mts +16 -0
- package/dist/events.mjs +26 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.mjs +1 -0
- package/dist/native.d.mts +6 -0
- package/dist/native.mjs +73 -0
- package/dist/vite.d.mts +17 -0
- package/dist/vite.mjs +213 -0
- package/package.json +58 -6
- package/platform.js +17 -1
package/README.md
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
# @tokamakdev/tok
|
|
2
2
|
|
|
3
|
-
The tokamak CLI,
|
|
4
|
-
workspace Cargo version, with a `-beta.N` suffix
|
|
3
|
+
The tokamak CLI, Vite plugin and app API, published from the tokamak
|
|
4
|
+
workspace. The version is the workspace Cargo version, with a `-beta.N` suffix
|
|
5
|
+
for automated pre-releases.
|
|
5
6
|
|
|
6
7
|
```sh
|
|
7
|
-
npm install -
|
|
8
|
-
tok targets
|
|
8
|
+
npm install --save-dev @tokamakdev/tok
|
|
9
|
+
npx tok targets
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
| Entry | Contents |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@tokamakdev/tok` | The `Config` type of the Vite plugin's options |
|
|
15
|
+
| `@tokamakdev/tok/events` | The app's events, `onStart`, `onResume` and `onSuspend`, and `getLifecycleStage()` |
|
|
16
|
+
| `@tokamakdev/tok/vite` | The Vite plugin, `tokamak()` |
|
|
17
|
+
| `tok` | The CLI |
|
|
18
|
+
|
|
19
|
+
The Vite plugin goes next to Cloudflare's, and its options are the app's
|
|
20
|
+
tokamak configuration:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
24
|
+
import { tokamak } from "@tokamakdev/tok/vite";
|
|
25
|
+
import { defineConfig } from "vite";
|
|
26
|
+
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
plugins: [cloudflare(), tokamak({ name: "My App", version: "1.0.0" })],
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
It does nothing unless `tok` runs the build or development command. Then it
|
|
33
|
+
makes the entry Worker import `src/tokamak.ts` and export the entrypoint the
|
|
34
|
+
runtime delivers events through. In development it delivers the device's
|
|
35
|
+
events to the development Worker through Cloudflare's dev registry, and passes
|
|
36
|
+
the development Worker's plugin calls to the device.
|
|
37
|
+
|
|
38
|
+
The package installs the `tok` binary for the current machine through an
|
|
12
39
|
optional dependency (`@tokamakdev/tok-darwin-arm64`, `-darwin-x64`,
|
|
13
|
-
`-linux-x64`, or `-win32-x64`)
|
|
14
|
-
|
|
15
|
-
|
|
40
|
+
`-linux-x64`, or `-win32-x64`), and the platform packs the machine can build
|
|
41
|
+
as `@tokamakdev/platform-<target>` optional dependencies:
|
|
42
|
+
|
|
43
|
+
| Machine | Platform packs |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| macOS, Apple silicon | `android-arm64`, `ios-arm64`, `ios-simulator-arm64`, `macos-arm64` |
|
|
46
|
+
| macOS, Intel | `android-arm64`, `ios-arm64`, `ios-simulator-x64`, `macos-x64` |
|
|
47
|
+
| Windows x64 | `android-arm64`, `windows-x64` |
|
|
48
|
+
| Linux x64 | `android-arm64` |
|
|
49
|
+
|
|
50
|
+
The launcher passes the installed platform packs to `tok` in
|
|
51
|
+
`TOKAMAK_PLATFORM_PACK_PATH`, unless that variable is already set.
|
package/bin/tok.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { spawnSync } = require("node:child_process");
|
|
5
|
-
const
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { binaryPath, platformPackRoots } = require("../platform.js");
|
|
6
7
|
|
|
7
8
|
let binary;
|
|
8
9
|
try {
|
|
@@ -16,7 +17,13 @@ try {
|
|
|
16
17
|
// so the launcher only waits for it.
|
|
17
18
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => {});
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
const env = { ...process.env };
|
|
21
|
+
if (!env.TOKAMAK_PLATFORM_PACK_PATH) {
|
|
22
|
+
const roots = platformPackRoots(require.resolve);
|
|
23
|
+
if (roots.length > 0) env.TOKAMAK_PLATFORM_PACK_PATH = roots.join(path.delimiter);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit", env });
|
|
20
27
|
if (result.error) {
|
|
21
28
|
console.error(`could not run ${binary}: ${result.error.message}`);
|
|
22
29
|
process.exit(1);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The header that carries the device's session token, which `tok dev`'s relay forwards. */
|
|
2
|
+
export declare const SESSION_HEADER = "x-tokamak-session";
|
|
3
|
+
/** A plugin's result, as the page's native transport receives it. */
|
|
4
|
+
export interface PluginResult {
|
|
5
|
+
value?: unknown;
|
|
6
|
+
error?: {
|
|
7
|
+
name: string;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
done: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** The loopback server the device's dev WebSocket reaches through `tok dev`'s relay. */
|
|
13
|
+
export interface DevSocket {
|
|
14
|
+
readonly port: number;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Listens on a loopback port, with `token`, for the device's dev WebSocket and the development
|
|
19
|
+
* Worker's plugin calls. Delivers each event the device sends to `TokamakEvents` in the
|
|
20
|
+
* development Worker `workerName`, and passes each plugin call the Worker posts to `/call` or
|
|
21
|
+
* `/subscribe` to the device.
|
|
22
|
+
*/
|
|
23
|
+
export declare function openDevSocket(workerName: string | undefined, token: string | undefined): Promise<DevSocket>;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
/** The header that carries the device's session token, which `tok dev`'s relay forwards. */
|
|
6
|
+
export const SESSION_HEADER = "x-tokamak-session";
|
|
7
|
+
/** How long a plugin call waits for the device to connect. */
|
|
8
|
+
const CONNECT_TIMEOUT = 10_000;
|
|
9
|
+
/** Calls `TokamakEvents` in the Worker its `TOKAMAK` service binding names, through the dev registry. */
|
|
10
|
+
const CALLER = `export default {
|
|
11
|
+
async fetch(request, env) {
|
|
12
|
+
const { name, event } = await request.json();
|
|
13
|
+
try {
|
|
14
|
+
const { reply } = await env.TOKAMAK.dispatch(name, event);
|
|
15
|
+
return Response.json({ reply: reply ?? null });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return new Response(error?.stack ?? String(error), { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};`;
|
|
21
|
+
/**
|
|
22
|
+
* Listens on a loopback port, with `token`, for the device's dev WebSocket and the development
|
|
23
|
+
* Worker's plugin calls. Delivers each event the device sends to `TokamakEvents` in the
|
|
24
|
+
* development Worker `workerName`, and passes each plugin call the Worker posts to `/call` or
|
|
25
|
+
* `/subscribe` to the device.
|
|
26
|
+
*/
|
|
27
|
+
export async function openDevSocket(workerName, token) {
|
|
28
|
+
const worker = new DevWorker(workerName);
|
|
29
|
+
const device = new Device();
|
|
30
|
+
const server = http.createServer((request, response) => {
|
|
31
|
+
if (!token || request.headers[SESSION_HEADER] !== token) {
|
|
32
|
+
response.writeHead(401).end();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (request.method === "POST" && (request.url === "/call" || request.url === "/subscribe")) {
|
|
36
|
+
void forward(device, request, response);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
response.writeHead(request.method === "GET" ? 426 : 404).end();
|
|
40
|
+
});
|
|
41
|
+
const sockets = new WebSocketServer({ noServer: true });
|
|
42
|
+
server.on("upgrade", (request, socket, head) => {
|
|
43
|
+
if (!token || request.headers[SESSION_HEADER] !== token) {
|
|
44
|
+
socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
sockets.handleUpgrade(request, socket, head, (websocket) => serve(websocket, worker, device));
|
|
48
|
+
});
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
server.once("error", reject);
|
|
51
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
port: server.address().port,
|
|
55
|
+
async close() {
|
|
56
|
+
for (const websocket of sockets.clients)
|
|
57
|
+
websocket.terminate();
|
|
58
|
+
server.closeAllConnections();
|
|
59
|
+
server.close();
|
|
60
|
+
await worker.dispose();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function serve(websocket, worker, device) {
|
|
65
|
+
device.connect(websocket);
|
|
66
|
+
// The device opens another dev WebSocket after this one fails.
|
|
67
|
+
websocket.on("error", () => websocket.terminate());
|
|
68
|
+
websocket.on("message", (data) => {
|
|
69
|
+
const message = parseMessage(String(data));
|
|
70
|
+
if (message?.type === "result") {
|
|
71
|
+
device.receive(message.id, message.result);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (message?.type !== "event")
|
|
75
|
+
return;
|
|
76
|
+
worker.dispatch(message.name, message.event).then((reply) => websocket.send(JSON.stringify({ type: "reply", id: message.id, reply })), (error) => websocket.send(JSON.stringify({ type: "error", id: message.id, message: String(error) })));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Passes the plugin call `request` carries to the device. A call's response is its result; a
|
|
81
|
+
* subscription's streams each result as a line of JSON until one is done, and closing it ends the
|
|
82
|
+
* subscription.
|
|
83
|
+
*/
|
|
84
|
+
async function forward(device, request, response) {
|
|
85
|
+
let call;
|
|
86
|
+
try {
|
|
87
|
+
call = JSON.parse(await readBody(request));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
response.writeHead(400).end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
94
|
+
if (request.url === "/call") {
|
|
95
|
+
response.end(JSON.stringify(await new Promise((resolve) => device.send("call", call, resolve))));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
response.flushHeaders();
|
|
99
|
+
const cancel = device.send("subscribe", call, (result) => {
|
|
100
|
+
response.write(`${JSON.stringify(result)}\n`);
|
|
101
|
+
if (result.done)
|
|
102
|
+
response.end();
|
|
103
|
+
});
|
|
104
|
+
response.once("close", () => {
|
|
105
|
+
if (!response.writableEnded)
|
|
106
|
+
cancel();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function readBody(request) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const chunks = [];
|
|
112
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
113
|
+
request.once("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
114
|
+
request.once("error", reject);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function parseMessage(text) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(text);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** A failed result, which ends a subscription. */
|
|
126
|
+
function failure(name, message) {
|
|
127
|
+
return { error: { name, message }, done: true };
|
|
128
|
+
}
|
|
129
|
+
/** The device, through its latest dev WebSocket, and the plugin calls waiting for its results. */
|
|
130
|
+
class Device {
|
|
131
|
+
#socket;
|
|
132
|
+
#connected = [];
|
|
133
|
+
#nextId = 1;
|
|
134
|
+
#pending = new Map();
|
|
135
|
+
connect(socket) {
|
|
136
|
+
this.#socket = socket;
|
|
137
|
+
for (const connected of this.#connected.splice(0))
|
|
138
|
+
connected();
|
|
139
|
+
socket.once("close", () => {
|
|
140
|
+
if (this.#socket === socket)
|
|
141
|
+
this.#socket = undefined;
|
|
142
|
+
for (const [id, pending] of this.#pending) {
|
|
143
|
+
if (pending.socket !== socket)
|
|
144
|
+
continue;
|
|
145
|
+
this.#pending.delete(id);
|
|
146
|
+
pending.receive(failure("NetworkError", "The device disconnected"));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
receive(id, result) {
|
|
151
|
+
const pending = this.#pending.get(id);
|
|
152
|
+
if (!pending)
|
|
153
|
+
return;
|
|
154
|
+
if (result.done || pending.type === "call")
|
|
155
|
+
this.#pending.delete(id);
|
|
156
|
+
pending.receive(result);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Sends a call or subscription once the device is connected, waiting up to 10 seconds for it,
|
|
160
|
+
* and passes each of its results to `receive`. Returns the function that unsubscribes.
|
|
161
|
+
*/
|
|
162
|
+
send(type, request, receive) {
|
|
163
|
+
const id = this.#nextId++;
|
|
164
|
+
let cancelled = false;
|
|
165
|
+
void this.#connection().then((socket) => {
|
|
166
|
+
if (cancelled)
|
|
167
|
+
return;
|
|
168
|
+
if (!socket) {
|
|
169
|
+
receive(failure("NetworkError", "The device is not connected"));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.#pending.set(id, { type, socket, receive });
|
|
173
|
+
socket.send(JSON.stringify({ type, id, ...request }));
|
|
174
|
+
});
|
|
175
|
+
return () => {
|
|
176
|
+
cancelled = true;
|
|
177
|
+
const pending = this.#pending.get(id);
|
|
178
|
+
if (!pending)
|
|
179
|
+
return;
|
|
180
|
+
this.#pending.delete(id);
|
|
181
|
+
pending.socket.send(JSON.stringify({ type: "unsubscribe", id }));
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** The open dev WebSocket, once the device connects, or undefined after 10 seconds without. */
|
|
185
|
+
#connection() {
|
|
186
|
+
if (this.#socket)
|
|
187
|
+
return Promise.resolve(this.#socket);
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
const connected = () => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
resolve(this.#socket);
|
|
192
|
+
};
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
this.#connected.splice(this.#connected.indexOf(connected), 1);
|
|
195
|
+
resolve(undefined);
|
|
196
|
+
}, CONNECT_TIMEOUT);
|
|
197
|
+
this.#connected.push(connected);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** The development Worker's `TokamakEvents`, called through a Miniflare session started on first use. */
|
|
202
|
+
class DevWorker {
|
|
203
|
+
name;
|
|
204
|
+
#miniflare;
|
|
205
|
+
constructor(name) {
|
|
206
|
+
this.name = name;
|
|
207
|
+
}
|
|
208
|
+
async dispatch(name, event) {
|
|
209
|
+
this.#miniflare ??= this.#start().catch((error) => {
|
|
210
|
+
this.#miniflare = undefined;
|
|
211
|
+
throw error;
|
|
212
|
+
});
|
|
213
|
+
const miniflare = await this.#miniflare;
|
|
214
|
+
const response = await miniflare.dispatchFetch("http://tokamak/", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: JSON.stringify({ name, event }),
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok)
|
|
219
|
+
throw new Error(await response.text());
|
|
220
|
+
return (await response.json()).reply;
|
|
221
|
+
}
|
|
222
|
+
async dispose() {
|
|
223
|
+
await (await this.#miniflare?.catch(() => undefined))?.dispose();
|
|
224
|
+
}
|
|
225
|
+
/** A Miniflare session from the Miniflare that Cloudflare's plugin uses. */
|
|
226
|
+
async #start() {
|
|
227
|
+
if (!this.name)
|
|
228
|
+
throw new Error("the Wrangler configuration names no Worker to deliver events to");
|
|
229
|
+
const require = createRequire(import.meta.resolve("@cloudflare/vite-plugin"));
|
|
230
|
+
const module = (await import(pathToFileURL(require.resolve("miniflare")).href));
|
|
231
|
+
const miniflare = new module.Miniflare(module.convertV4MiniflareOptions({
|
|
232
|
+
modules: true,
|
|
233
|
+
script: CALLER,
|
|
234
|
+
compatibilityDate: "2025-01-01",
|
|
235
|
+
serviceBindings: { TOKAMAK: { name: this.name, entrypoint: "TokamakEvents" } },
|
|
236
|
+
unsafeDevRegistryPath: module.getDefaultDevRegistryPath(),
|
|
237
|
+
}));
|
|
238
|
+
await miniflare.ready;
|
|
239
|
+
return miniflare;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Runs once per runtime start, including when the system starts the app in the background. */
|
|
2
|
+
export declare const onStart: (listener: import("@tokamakdev/plugin/events").Listener<{
|
|
3
|
+
readonly foreground: boolean;
|
|
4
|
+
}, void>) => void;
|
|
5
|
+
/** Runs each time the app moves into the foreground, but not when it starts there. */
|
|
6
|
+
export declare const onResume: (listener: import("@tokamakdev/plugin/events").Listener<Readonly<Record<string, never>>, void>) => void;
|
|
7
|
+
/** Runs each time the app moves out of the foreground. */
|
|
8
|
+
export declare const onSuspend: (listener: import("@tokamakdev/plugin/events").Listener<Readonly<Record<string, never>>, void>) => void;
|
|
9
|
+
/** Whether the app is on screen. */
|
|
10
|
+
export type LifecycleStage = "foreground" | "background";
|
|
11
|
+
/**
|
|
12
|
+
* The app's stage now, which can differ from the last event delivered: an `onSuspend` listener
|
|
13
|
+
* that runs late reads `"foreground"` once the user has returned. Rejects with
|
|
14
|
+
* `NotSupportedError` outside a tokamak Worker.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getLifecycleStage(): Promise<LifecycleStage>;
|
package/dist/events.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Plugin } from "@tokamakdev/plugin";
|
|
2
|
+
import { defineEvent } from "@tokamakdev/plugin/events";
|
|
3
|
+
/** Runs once per runtime start, including when the system starts the app in the background. */
|
|
4
|
+
export const onStart = defineEvent("start");
|
|
5
|
+
/** Runs each time the app moves into the foreground, but not when it starts there. */
|
|
6
|
+
export const onResume = defineEvent("resume");
|
|
7
|
+
/** Runs each time the app moves out of the foreground. */
|
|
8
|
+
export const onSuspend = defineEvent("suspend");
|
|
9
|
+
/** The functions the runtime answers itself, as the reserved plugin `tokamak`. */
|
|
10
|
+
class Tokamak extends Plugin {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("tokamak");
|
|
13
|
+
}
|
|
14
|
+
lifecycleStage() {
|
|
15
|
+
return this.call("lifecycleStage");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const tokamak = new Tokamak();
|
|
19
|
+
/**
|
|
20
|
+
* The app's stage now, which can differ from the last event delivered: an `onSuspend` listener
|
|
21
|
+
* that runs late reads `"foreground"` once the user has returned. Rejects with
|
|
22
|
+
* `NotSupportedError` outside a tokamak Worker.
|
|
23
|
+
*/
|
|
24
|
+
export function getLifecycleStage() {
|
|
25
|
+
return tokamak.lifecycleStage();
|
|
26
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** A setting value. */
|
|
2
|
+
export type Value = string | number | boolean;
|
|
3
|
+
/** One platform's settings: the shared keys and the settings its platform pack declares. */
|
|
4
|
+
export interface PlatformConfig {
|
|
5
|
+
/** Display name. */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** Application identifier. */
|
|
8
|
+
identifier?: string;
|
|
9
|
+
/** Icon in the platform's format, relative to the Vite root. */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** A setting the platform pack declares; `tok` validates it. */
|
|
12
|
+
[key: string]: Value | undefined;
|
|
13
|
+
}
|
|
14
|
+
/** One plugin's settings: values for every platform, and each platform's own. */
|
|
15
|
+
export interface PluginConfig {
|
|
16
|
+
android?: Record<string, Value | undefined>;
|
|
17
|
+
ios?: Record<string, Value | undefined>;
|
|
18
|
+
macos?: Record<string, Value | undefined>;
|
|
19
|
+
windows?: Record<string, Value | undefined>;
|
|
20
|
+
/** A setting the plugin declares; `tok` validates it. */
|
|
21
|
+
[key: string]: Value | Record<string, Value | undefined> | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The app's tokamak configuration: the tokamak Vite plugin's options other than `module`. Other
|
|
25
|
+
* keys are plugins' settings, by plugin ID.
|
|
26
|
+
*/
|
|
27
|
+
export interface Config {
|
|
28
|
+
/** Display name for every platform. */
|
|
29
|
+
name?: string;
|
|
30
|
+
/** Application identifier for every platform. */
|
|
31
|
+
identifier?: string;
|
|
32
|
+
/** Icon for every platform, relative to the Vite root. */
|
|
33
|
+
icon?: string;
|
|
34
|
+
/** App version. */
|
|
35
|
+
version?: string;
|
|
36
|
+
android?: PlatformConfig;
|
|
37
|
+
ios?: PlatformConfig;
|
|
38
|
+
macos?: PlatformConfig;
|
|
39
|
+
windows?: PlatformConfig;
|
|
40
|
+
/** A plugin's settings, by its ID. */
|
|
41
|
+
[plugin: string]: string | PlatformConfig | PluginConfig | undefined;
|
|
42
|
+
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines `__tokamakNativeCall` and `__tokamakNativeListen`, which post to the plugin's loopback
|
|
3
|
+
* `port` with the session `token`. `waitUntil` keeps each listener's invocation running, as
|
|
4
|
+
* `waitUntil` from `cloudflare:workers` does.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineNativeCalls(port: number, token: string, waitUntil: (promise: Promise<unknown>) => void): void;
|
package/dist/native.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Defines the development Worker's native calls, which reach the device through the tokamak Vite
|
|
2
|
+
// plugin. The plugin's entry Worker transform imports this module in development only.
|
|
3
|
+
/**
|
|
4
|
+
* Defines `__tokamakNativeCall` and `__tokamakNativeListen`, which post to the plugin's loopback
|
|
5
|
+
* `port` with the session `token`. `waitUntil` keeps each listener's invocation running, as
|
|
6
|
+
* `waitUntil` from `cloudflare:workers` does.
|
|
7
|
+
*/
|
|
8
|
+
export function defineNativeCalls(port, token, waitUntil) {
|
|
9
|
+
const post = async (path, plugin, method, arguments_, signal) => {
|
|
10
|
+
const response = await fetch(`http://127.0.0.1:${String(port)}${path}`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "x-tokamak-session": token },
|
|
13
|
+
body: JSON.stringify({ plugin, method, arguments: arguments_ }),
|
|
14
|
+
signal,
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
throw new Error(`the tokamak Vite plugin answered HTTP ${String(response.status)}`);
|
|
18
|
+
return response;
|
|
19
|
+
};
|
|
20
|
+
globalThis.__tokamakNativeCall = async (plugin, method, arguments_ = null) => {
|
|
21
|
+
let result;
|
|
22
|
+
try {
|
|
23
|
+
result = (await (await post("/call", plugin, method, arguments_)).json());
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw networkError(error);
|
|
27
|
+
}
|
|
28
|
+
if (result.error)
|
|
29
|
+
throw new DOMException(result.error.message, result.error.name);
|
|
30
|
+
return result.value;
|
|
31
|
+
};
|
|
32
|
+
globalThis.__tokamakNativeListen = (plugin, method, arguments_, next, error) => {
|
|
33
|
+
const listening = new AbortController();
|
|
34
|
+
const deliver = (result) => {
|
|
35
|
+
try {
|
|
36
|
+
if (result.error)
|
|
37
|
+
error(new DOMException(result.error.message, result.error.name));
|
|
38
|
+
else
|
|
39
|
+
next(result.value);
|
|
40
|
+
}
|
|
41
|
+
catch (thrown) {
|
|
42
|
+
reportError(thrown);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const listen = async () => {
|
|
46
|
+
const response = await post("/subscribe", plugin, method, arguments_ ?? null, listening.signal);
|
|
47
|
+
for await (const result of lines(response))
|
|
48
|
+
deliver(result);
|
|
49
|
+
};
|
|
50
|
+
waitUntil(listen().catch((failure) => {
|
|
51
|
+
if (!listening.signal.aborted)
|
|
52
|
+
deliver({ error: networkError(failure), done: true });
|
|
53
|
+
}));
|
|
54
|
+
return () => {
|
|
55
|
+
listening.abort();
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Each line of `response`'s body, parsed as a result. */
|
|
60
|
+
async function* lines(response) {
|
|
61
|
+
const decoder = new TextDecoder();
|
|
62
|
+
let text = "";
|
|
63
|
+
for await (const chunk of response.body ?? []) {
|
|
64
|
+
text += decoder.decode(chunk, { stream: true });
|
|
65
|
+
const complete = text.split("\n");
|
|
66
|
+
text = complete.pop() ?? "";
|
|
67
|
+
for (const line of complete)
|
|
68
|
+
yield JSON.parse(line);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function networkError(error) {
|
|
72
|
+
return new DOMException(`The device could not be reached: ${String(error)}`, "NetworkError");
|
|
73
|
+
}
|
package/dist/vite.d.mts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Plugin } from "vite";
|
|
2
|
+
import type { Config } from "./index.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Reports the app's tokamak configuration, and the development server's
|
|
5
|
+
* address, HTTPS certificate, Worker name and dev socket port, to `tok`, and
|
|
6
|
+
* makes the entry Worker import `module` and export `TokamakEvents`. In
|
|
7
|
+
* development, the entry Worker also defines native calls that reach the
|
|
8
|
+
* device through the dev socket. Without `TOKAMAK_VITE_OUTPUT`, which `tok`
|
|
9
|
+
* sets, it adds no hooks.
|
|
10
|
+
*/
|
|
11
|
+
export declare function tokamak({ module, ...config }?: Config & {
|
|
12
|
+
/**
|
|
13
|
+
* The module the entry Worker imports, relative to the Vite root; by
|
|
14
|
+
* default `src/tokamak.ts`, then `src/tokamak.js`, when one exists.
|
|
15
|
+
*/
|
|
16
|
+
module?: string;
|
|
17
|
+
}): Plugin[];
|
package/dist/vite.mjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { X509Certificate } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { normalizePath, } from "vite";
|
|
6
|
+
import { openDevSocket } from "./dev-socket.mjs";
|
|
7
|
+
/** Modules tried in order when the options name none. */
|
|
8
|
+
const DEFAULT_MODULES = ["src/tokamak.ts", "src/tokamak.js"];
|
|
9
|
+
/** Wrangler configuration files Cloudflare's plugin tries in order. */
|
|
10
|
+
const WRANGLER_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
11
|
+
const MIDDLEWARE_MODE_ERROR = "Vite is running in middleware mode, so tokamak cannot find the development server's address; tok dev needs Vite's own dev server";
|
|
12
|
+
/** The module that defines the development Worker's native calls. */
|
|
13
|
+
const NATIVE_CALLS = normalizePath(fileURLToPath(new URL("./native.mjs", import.meta.url)));
|
|
14
|
+
/**
|
|
15
|
+
* Reports the app's tokamak configuration, and the development server's
|
|
16
|
+
* address, HTTPS certificate, Worker name and dev socket port, to `tok`, and
|
|
17
|
+
* makes the entry Worker import `module` and export `TokamakEvents`. In
|
|
18
|
+
* development, the entry Worker also defines native calls that reach the
|
|
19
|
+
* device through the dev socket. Without `TOKAMAK_VITE_OUTPUT`, which `tok`
|
|
20
|
+
* sets, it adds no hooks.
|
|
21
|
+
*/
|
|
22
|
+
export function tokamak({ module, ...config } = {}) {
|
|
23
|
+
const output = process.env.TOKAMAK_VITE_OUTPUT;
|
|
24
|
+
if (!output) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
let file;
|
|
28
|
+
const events = import.meta.resolve("@tokamakdev/plugin/events");
|
|
29
|
+
/** The module that defines `TokamakEvents`. */
|
|
30
|
+
const entrypoint = normalizePath(fileURLToPath(new URL("./entrypoint.js", events)));
|
|
31
|
+
/** The module that holds event listeners. */
|
|
32
|
+
const registryFile = normalizePath(fileURLToPath(new URL("./registry.ts", events)));
|
|
33
|
+
/** The module IDs of each entry environment's inputs. */
|
|
34
|
+
const entryIds = new Map();
|
|
35
|
+
/** The entry environments whose input exports `TokamakEvents`. */
|
|
36
|
+
const exporting = new Set();
|
|
37
|
+
/** The development server's dev socket, once open. */
|
|
38
|
+
let devSocket;
|
|
39
|
+
const token = process.env.TOKAMAK_SESSION_TOKEN;
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
name: "tokamak",
|
|
43
|
+
// Keeps @tokamakdev/plugin out of pre-bundled dependencies, so they
|
|
44
|
+
// share the one module that holds event listeners.
|
|
45
|
+
configEnvironment() {
|
|
46
|
+
return { optimizeDeps: { exclude: ["@tokamakdev/plugin"] } };
|
|
47
|
+
},
|
|
48
|
+
configResolved({ root }) {
|
|
49
|
+
file = findModule(root, module);
|
|
50
|
+
writeJson(output, "config.json", { root, config });
|
|
51
|
+
},
|
|
52
|
+
// Runs before plugins that load the entry Worker, whose native calls
|
|
53
|
+
// need the dev socket's port.
|
|
54
|
+
configureServer: {
|
|
55
|
+
order: "pre",
|
|
56
|
+
async handler(server) {
|
|
57
|
+
const httpServer = server.httpServer;
|
|
58
|
+
// A middleware-mode server beside a reported one, such as the server
|
|
59
|
+
// Astro syncs content with, leaves the report in place.
|
|
60
|
+
if (!httpServer) {
|
|
61
|
+
if (!fs.existsSync(path.join(output, "server.json"))) {
|
|
62
|
+
writeJson(output, "server.json", { error: MIDDLEWARE_MODE_ERROR });
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const worker = await readWorker(server.config.root);
|
|
67
|
+
const socket = await openDevSocket(worker?.name, token);
|
|
68
|
+
devSocket = socket;
|
|
69
|
+
httpServer.once("close", () => void socket.close());
|
|
70
|
+
httpServer.once("listening", () => {
|
|
71
|
+
const urls = server.resolvedUrls;
|
|
72
|
+
writeJson(output, "server.json", {
|
|
73
|
+
url: urls?.local[0] ?? urls?.network[0],
|
|
74
|
+
workerName: worker?.topLevelName ?? worker?.name,
|
|
75
|
+
certificates: endEntityCertificates(server.config.server.https),
|
|
76
|
+
socketPort: socket.port,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
async buildApp(builder) {
|
|
82
|
+
const environments = Object.values(builder.environments);
|
|
83
|
+
if (!environments.some((environment) => buildsEntryWorker(environment.config))) {
|
|
84
|
+
throw new Error("no environment builds the entry Worker to export TokamakEvents from");
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "tokamak:entry",
|
|
90
|
+
applyToEnvironment: ({ config }) => buildsEntryWorker(config),
|
|
91
|
+
async transform(code, id) {
|
|
92
|
+
const environment = this.environment;
|
|
93
|
+
const ids = entryIds.get(environment.name) ??
|
|
94
|
+
Promise.all(inputs(environment.config.build).map(async (input) => (await this.resolve(input))?.id));
|
|
95
|
+
entryIds.set(environment.name, ids);
|
|
96
|
+
if (!(await ids).includes(id)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
exporting.add(environment.name);
|
|
100
|
+
// In Cloudflare's Worker entry, which dev and build both start from,
|
|
101
|
+
// this follows the imports of its polyfills and of the app's entry.
|
|
102
|
+
const imports = file ? `import ${JSON.stringify(file)};\n` : "";
|
|
103
|
+
const native = environment.mode === "dev" && devSocket && token ? nativeCalls(devSocket.port, token) : "";
|
|
104
|
+
return {
|
|
105
|
+
code: `${code}\n${imports}export { TokamakEvents } from ${JSON.stringify(entrypoint)};\n${native}`,
|
|
106
|
+
map: null,
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
// A module that registers listeners adds them again each time it
|
|
110
|
+
// evaluates, so when an update re-evaluates one, the registry and every
|
|
111
|
+
// module that imports it evaluate again too.
|
|
112
|
+
async hotUpdate({ modules, timestamp }) {
|
|
113
|
+
const graph = this.environment.moduleGraph;
|
|
114
|
+
const registry = graph.getModuleById(registryFile);
|
|
115
|
+
if (!registry) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const entries = (await entryIds.get(this.environment.name)) ?? [];
|
|
119
|
+
const registering = [...withImporters(registry)].filter((module) => !entries.some((id) => id === module.id));
|
|
120
|
+
const updated = new Set(modules.flatMap((changed) => [...withImporters(changed)]));
|
|
121
|
+
if (registering.some((module) => updated.has(module))) {
|
|
122
|
+
graph.invalidateModule(registry, new Set(), timestamp, true);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
generateBundle() {
|
|
126
|
+
if (!exporting.has(this.environment.name)) {
|
|
127
|
+
this.error(`the input of the ${this.environment.name} environment did not export TokamakEvents`);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Code that defines the development Worker's native calls on the dev socket's
|
|
135
|
+
* `port`, with the session `token`, which only the entry Worker's environment
|
|
136
|
+
* sees.
|
|
137
|
+
*/
|
|
138
|
+
function nativeCalls(port, token) {
|
|
139
|
+
return `import { waitUntil as __tokamakWaitUntil } from "cloudflare:workers";
|
|
140
|
+
import { defineNativeCalls as __tokamakDefineNativeCalls } from ${JSON.stringify(NATIVE_CALLS)};
|
|
141
|
+
__tokamakDefineNativeCalls(${String(port)}, ${JSON.stringify(token)}, __tokamakWaitUntil);
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
/** `module` and every module that imports it, directly or through others. */
|
|
145
|
+
function withImporters(module) {
|
|
146
|
+
const found = new Set();
|
|
147
|
+
const pending = [module];
|
|
148
|
+
for (let current = pending.pop(); current; current = pending.pop()) {
|
|
149
|
+
if (!found.has(current)) {
|
|
150
|
+
found.add(current);
|
|
151
|
+
pending.push(...current.importers);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
/** The module `name` under `root`, or else the first default under `root` that exists. */
|
|
157
|
+
function findModule(root, name) {
|
|
158
|
+
const candidates = (name ? [name] : DEFAULT_MODULES).map((candidate) => normalizePath(path.resolve(root, candidate)));
|
|
159
|
+
const file = candidates.find((candidate) => fs.existsSync(candidate));
|
|
160
|
+
if (name && !file) {
|
|
161
|
+
throw new Error(`tokamak module not found: ${candidates[0]}`);
|
|
162
|
+
}
|
|
163
|
+
return file;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The Worker in the Wrangler configuration file in `root`, read as
|
|
167
|
+
* Cloudflare's plugin reads it without `configPath`, in the `CLOUDFLARE_ENV`
|
|
168
|
+
* environment: its name, and its name without the environment's suffix.
|
|
169
|
+
*/
|
|
170
|
+
async function readWorker(root) {
|
|
171
|
+
const file = WRANGLER_FILES.map((name) => path.join(root, name)).find((candidate) => fs.existsSync(candidate));
|
|
172
|
+
if (!file) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
const { unstable_readConfig } = await import("wrangler");
|
|
176
|
+
return unstable_readConfig({ config: file, env: process.env.CLOUDFLARE_ENV }, { hideWarnings: true });
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The PEM of the end-entity certificate of each chain in `https.cert`, which
|
|
180
|
+
* Vite reads from a file when a string names one.
|
|
181
|
+
*/
|
|
182
|
+
function endEntityCertificates(https) {
|
|
183
|
+
const cert = typeof https?.cert === "string" ? readFileIfExists(https.cert) : https?.cert;
|
|
184
|
+
return cert && [cert].flat().map((chain) => new X509Certificate(chain).toString()).join("");
|
|
185
|
+
}
|
|
186
|
+
function readFileIfExists(file) {
|
|
187
|
+
try {
|
|
188
|
+
return fs.readFileSync(path.resolve(file));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return file;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Whether an environment builds from the entry Worker: Cloudflare's plugin
|
|
196
|
+
* gives a manifest to its entry and prerender Workers, and Astro to its server
|
|
197
|
+
* and prerender environments.
|
|
198
|
+
*/
|
|
199
|
+
function buildsEntryWorker(config) {
|
|
200
|
+
return config.consumer === "server" && Boolean(config.build.manifest);
|
|
201
|
+
}
|
|
202
|
+
/** The modules an environment's build starts from. */
|
|
203
|
+
function inputs(build) {
|
|
204
|
+
const { input } = build.rollupOptions;
|
|
205
|
+
return typeof input === "string" ? [input] : Object.values(input ?? {});
|
|
206
|
+
}
|
|
207
|
+
/** Writes `value` to `name` whole, as `tok` reads the file as soon as it exists. */
|
|
208
|
+
function writeJson(output, name, value) {
|
|
209
|
+
const file = path.join(output, name);
|
|
210
|
+
fs.mkdirSync(output, { recursive: true });
|
|
211
|
+
fs.writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`);
|
|
212
|
+
fs.renameSync(`${file}.tmp`, file);
|
|
213
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokamakdev/tok",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "The tokamak CLI: build native apps from a Cloudflare Workers codebase",
|
|
3
|
+
"version": "0.1.1-beta.100",
|
|
4
|
+
"description": "The tokamak CLI, Vite plugin and app API: build native apps from a Cloudflare Workers codebase",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/mantty/tokamak.git",
|
|
8
8
|
"directory": "tokamak-cli/npm"
|
|
9
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./events": {
|
|
16
|
+
"types": "./dist/events.d.mts",
|
|
17
|
+
"default": "./dist/events.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./vite": {
|
|
20
|
+
"types": "./dist/vite.d.mts",
|
|
21
|
+
"default": "./dist/vite.mjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
10
24
|
"bin": {
|
|
11
25
|
"tok": "bin/tok.js"
|
|
12
26
|
},
|
|
13
27
|
"files": [
|
|
14
28
|
"bin",
|
|
29
|
+
"dist",
|
|
15
30
|
"platform.js",
|
|
16
31
|
"README.md"
|
|
17
32
|
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"test": "node --test"
|
|
37
|
+
},
|
|
18
38
|
"engines": {
|
|
19
39
|
"node": ">=18"
|
|
20
40
|
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@tokamakdev/plugin": "0.1.0-beta.100",
|
|
43
|
+
"ws": "^8.22.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@cloudflare/vite-plugin": "^1.54.0",
|
|
47
|
+
"vite": "^7.0.0 || ^8.0.0",
|
|
48
|
+
"wrangler": "^4.126.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@cloudflare/vite-plugin": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"wrangler": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@cloudflare/vite-plugin": "1.62.5",
|
|
60
|
+
"@types/node": "22.19.15",
|
|
61
|
+
"@types/ws": "8.18.2",
|
|
62
|
+
"typescript": "5.9.3",
|
|
63
|
+
"vite": "8.3.2",
|
|
64
|
+
"wrangler": "4.147.0"
|
|
65
|
+
},
|
|
21
66
|
"optionalDependencies": {
|
|
22
|
-
"@tokamakdev/
|
|
23
|
-
"@tokamakdev/
|
|
24
|
-
"@tokamakdev/
|
|
25
|
-
"@tokamakdev/
|
|
67
|
+
"@tokamakdev/platform-android-arm64": "0.1.1-beta.100",
|
|
68
|
+
"@tokamakdev/platform-ios-arm64": "0.1.1-beta.100",
|
|
69
|
+
"@tokamakdev/platform-ios-simulator-arm64": "0.1.1-beta.100",
|
|
70
|
+
"@tokamakdev/platform-ios-simulator-x64": "0.1.1-beta.100",
|
|
71
|
+
"@tokamakdev/platform-macos-arm64": "0.1.1-beta.100",
|
|
72
|
+
"@tokamakdev/platform-macos-x64": "0.1.1-beta.100",
|
|
73
|
+
"@tokamakdev/platform-windows-x64": "0.1.1-beta.100",
|
|
74
|
+
"@tokamakdev/tok-darwin-arm64": "0.1.1-beta.100",
|
|
75
|
+
"@tokamakdev/tok-darwin-x64": "0.1.1-beta.100",
|
|
76
|
+
"@tokamakdev/tok-linux-x64": "0.1.1-beta.100",
|
|
77
|
+
"@tokamakdev/tok-win32-x64": "0.1.1-beta.100"
|
|
26
78
|
},
|
|
27
79
|
"publishConfig": {
|
|
28
80
|
"access": "public"
|
package/platform.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { optionalDependencies } = require("./package.json");
|
|
5
|
+
|
|
3
6
|
const PACKAGES = {
|
|
4
7
|
"darwin-arm64": "@tokamakdev/tok-darwin-arm64",
|
|
5
8
|
"darwin-x64": "@tokamakdev/tok-darwin-x64",
|
|
@@ -7,6 +10,8 @@ const PACKAGES = {
|
|
|
7
10
|
"win32-x64": "@tokamakdev/tok-win32-x64",
|
|
8
11
|
};
|
|
9
12
|
|
|
13
|
+
const PLATFORM_PACKS = Object.keys(optionalDependencies).filter((name) => name.startsWith("@tokamakdev/platform-"));
|
|
14
|
+
|
|
10
15
|
function packageFor(platform, arch) {
|
|
11
16
|
const name = PACKAGES[`${platform}-${arch}`];
|
|
12
17
|
if (!name) throw new Error(`tokamak does not publish a CLI for ${platform} ${arch}`);
|
|
@@ -23,4 +28,15 @@ function binaryPath(platform, arch, resolve) {
|
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
// npm installs only the platform packs whose os and cpu match this host.
|
|
32
|
+
function platformPackRoots(resolve) {
|
|
33
|
+
return PLATFORM_PACKS.flatMap((name) => {
|
|
34
|
+
try {
|
|
35
|
+
return [path.dirname(resolve(`${name}/package.json`))];
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { PLATFORM_PACKS, packageFor, binaryPath, platformPackRoots };
|