@tokamakdev/tok 0.1.1-beta.106 → 0.1.1-beta.112

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/dist/native.mjs CHANGED
@@ -1,72 +1,119 @@
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.
1
+ import { decodeMessage, encodeMessage, messages, nativeError } from "@tokamakdev/plugin/native-protocol";
2
+ const encoder = new TextEncoder();
3
+ const decoder = new TextDecoder();
3
4
  /**
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.
5
+ * The development invocation `ctx`'s native connection, which reaches the byte endpoints of a
6
+ * device on `platform` through the relay on `port`. Its subscriptions are `ctx`'s `waitUntil` work.
7
7
  */
8
- export function defineNativeCalls(port, token, waitUntil) {
9
- const post = async (path, plugin, method, arguments_, signal) => {
8
+ export function connectDevice(port, token, ctx, platform) {
9
+ const post = async (path, request, signal) => {
10
10
  const response = await fetch(`http://127.0.0.1:${String(port)}${path}`, {
11
11
  method: "POST",
12
12
  headers: { "x-tokamak-session": token },
13
- body: JSON.stringify({ plugin, method, arguments: arguments_ }),
13
+ body: request,
14
14
  signal,
15
+ duplex: "half",
15
16
  });
16
17
  if (!response.ok)
17
18
  throw new Error(`the tokamak Vite plugin answered HTTP ${String(response.status)}`);
18
19
  return response;
19
20
  };
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;
21
+ const result = ({ header, payload }) => {
22
+ if (header.failed)
23
+ throw nativeError(JSON.parse(decoder.decode(payload)));
24
+ return payload;
31
25
  };
32
- globalThis.__tokamakNativeListen = (plugin, method, arguments_, next, error) => {
33
- const listening = new AbortController();
34
- const deliver = (result) => {
26
+ return {
27
+ platform,
28
+ async call(plugin, method, payload, { body, signal } = {}) {
29
+ signal?.throwIfAborted();
30
+ const header = { type: body ? "callWithBody" : "call", plugin, method };
31
+ let message;
35
32
  try {
36
- if (result.error)
37
- error(new DOMException(result.error.message, result.error.name));
38
- else
39
- next(result.value);
33
+ const request = body ? framed(encodeMessage(header, payload), body) : encodeMessage(header, payload);
34
+ message = decodeMessage(new Uint8Array(await (await post("/call", request, signal)).arrayBuffer()));
40
35
  }
41
- catch (thrown) {
42
- reportError(thrown);
36
+ catch (error) {
37
+ if (signal?.aborted)
38
+ throw signal.reason;
39
+ throw networkError(error);
43
40
  }
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
- };
41
+ return result(message);
42
+ },
43
+ listen(plugin, method, payload, { next, error, overflow = "error" }) {
44
+ const listening = new AbortController();
45
+ const deliver = (message) => {
46
+ let value;
47
+ try {
48
+ value = result(message);
49
+ }
50
+ catch (failure) {
51
+ try {
52
+ error(failure, message.header.done === true);
53
+ }
54
+ catch (thrown) {
55
+ reportError(thrown);
56
+ }
57
+ return;
58
+ }
59
+ try {
60
+ next(value, message.header.done === true);
61
+ }
62
+ catch (thrown) {
63
+ reportError(thrown);
64
+ }
65
+ };
66
+ const listen = async () => {
67
+ const request = encodeMessage({ type: "subscribe", plugin, method, overflow }, payload);
68
+ const response = await post("/subscribe", request, listening.signal);
69
+ for await (const message of messages(response.body))
70
+ deliver(message);
71
+ };
72
+ ctx.waitUntil(listen().catch((failure) => {
73
+ if (!listening.signal.aborted) {
74
+ try {
75
+ error(networkError(failure), true);
76
+ }
77
+ catch (thrown) {
78
+ reportError(thrown);
79
+ }
80
+ }
81
+ }));
82
+ return () => { listening.abort(); };
83
+ },
57
84
  };
58
85
  }
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
- }
86
+ /** The call message `first`, then a `chunk` message per chunk of `body` as the relay reads them. */
87
+ function framed(first, body) {
88
+ const reader = body.getReader();
89
+ let started = false;
90
+ const chunk = (header, payload) => encodeMessage({ type: "chunk", ...header }, payload);
91
+ return new ReadableStream({
92
+ async pull(controller) {
93
+ if (!started) {
94
+ started = true;
95
+ controller.enqueue(first);
96
+ return;
97
+ }
98
+ try {
99
+ const { value, done } = await reader.read();
100
+ if (done) {
101
+ controller.enqueue(chunk({ done: true }, new Uint8Array()));
102
+ controller.close();
103
+ return;
104
+ }
105
+ if (!(value instanceof Uint8Array))
106
+ throw new TypeError("A native call's body must contain Uint8Array chunks");
107
+ controller.enqueue(chunk({}, value));
108
+ }
109
+ catch (error) {
110
+ const failure = { name: error?.name ?? "Error", message: String(error?.message ?? error) };
111
+ controller.enqueue(chunk({ failed: true, done: true }, encoder.encode(JSON.stringify(failure))));
112
+ controller.close();
113
+ }
114
+ },
115
+ cancel(reason) { return reader.cancel(reason); },
116
+ });
70
117
  }
71
118
  function networkError(error) {
72
119
  return new DOMException(`The device could not be reached: ${String(error)}`, "NetworkError");
@@ -0,0 +1,31 @@
1
+ export interface ModuleExport {
2
+ module: string;
3
+ export: string;
4
+ }
5
+ /** A native platform a plugin builds for. */
6
+ export type NativePlatform = "ios" | "macos" | "android" | "windows" | "linux";
7
+ /** Where an operation runs, and what it needs there. */
8
+ export interface Operation {
9
+ /** The native platforms that implement it, with any minimum OS version. */
10
+ platforms: {
11
+ name: NativePlatform;
12
+ minimum?: string;
13
+ }[];
14
+ /** The native platforms where it needs the app in the foreground, to show UI. */
15
+ foreground: NativePlatform[];
16
+ }
17
+ export interface PluginDefinition {
18
+ id: string;
19
+ package: string;
20
+ manifest: string;
21
+ api: ModuleExport;
22
+ web?: ModuleExport;
23
+ binding: string;
24
+ client: string;
25
+ /** The native platforms the plugin builds for. */
26
+ platforms: NativePlatform[];
27
+ /** Each operation the manifest describes; others run on every platform the plugin builds for. */
28
+ operations: Record<string, Operation>;
29
+ }
30
+ /** Installed direct dependencies select the application's plugin services. */
31
+ export declare function discoverPlugins(root: string): PluginDefinition[];
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ const nativePlatforms = ["ios", "macos", "android", "windows", "linux"];
5
+ const reservedIds = ["tokamak", "android", "ios", "macos", "windows", "name", "identifier", "icon", "version",
6
+ "module", "build", "project", "platform", "skip", "host", "help", "vite", "session"];
7
+ /** Installed direct dependencies select the application's plugin services. */
8
+ export function discoverPlugins(root) {
9
+ const file = path.join(root, "package.json");
10
+ if (!fs.existsSync(file))
11
+ return [];
12
+ const app = JSON.parse(fs.readFileSync(file, "utf8"));
13
+ const dependencies = new Set(["dependencies", "devDependencies", "peerDependencies"]
14
+ .flatMap(key => Object.keys(app[key] ?? {})));
15
+ const require = createRequire(file);
16
+ const plugins = new Map();
17
+ for (const name of dependencies) {
18
+ const directory = require.resolve.paths(name)?.map(base => path.join(base, name))
19
+ .find(candidate => fs.existsSync(path.join(candidate, "package.json")));
20
+ if (!directory)
21
+ continue;
22
+ const candidate = path.join(directory, "tokamak-plugin.json");
23
+ if (!fs.existsSync(candidate))
24
+ continue;
25
+ const manifest = fs.realpathSync(candidate).split(path.sep).join("/");
26
+ const data = JSON.parse(fs.readFileSync(manifest, "utf8"));
27
+ // Schema version one supplies only native build metadata.
28
+ if (data.schemaVersion === 1)
29
+ continue;
30
+ if (data.schemaVersion !== 2)
31
+ throw new Error(`${manifest}: unsupported plugin schema version ${data.schemaVersion}.`);
32
+ if (typeof data.id !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(data.id) || data.id.length > 63) {
33
+ throw new Error(`${manifest}: plugin id must be lowercase words joined by single hyphens.`);
34
+ }
35
+ const reserved = reservedIds.find(word => data.id === word || data.id.startsWith(`${word}-`));
36
+ if (reserved)
37
+ throw new Error(`${manifest}: plugin id must not start with '${reserved}', which Tokamak's options use.`);
38
+ const api = moduleExport(directory, data.api, "api");
39
+ if (plugins.has(data.id))
40
+ throw new Error(`Duplicate Tokamak plugin id ${data.id}.`);
41
+ const platforms = nativePlatforms.filter(platform => data.platforms?.[platform] !== undefined);
42
+ plugins.set(data.id, {
43
+ id: data.id, package: name, manifest, api,
44
+ web: data.web === undefined ? undefined : moduleExport(directory, data.web, "web"),
45
+ binding: `TOKAMAK_${data.id.toUpperCase().replaceAll("-", "_")}`,
46
+ client: `${name}/client`,
47
+ platforms,
48
+ operations: operations(manifest, data.operations ?? {}, platforms),
49
+ });
50
+ }
51
+ return [...plugins.values()].sort((left, right) => left.id.localeCompare(right.id));
52
+ }
53
+ function moduleExport(root, reference, field) {
54
+ const value = reference;
55
+ if (!value || typeof value.module !== "string" || typeof value.export !== "string" || !value.export) {
56
+ throw new Error(`${root}: ${field} must name a module and export.`);
57
+ }
58
+ const module = fs.realpathSync(path.resolve(root, value.module));
59
+ const relative = path.relative(fs.realpathSync(root), module);
60
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || !fs.statSync(module).isFile()) {
61
+ throw new Error(`${root}: ${field}.module must be a file inside the plugin package.`);
62
+ }
63
+ return { module: module.split(path.sep).join("/"), export: value.export };
64
+ }
65
+ /** The manifest's `operations`, each on platforms the plugin builds for. */
66
+ function operations(manifest, value, built) {
67
+ const fail = (message) => { throw new Error(`${manifest}: ${message}`); };
68
+ const platform = (name, field) => built.includes(name) ? name : fail(`${field} names ${name}, which the plugin does not build for.`);
69
+ if (!isRecord(value))
70
+ return fail("operations must be an object.");
71
+ const result = {};
72
+ for (const [name, entry] of Object.entries(value)) {
73
+ const field = `operations.${name}`;
74
+ if (!isRecord(entry))
75
+ return fail(`${field} must be an object.`);
76
+ const { platforms, foreground = false, ...unknown } = entry;
77
+ if (Object.keys(unknown).length)
78
+ fail(`${field} has unknown keys: ${Object.keys(unknown).join(", ")}.`);
79
+ if (platforms !== undefined && (!isRecord(platforms) ||
80
+ !Object.values(platforms).every(version => typeof version === "string" || version === true))) {
81
+ fail(`${field}.platforms must map platforms to a minimum OS version, or true for the app's minimum.`);
82
+ }
83
+ if (typeof foreground !== "boolean" && !(Array.isArray(foreground) && foreground.every(name => typeof name === "string"))) {
84
+ fail(`${field}.foreground must be true or a list of platforms.`);
85
+ }
86
+ const implemented = platforms === undefined
87
+ ? built.map(name => ({ name }))
88
+ : Object.entries(platforms).map(([name, minimum]) => ({ name: platform(name, `${field}.platforms`), minimum: minimum === true ? undefined : minimum }));
89
+ const needs = foreground === true ? implemented.map(({ name }) => name) : foreground === false ? [] : foreground;
90
+ result[name] = { platforms: implemented, foreground: needs.map(name => platform(name, `${field}.foreground`)) };
91
+ }
92
+ return result;
93
+ }
94
+ function isRecord(value) {
95
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96
+ }
@@ -0,0 +1,9 @@
1
+ import ts from "typescript";
2
+ /** A method that passes a value only the Tokamak worker can hold. */
3
+ export declare class WorkerOnlyMethod extends Error {
4
+ }
5
+ /**
6
+ * Validates every overload before generating a remote method. A signature containing the
7
+ * `workerOnly` type throws `WorkerOnlyMethod`.
8
+ */
9
+ export declare function checkRpcMethod(checker: ts.TypeChecker, method: ts.Type, name: string, workerOnly?: ts.Symbol): void;
@@ -0,0 +1,168 @@
1
+ import ts from "typescript";
2
+ const scalars = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike |
3
+ ts.TypeFlags.BigIntLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Never;
4
+ const binaryViews = new Set([
5
+ "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array",
6
+ "Int32Array", "Uint32Array", "Float16Array", "Float32Array", "Float64Array",
7
+ "BigInt64Array", "BigUint64Array", "DataView",
8
+ ]);
9
+ const errors = new Set(["Error", "DOMException", "AggregateError", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"]);
10
+ /** A method that passes a value only the Tokamak worker can hold. */
11
+ export class WorkerOnlyMethod extends Error {
12
+ }
13
+ /**
14
+ * Validates every overload before generating a remote method. A signature containing the
15
+ * `workerOnly` type throws `WorkerOnlyMethod`.
16
+ */
17
+ export function checkRpcMethod(checker, method, name, workerOnly) {
18
+ const signatures = method.getCallSignatures();
19
+ if (!signatures.length)
20
+ reject(checker, method, name, "expected a callable method");
21
+ for (const signature of signatures)
22
+ checkSignature(checker, signature, name, workerOnly);
23
+ }
24
+ function checkSignature(checker, signature, name, workerOnly) {
25
+ if (signature.typeParameters?.length) {
26
+ throw new Error(`Unsupported RPC signature at ${name}: generic methods need a concrete service signature.`);
27
+ }
28
+ const traversal = { seen: new Set(), depth: 0, workerOnly };
29
+ for (const parameter of signature.parameters) {
30
+ checkValue(checker, checker.getTypeOfSymbol(parameter), `${name}.${parameter.name}`, traversal);
31
+ }
32
+ const result = checker.getAwaitedType(signature.getReturnType());
33
+ if (!result)
34
+ throw new Error(`Unsupported RPC result at ${name}.result: cannot resolve the awaited result.`);
35
+ for (const member of result.isUnion() ? result.types : [result]) {
36
+ if (!(member.flags & ts.TypeFlags.Void))
37
+ checkValue(checker, member, `${name}.result`, traversal);
38
+ }
39
+ }
40
+ function reject(checker, type, path, reason) {
41
+ throw new Error(`Unsupported RPC type at ${path}: ${reason ?? checker.typeToString(type)}.`);
42
+ }
43
+ /** Global ambient types identify host values; module-local names do not. */
44
+ function builtin(checker, type) {
45
+ const symbol = type.getSymbol();
46
+ if (!symbol)
47
+ return;
48
+ if (symbol.name === "ReadableStream" && nodeReadableStream(checker, symbol))
49
+ return symbol.name;
50
+ if (symbol !== checker.resolveName(symbol.name, undefined, ts.SymbolFlags.Type, false))
51
+ return;
52
+ const ambient = symbol.declarations?.some((declaration) => declaration.getSourceFile().isDeclarationFile);
53
+ const value = checker.resolveName(symbol.name, undefined, ts.SymbolFlags.Value, false);
54
+ return ambient && (value || ["ReadonlyArray", "ReadonlyMap", "ReadonlySet"].includes(symbol.name)) ? symbol.name : undefined;
55
+ }
56
+ function nodeReadableStream(checker, symbol) {
57
+ return checker.getAmbientModules().filter((module) => ["\"stream/web\"", "\"node:stream/web\""].includes(module.name))
58
+ .some((module) => checker.getExportsOfModule(module).some((member) => member.name === "ReadableStream" &&
59
+ (member.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(member) : member) === symbol));
60
+ }
61
+ function argumentsOf(checker, type) {
62
+ return type.flags & ts.TypeFlags.Object && type.objectFlags & ts.ObjectFlags.Reference
63
+ ? checker.getTypeArguments(type) : [];
64
+ }
65
+ function checkValue(checker, type, path, traversal) {
66
+ if (type.flags & scalars || traversal.seen.has(type))
67
+ return;
68
+ if (traversal.depth >= 100)
69
+ reject(checker, type, path, "type expansion is too deep to verify; use a finite recursive data shape");
70
+ traversal.seen.add(type);
71
+ traversal.depth++;
72
+ try {
73
+ checkObject(checker, type, path, traversal);
74
+ }
75
+ finally {
76
+ traversal.depth--;
77
+ }
78
+ }
79
+ function checkObject(checker, type, path, traversal) {
80
+ if (type.isIntersection() && type.types.some((member) => Boolean(member.flags & scalars)) &&
81
+ !type.getCallSignatures().length && !type.getConstructSignatures().length)
82
+ return;
83
+ if (type.isUnionOrIntersection()) {
84
+ for (const member of type.types)
85
+ checkValue(checker, member, path, traversal);
86
+ return;
87
+ }
88
+ if (!(type.flags & ts.TypeFlags.Object))
89
+ reject(checker, type, path);
90
+ if (traversal.workerOnly && type.getSymbol() === traversal.workerOnly) {
91
+ throw new WorkerOnlyMethod(`${path} is ${checker.typeToString(type)}, which stays in the Tokamak worker.`);
92
+ }
93
+ if (type.getCallSignatures().length || type.getConstructSignatures().length)
94
+ reject(checker, type, path);
95
+ const name = builtin(checker, type);
96
+ if (name === "Date" || name === "RegExp" || name === "ArrayBuffer")
97
+ return;
98
+ if (name && binaryViews.has(name))
99
+ return checkBuffer(checker, type, path);
100
+ if (name === "ReadableStream")
101
+ return checkStream(checker, type, path);
102
+ if (checker.isArrayType(type) || checker.isTupleType(type) || name === "ReadonlyArray") {
103
+ checkArguments(checker, type, path, traversal);
104
+ return;
105
+ }
106
+ if (["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(name ?? "")) {
107
+ checkArguments(checker, type, path, traversal);
108
+ return;
109
+ }
110
+ if (isError(checker, type))
111
+ return checkProperties(checker, type, path, traversal, true);
112
+ const classType = type.getSymbol()?.declarations?.some((node) => ts.isClassDeclaration(node) || ts.isClassExpression(node));
113
+ if (name || classType)
114
+ reject(checker, type, path);
115
+ checkProperties(checker, type, path, traversal);
116
+ }
117
+ function isError(checker, type) {
118
+ if (errors.has(builtin(checker, type) ?? ""))
119
+ return true;
120
+ if (!(type.flags & ts.TypeFlags.Object))
121
+ return false;
122
+ const object = type;
123
+ const target = object.objectFlags & ts.ObjectFlags.Reference ? type.target : object;
124
+ return Boolean(target.objectFlags & ts.ObjectFlags.ClassOrInterface) &&
125
+ checker.getBaseTypes(target).some((base) => isError(checker, base));
126
+ }
127
+ function checkArguments(checker, type, path, traversal) {
128
+ for (const [index, argument] of argumentsOf(checker, type).entries()) {
129
+ checkValue(checker, argument, `${path}[${index}]`, traversal);
130
+ }
131
+ }
132
+ function checkBuffer(checker, type, path) {
133
+ const [buffer] = argumentsOf(checker, type);
134
+ // Default ArrayBufferLike views require the runtime's non-shared-buffer check.
135
+ if (buffer && builtin(checker, buffer) === "SharedArrayBuffer")
136
+ reject(checker, type, path, "shared buffers cannot cross RPC");
137
+ if (buffer && buffer.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter))
138
+ reject(checker, buffer, path);
139
+ }
140
+ function checkStream(checker, type, path) {
141
+ const [chunk] = argumentsOf(checker, type);
142
+ if (!chunk || builtin(checker, chunk) !== "Uint8Array") {
143
+ reject(checker, type, path, "readable streams must contain Uint8Array chunks");
144
+ }
145
+ checkBuffer(checker, chunk, path);
146
+ }
147
+ function checkProperties(checker, type, path, traversal, error = false) {
148
+ const properties = type.getProperties();
149
+ const indexes = checker.getIndexInfosOfType(type);
150
+ if (!error && !properties.length && !indexes.length)
151
+ reject(checker, type, path, "an unconstrained object is not a data shape");
152
+ for (const property of properties) {
153
+ if (String(property.escapedName).startsWith("__@"))
154
+ reject(checker, type, path, "symbol properties cannot cross RPC");
155
+ if (error && standardErrorField(checker, property))
156
+ continue;
157
+ checkValue(checker, checker.getTypeOfSymbol(property), `${path}.${property.name}`, traversal);
158
+ }
159
+ for (const index of indexes) {
160
+ if (index.keyType.flags & ts.TypeFlags.ESSymbolLike)
161
+ reject(checker, type, path, "symbol indexes cannot cross RPC");
162
+ checkValue(checker, index.type, `${path}[key]`, traversal);
163
+ }
164
+ }
165
+ function standardErrorField(checker, property) {
166
+ return property.declarations?.some(({ parent }) => (ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) &&
167
+ errors.has(builtin(checker, checker.getTypeAtLocation(parent)) ?? "")) ?? false;
168
+ }
@@ -0,0 +1,9 @@
1
+ import type { NativeService } from "./worker-types.mjs";
2
+ export interface ServiceMethod {
3
+ name: string;
4
+ overloads: number;
5
+ documentation: string;
6
+ }
7
+ /** Retain the original types while making every exposed overload asynchronous. */
8
+ export declare function serviceTypes(instance: string, methods: ServiceMethod[], name?: string): string;
9
+ export declare function writeEnvironmentTypes(root: string, services: NativeService[], main: string, bindings: string[]): void;
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /** Retain the original types while making every exposed overload asynchronous. */
4
+ export function serviceTypes(instance, methods, name = "Tokamak") {
5
+ const counts = [...new Set(methods.map((method) => method.overloads))].sort((a, b) => a - b);
6
+ const helpers = counts.map((count) => {
7
+ const signatures = Array.from({ length: count }, (_, index) => index);
8
+ const match = signatures.map((index) => `(...args: infer A${index}): infer R${index};`);
9
+ const wrapped = signatures.map((index) => `(...args: A${index}): Promise<Awaited<R${index}>>;`);
10
+ return `type Async${count}<T> = T extends {\n ${match.join("\n ")}\n} ? {\n ${wrapped.join("\n ")}\n} : never;`;
11
+ });
12
+ const members = methods.map(({ name, overloads, documentation }) => {
13
+ const comment = documentation ? ` /** ${documentation.replaceAll("*/", "* /").replaceAll("\n", "\n * ")} */\n` : "";
14
+ return `${comment} ${JSON.stringify(name)}: Async${overloads}<Worker[${JSON.stringify(name)}]>;`;
15
+ });
16
+ const operations = methods.map(({ name }) => JSON.stringify(name)).join(" | ") || "never";
17
+ return `type Worker = ${instance};
18
+
19
+ ${helpers.join("\n\n")}
20
+
21
+ export interface ${name}Operations {
22
+ ${members.join("\n")}
23
+ }
24
+
25
+ export interface ${name}Client extends ${name}Operations {
26
+ /** Whether each operation is implemented where the app runs. It does not report permission or promise that a call succeeds. */
27
+ readonly capabilities: Readonly<Record<${operations}, boolean>>;
28
+ }
29
+
30
+ export interface ${name}Service extends ${name}Operations {
31
+ fetch(input: Request | string | URL, init?: RequestInit): Promise<Response>;
32
+ }
33
+ `;
34
+ }
35
+ export function writeEnvironmentTypes(root, services, main, bindings) {
36
+ const file = path.join(root, "tokamak-env.d.ts");
37
+ const previous = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : undefined;
38
+ if (previous !== undefined && !previous.startsWith("// Generated by tokamak.\n")) {
39
+ throw new Error(`${file} is reserved for generated Tokamak types; move the existing declarations to another file.`);
40
+ }
41
+ const clients = services.map(service => `declare module ${JSON.stringify(service.client)} {
42
+ ${service.contract.declarations.trimEnd().split("\n").map(line => line ? ` ${line}` : line).join("\n")}
43
+ const client: TokamakClient;
44
+ export default client;
45
+ }`).join("\n\n");
46
+ const source = `// Generated by tokamak.
47
+ ${clients}
48
+
49
+ declare namespace Cloudflare {
50
+ interface Env {
51
+ ${services.map(service => ` ${service.binding}?: import(${JSON.stringify(service.client)}).TokamakService;`).join("\n")}
52
+ }
53
+ }
54
+
55
+ declare namespace Tokamak {
56
+ ${main.trimEnd().split("\n").map(line => line ? ` ${line}` : line).join("\n")}
57
+ interface Env extends Pick<Cloudflare.Env, Extract<keyof Cloudflare.Env, ${bindings.map(name => JSON.stringify(name)).join(" | ") || "never"}>> {
58
+ MAIN: MainService;
59
+ }
60
+ }
61
+ `;
62
+ if (previous !== source)
63
+ fs.writeFileSync(file, source);
64
+ }
package/dist/vite.d.mts CHANGED
@@ -1,17 +1,6 @@
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[];
1
+ import { type PluginConfig } from "@cloudflare/vite-plugin";
2
+ import type { Plugin } from "vite";
3
+ import { type TokamakOptions } from "./native-worker.mjs";
4
+ export type { TokamakOptions } from "./native-worker.mjs";
5
+ /** Cloudflare's Vite integration, with a separate native worker when run by tok. */
6
+ export declare function tokamak(options?: TokamakOptions, cloudflareOptions?: PluginConfig): Plugin[];