@tokamakdev/tok 0.1.1-beta.114 → 0.1.1-beta.116

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/astro.mjs CHANGED
@@ -8,6 +8,9 @@ import { normalizePath } from "vite";
8
8
  import { experimental_readRawConfig, unstable_readConfig } from "wrangler";
9
9
  import { createNativeWorker } from "./native-worker.mjs";
10
10
  import { createClientPlugin } from "./client.mjs";
11
+ import { writeChanged } from "./worker-graph.mjs";
12
+ /** The Worker entry Astro's Cloudflare adapter builds when the configuration names none. */
13
+ const ASTRO_SERVER = "@astrojs/cloudflare/entrypoints/server";
11
14
  /** Astro's Cloudflare adapter, with Tokamak's native worker and development relay. */
12
15
  export function tokamak(options = {}, cloudflareOptions = {}) {
13
16
  const output = process.env.TOKAMAK_VITE_OUTPUT;
@@ -25,16 +28,11 @@ export function tokamak(options = {}, cloudflareOptions = {}) {
25
28
  config(vite) {
26
29
  const root = path.resolve(vite.root ?? process.cwd());
27
30
  const source = configuration(root, cloudflareOptions.configPath, false);
28
- const raw = source ? experimental_readRawConfig({ config: source }, { useRedirectIfAvailable: false }).rawConfig : {};
29
- const environment = process.env.CLOUDFLARE_ENV;
30
- const selected = source ? unstable_readConfig({ config: source, env: environment }, { hideWarnings: true }) : {};
31
- const main = (environment ? raw.env?.[environment]?.main : undefined)
32
- ?? raw.main ?? "@astrojs/cloudflare/entrypoints/server";
31
+ const selected = source ? readWorker(source).selected : { main: ASTRO_SERVER };
33
32
  client.configure({
34
33
  ...selected,
35
34
  // Type inspection needs the server source, not a client-optimized module.
36
- main: main === "@astrojs/cloudflare/entrypoints/server"
37
- ? fileURLToPath(import.meta.resolve(main)) : main,
35
+ main: selected.main === ASTRO_SERVER ? fileURLToPath(import.meta.resolve(ASTRO_SERVER)) : selected.main,
38
36
  kv_namespaces: [...selected.kv_namespaces ?? [], ...sessionBinding ? [{ binding: sessionBinding }] : []],
39
37
  });
40
38
  },
@@ -85,42 +83,26 @@ export function tokamak(options = {}, cloudflareOptions = {}) {
85
83
  const native = createNativeWorker(options, output, (root, graph) => {
86
84
  const source = configuration(root, cloudflareOptions.configPath);
87
85
  generatedPath = path.resolve(root, generated);
88
- const rawConfig = experimental_readRawConfig({ config: source }, { useRedirectIfAvailable: false }).rawConfig;
89
- const environment = process.env.CLOUDFLARE_ENV;
90
- const selected = unstable_readConfig({ config: source, env: environment }, { hideWarnings: true });
91
- selected.main = (environment ? rawConfig.env?.[environment]?.main : undefined)
92
- ?? rawConfig.main ?? "@astrojs/cloudflare/entrypoints/server";
93
- const main = selected.main;
86
+ const { raw, selected } = readWorker(source);
87
+ const appMain = selected.main;
94
88
  graph.configure(selected);
95
- const environments = [[undefined, rawConfig], ...Object.entries(rawConfig.env ?? {})];
96
- for (const [env, config] of environments) {
89
+ // In development the graph serves main through its own module, which every environment uses.
90
+ const servedMain = selected.main === appMain ? undefined : selected.main;
91
+ for (const [env, config] of [[undefined, raw], ...Object.entries(raw.env ?? {})]) {
97
92
  const resolved = unstable_readConfig({ config: source, env }, { hideWarnings: true });
98
- if (!resolved.name)
99
- throw new Error("Tokamak requires a name in the main Worker's configuration.");
100
- const services = graph.services(resolved.name);
101
- const reserved = config.services?.find(binding => services.some(service => service.binding === binding.binding));
102
- if (reserved) {
103
- throw new Error(`${reserved.binding} is reserved for a generated native worker binding.`);
104
- }
105
- config.services = [...config.services ?? [], ...services];
106
- if (selected.main !== main)
107
- config.main = selected.main;
93
+ config.services = [...config.services ?? [], ...graph.serviceBindings(resolved)];
94
+ if (servedMain)
95
+ config.main = servedMain;
108
96
  }
109
97
  // Keeping the generated configuration beside its source preserves relative paths and .dev.vars.
110
- const content = `${JSON.stringify(rawConfig, null, 2)}\n`;
111
- if (!fs.existsSync(generatedPath) || fs.readFileSync(generatedPath, "utf8") !== content) {
112
- fs.writeFileSync(generatedPath, content);
113
- }
98
+ writeChanged(generatedPath, `${JSON.stringify(raw, null, 2)}\n`);
114
99
  process.off("exit", cleanup);
115
100
  process.once("exit", cleanup);
116
101
  });
117
102
  const adapter = cloudflare({
118
103
  ...cloudflareOptions,
119
104
  configPath: generated,
120
- auxiliaryWorkers: [{
121
- viteEnvironment: { name: "tokamak" },
122
- config: (worker, context) => native.graph().native(worker, context),
123
- }],
105
+ auxiliaryWorkers: [native.auxiliaryWorker],
124
106
  });
125
107
  return {
126
108
  ...adapter,
@@ -149,6 +131,14 @@ export function tokamak(options = {}, cloudflareOptions = {}) {
149
131
  },
150
132
  };
151
133
  }
134
+ /** The Wrangler configuration at `source`: as written, and resolved for `CLOUDFLARE_ENV` with Astro's server as its default main. */
135
+ function readWorker(source) {
136
+ const raw = experimental_readRawConfig({ config: source }, { useRedirectIfAvailable: false }).rawConfig;
137
+ const env = process.env.CLOUDFLARE_ENV;
138
+ const selected = unstable_readConfig({ config: source, env }, { hideWarnings: true });
139
+ selected.main = (env ? raw.env?.[env]?.main : undefined) ?? raw.main ?? ASTRO_SERVER;
140
+ return { raw, selected };
141
+ }
152
142
  function configuration(root, configured, required = true) {
153
143
  if (configured)
154
144
  return path.resolve(root, configured);
package/dist/client.mjs CHANGED
@@ -88,13 +88,11 @@ export default Object.assign(client, { capabilities: Object.freeze(Object.fromEn
88
88
  if (!files.includes(file))
89
89
  return;
90
90
  contracts.refresh({ file, timestamp });
91
- for (const id of clients) {
92
- const client = this.environment.moduleGraph.getModuleById(id);
93
- if (!client)
94
- continue;
91
+ const loaded = [...clients].map(id => this.environment.moduleGraph.getModuleById(id)).filter(client => client !== undefined);
92
+ for (const client of loaded)
95
93
  this.environment.moduleGraph.invalidateModule(client);
94
+ if (loaded.length)
96
95
  this.environment.hot.send({ type: "full-reload" });
97
- }
98
96
  },
99
97
  };
100
98
  }
@@ -17,8 +17,9 @@ const CONNECT_TIMEOUT = 10_000;
17
17
  export async function openDevSocket(workerName, token) {
18
18
  const worker = new DevWorker(workerName);
19
19
  const device = new Device();
20
+ const authorized = (request) => token !== undefined && request.headers[SESSION_HEADER] === token;
20
21
  const server = http.createServer((request, response) => {
21
- if (!token || request.headers[SESSION_HEADER] !== token) {
22
+ if (!authorized(request)) {
22
23
  response.writeHead(401).end();
23
24
  return;
24
25
  }
@@ -34,7 +35,7 @@ export async function openDevSocket(workerName, token) {
34
35
  });
35
36
  const sockets = new WebSocketServer({ noServer: true });
36
37
  server.on("upgrade", (request, socket, head) => {
37
- if (!token || request.headers[SESSION_HEADER] !== token) {
38
+ if (!authorized(request)) {
38
39
  socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
39
40
  return;
40
41
  }
@@ -127,7 +128,7 @@ function serve(websocket, worker, device) {
127
128
  }
128
129
  const { header, payload } = message;
129
130
  if (header.type === "result" && header.id !== undefined) {
130
- device.receive(header.id, { payload, failed: header.failed === true, done: header.done === true });
131
+ device.receive(header.id, pluginResult(message));
131
132
  return;
132
133
  }
133
134
  if (header.type === "pull" && header.id !== undefined) {
@@ -177,14 +178,14 @@ async function forward(device, request, response) {
177
178
  response.end(frame);
178
179
  }, async () => {
179
180
  const next = await frames.next().catch(() => ({ done: true, value: undefined }));
180
- return next.done ? failure("DataCloneError", "The call's body ended early") : chunkResult(next.value);
181
+ return next.done ? failure("DataCloneError", "The call's body ended early") : pluginResult(next.value);
181
182
  });
182
183
  response.once("close", () => {
183
184
  if (!response.writableEnded)
184
185
  cancel();
185
186
  });
186
187
  }
187
- function chunkResult({ header, payload }) {
188
+ function pluginResult({ header, payload }) {
188
189
  return { payload, failed: header.failed === true, done: header.done === true };
189
190
  }
190
191
  /** The most a subscription's response holds while the Worker is not reading it. */
@@ -1,3 +1,4 @@
1
+ import type { PluginConfig } from "@cloudflare/vite-plugin";
1
2
  import { type Plugin } from "vite";
2
3
  import type { Config } from "./index.mjs";
3
4
  import { type WorkerGraph } from "./worker-graph.mjs";
@@ -5,8 +6,11 @@ export type TokamakOptions = Config & {
5
6
  /** Native worker entrypoint, relative to the Vite root; defaults to src/tokamak.ts or .js. */
6
7
  module?: string;
7
8
  };
9
+ type AuxiliaryWorker = NonNullable<PluginConfig["auxiliaryWorkers"]>[number];
8
10
  /** The native worker and its development relay. */
9
11
  export declare function createNativeWorker(options: TokamakOptions, output: string, prepare?: (root: string, graph: WorkerGraph) => void | Promise<void>): {
10
12
  plugin: Plugin;
11
13
  graph(): WorkerGraph;
14
+ auxiliaryWorker: AuxiliaryWorker;
12
15
  };
16
+ export {};
@@ -47,7 +47,7 @@ export function createNativeWorker(options, output, prepare) {
47
47
  async handler(server) {
48
48
  const devSocket = await serveNativeWorker(server, graph.names(), output, token);
49
49
  if (devSocket && token)
50
- graph.relay(devSocket.port, token, platform);
50
+ graph.relay({ port: devSocket.port, token, platform });
51
51
  // A page loaded from this server, not the device, still gets the device's capabilities.
52
52
  server.middlewares.use(CAPABILITIES, (_request, response) => {
53
53
  const capabilities = capabilitiesFor(graph.availability(), platform);
@@ -57,7 +57,12 @@ export function createNativeWorker(options, output, prepare) {
57
57
  },
58
58
  },
59
59
  };
60
- return { plugin, graph: () => graph };
60
+ return {
61
+ plugin,
62
+ graph: () => graph,
63
+ // The graph exists once Vite's config is known, after Cloudflare's plugin is created.
64
+ auxiliaryWorker: { viteEnvironment: { name: "tokamak" }, config: (worker, context) => graph.native(worker, context) },
65
+ };
61
66
  }
62
67
  async function serveNativeWorker(server, names, output, token) {
63
68
  const httpServer = server.httpServer;
package/dist/native.d.mts CHANGED
@@ -1,11 +1,17 @@
1
1
  import type { NativePlatform } from "@tokamakdev/plugin/capabilities";
2
+ /** The device relay a development Worker reaches native code through. */
3
+ export interface DeviceRelay {
4
+ port: number;
5
+ token: string;
6
+ platform?: NativePlatform;
7
+ }
2
8
  /**
3
- * The development invocation `ctx`'s native connection, which reaches the byte endpoints of a
4
- * device on `platform` through the relay on `port`. Its subscriptions are `ctx`'s `waitUntil` work.
9
+ * The development invocation `ctx`'s native connection, which reaches the byte endpoints of the
10
+ * `relay`'s device. Its subscriptions are `ctx`'s `waitUntil` work.
5
11
  */
6
- export declare function connectDevice(port: number, token: string, ctx: {
12
+ export declare function connectDevice({ port, token, platform }: DeviceRelay, ctx: {
7
13
  waitUntil(promise: Promise<unknown>): void;
8
- }, platform?: NativePlatform): {
14
+ }): {
9
15
  platform: NativePlatform | undefined;
10
16
  call(plugin: string, method: string, payload: Uint8Array, { body, signal }?: {
11
17
  body?: ReadableStream<Uint8Array>;
package/dist/native.mjs CHANGED
@@ -2,10 +2,10 @@ import { decodeMessage, encodeMessage, messages, nativeError } from "@tokamakdev
2
2
  const encoder = new TextEncoder();
3
3
  const decoder = new TextDecoder();
4
4
  /**
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.
5
+ * The development invocation `ctx`'s native connection, which reaches the byte endpoints of the
6
+ * `relay`'s device. Its subscriptions are `ctx`'s `waitUntil` work.
7
7
  */
8
- export function connectDevice(port, token, ctx, platform) {
8
+ export function connectDevice({ port, token, platform }, ctx) {
9
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",
@@ -1,9 +1,9 @@
1
+ import type { NativePlatform } from "@tokamakdev/plugin/capabilities";
2
+ export type { NativePlatform };
1
3
  export interface ModuleExport {
2
4
  module: string;
3
5
  export: string;
4
6
  }
5
- /** A native platform a plugin builds for. */
6
- export type NativePlatform = "ios" | "macos" | "android" | "windows" | "linux";
7
7
  /** Where an operation runs, and what it needs there. */
8
8
  export interface Operation {
9
9
  /** The native platforms that implement it, with any minimum OS version. */
@@ -7,3 +7,5 @@ export declare class WorkerOnlyMethod extends Error {
7
7
  * `workerOnly` type throws `WorkerOnlyMethod`.
8
8
  */
9
9
  export declare function checkRpcMethod(checker: ts.TypeChecker, method: ts.Type, name: string, workerOnly?: ts.Symbol): void;
10
+ /** The symbol that `symbol` names: itself, or the declaration an import or re-export refers to. */
11
+ export declare function aliased(checker: ts.TypeChecker, symbol: ts.Symbol): ts.Symbol;
@@ -56,7 +56,11 @@ function builtin(checker, type) {
56
56
  function nodeReadableStream(checker, symbol) {
57
57
  return checker.getAmbientModules().filter((module) => ["\"stream/web\"", "\"node:stream/web\""].includes(module.name))
58
58
  .some((module) => checker.getExportsOfModule(module).some((member) => member.name === "ReadableStream" &&
59
- (member.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(member) : member) === symbol));
59
+ aliased(checker, member) === symbol));
60
+ }
61
+ /** The symbol that `symbol` names: itself, or the declaration an import or re-export refers to. */
62
+ export function aliased(checker, symbol) {
63
+ return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
60
64
  }
61
65
  function argumentsOf(checker, type) {
62
66
  return type.flags & ts.TypeFlags.Object && type.objectFlags & ts.ObjectFlags.Reference
package/dist/vite.mjs CHANGED
@@ -19,17 +19,14 @@ export function tokamak(options = {}, cloudflareOptions = {}) {
19
19
  ...worker, ...custom,
20
20
  services: [...custom?.services ?? [], ...worker.services ?? []],
21
21
  };
22
- client.configure(configured);
23
- if (!native)
22
+ if (!native) {
23
+ client.configure(configured);
24
24
  return custom;
25
- const graph = native.graph();
26
- graph.configure(configured);
25
+ }
26
+ const generated = native.graph().configure(configured);
27
27
  // Cloudflare merges returned service entries with those already in the config.
28
- return { ...custom, main: configured.main, services: [...custom?.services ?? [], ...graph.services(graph.names().main)] };
28
+ return { ...custom, main: configured.main, services: [...custom?.services ?? [], ...generated] };
29
29
  },
30
- auxiliaryWorkers: native ? [{
31
- viteEnvironment: { name: "tokamak" },
32
- config: (worker, context) => native.graph().native(worker, context),
33
- }] : cloudflareOptions.auxiliaryWorkers,
30
+ auxiliaryWorkers: native ? [native.auxiliaryWorker] : cloudflareOptions.auxiliaryWorkers,
34
31
  })];
35
32
  }
@@ -11,6 +11,8 @@ export interface PluginContract extends WorkerContract {
11
11
  /** Every operation of the native SDK, including those only the Tokamak worker can call. */
12
12
  operations: string[];
13
13
  }
14
+ /** The contract of a Tokamak worker without a module. */
15
+ export declare function emptyContract(): WorkerContract;
14
16
  export declare function findModule(root: string, name: string | undefined): string | undefined;
15
17
  /** Resolve the application's types before generating its service adapters. */
16
18
  export declare function readWorkerContract(entry: string, root: string): WorkerContract;
@@ -2,8 +2,14 @@ import path from "node:path";
2
2
  import fs from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import ts from "typescript";
5
- import { checkRpcMethod, WorkerOnlyMethod } from "./rpc-contract.mjs";
5
+ import { aliased, checkRpcMethod, WorkerOnlyMethod } from "./rpc-contract.mjs";
6
6
  import { serviceTypes } from "./service-types.mjs";
7
+ /** Worker handler names, which are not RPC methods. */
8
+ const HANDLERS = ["fetch", "event", "queue", "scheduled", "email", "tail", "trace", "tailStream", "test"];
9
+ /** The contract of a Tokamak worker without a module. */
10
+ export function emptyContract() {
11
+ return { methods: [], files: [], declarations: serviceTypes("{}", []) };
12
+ }
7
13
  export function findModule(root, name) {
8
14
  const candidates = (name ? [name] : ["src/tokamak.ts", "src/tokamak.js"])
9
15
  .map(candidate => path.resolve(root, candidate).split(path.sep).join("/"));
@@ -27,8 +33,7 @@ export function readWorkerContracts(entry, main, root, validate, plugins = []) {
27
33
  const project = entries.length ? workerProgram(entries, root) : undefined;
28
34
  const native = entry && project ? workerModule(entry, project) : undefined;
29
35
  return {
30
- native: native ? classContract(native, "default", true, validate)
31
- : { methods: [], files: [], declarations: serviceTypes("{}", []) },
36
+ native: native ? classContract(native, "default", true, validate) : emptyContract(),
32
37
  named: native ? namedContracts(native, validate) : {},
33
38
  main: main && project ? serviceContracts(workerModule(main, project), validate) : {},
34
39
  plugins: project ? plugins.map(plugin => pluginContract(workerModule(plugin.api.module, project), plugin, validate)) : [],
@@ -95,8 +100,7 @@ function workerModule(entry, project) {
95
100
  throw new Error(`Cannot read Tokamak entrypoint ${entry}.`);
96
101
  const module = checker.getSymbolAtLocation(source);
97
102
  const exports = new Map((module ? checker.getExportsOfModule(module) : []).map(exported => {
98
- const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
99
- return [exported.name, checker.getTypeOfSymbolAtLocation(symbol, source)];
103
+ return [exported.name, checker.getTypeOfSymbolAtLocation(aliased(checker, exported), source)];
100
104
  }));
101
105
  return { ...project, entry, source, exports };
102
106
  }
@@ -126,7 +130,7 @@ function pluginContract(module, definition, validate) {
126
130
  if (property.name === "capabilities")
127
131
  continue;
128
132
  assertMethodName(property.name);
129
- if (["fetch", "event", "queue", "scheduled", "email", "tail", "trace", "tailStream", "test"].includes(property.name)) {
133
+ if (HANDLERS.includes(property.name)) {
130
134
  throw new Error(`Plugin method ${property.name} is reserved for a Worker handler.`);
131
135
  }
132
136
  const method = checker.getTypeOfSymbolAtLocation(property, source);
@@ -146,7 +150,7 @@ function pluginContract(module, definition, validate) {
146
150
  }
147
151
  methods.push(methodDeclaration(checker, property, method));
148
152
  }
149
- methods.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
153
+ methods.sort(byName);
150
154
  const unknown = Object.keys(definition.operations).find(name => !operations.includes(name));
151
155
  if (unknown)
152
156
  throw new Error(`${definition.manifest}: operations.${unknown} names no method of ${definition.api.export}.`);
@@ -244,13 +248,16 @@ function classContract(module, name, native, validate) {
244
248
  checkRpcMethod(checker, type, property.name);
245
249
  methods.push(methodDeclaration(checker, property, type));
246
250
  }
247
- methods.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
251
+ methods.sort(byName);
248
252
  return {
249
253
  methods: methods.map((method) => method.name),
250
254
  declarations: serviceTypes(`InstanceType<typeof import(${JSON.stringify(entry.split(path.sep).join("/"))}, { with: { "resolution-mode": "import" } })[${JSON.stringify(name)}]>`, methods, native ? "Tokamak" : "Main"),
251
255
  files: module.files,
252
256
  };
253
257
  }
258
+ function byName(left, right) {
259
+ return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
260
+ }
254
261
  function methodDeclaration(checker, property, type) {
255
262
  return {
256
263
  name: property.name,
@@ -264,7 +271,7 @@ function methodDeclaration(checker, property, type) {
264
271
  function applicationPrototypes(program, checker, instance, entry, allowNative = false) {
265
272
  const module = checker.getAmbientModules().find((symbol) => symbol.name === '"cloudflare:workers"');
266
273
  const exported = module && checker.getExportsOfModule(module).find((symbol) => symbol.name === "WorkerEntrypoint");
267
- const base = exported && (exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported);
274
+ const base = exported && aliased(checker, exported);
268
275
  const native = allowNative ? nativeSdkExport(program, checker, entry, "TokamakEntrypoint") : undefined;
269
276
  const prototypes = new Set();
270
277
  let current = instance;
@@ -288,7 +295,7 @@ function nativeSdkExport(program, checker, entry, name, specifier = "@tokamakdev
288
295
  const source = resolved && program.getSourceFile(resolved.resolvedFileName);
289
296
  const module = source && checker.getSymbolAtLocation(source);
290
297
  const exported = module && checker.getExportsOfModule(module).find(symbol => symbol.name === name);
291
- return exported && (exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported);
298
+ return exported && aliased(checker, exported);
292
299
  }
293
300
  function exposedMethod(property) {
294
301
  const declarations = property.declarations?.filter(ts.isMethodDeclaration) ?? [];
@@ -299,7 +306,7 @@ function exposedMethod(property) {
299
306
  throw new Error(`Method ${property.name} is on the public JavaScript prototype; use a #private method for a local helper.`);
300
307
  }
301
308
  assertMethodName(property.name);
302
- return !["fetch", "event", "queue", "scheduled", "email", "tail", "trace", "tailStream", "test"].includes(property.name);
309
+ return !HANDLERS.includes(property.name);
303
310
  }
304
311
  function assertMethodName(name) {
305
312
  if (["then", "connect", "dup", "constructor", "alarm", "webSocketMessage", "webSocketClose", "webSocketError", "__tokamakRpc"].includes(name)) {
@@ -1,28 +1,29 @@
1
1
  import type { PluginConfig } from "@cloudflare/vite-plugin";
2
2
  import { type EnvironmentOptions, type Plugin } from "vite";
3
- import type { Availability, NativePlatform } from "@tokamakdev/plugin/capabilities";
4
- import type { WorkerContract } from "./worker-contract.mjs";
3
+ import type { Availability } from "@tokamakdev/plugin/capabilities";
4
+ import type { DeviceRelay } from "./native.mjs";
5
5
  import { WorkerTypes, type WorkerConfiguration } from "./worker-types.mjs";
6
6
  type ServiceConfiguration = Pick<WorkerConfiguration, "name" | "topLevelName" | "services" | "main" | "configPath" | "vars" | "kv_namespaces" | "d1_databases" | "r2_buckets">;
7
+ type ServiceBindings = NonNullable<ServiceConfiguration["services"]>;
7
8
  type Auxiliary = NonNullable<PluginConfig["auxiliaryWorkers"]>[number];
8
9
  type AuxiliaryCustomizer = Extract<Extract<Auxiliary, {
9
10
  config: unknown;
10
11
  }>["config"], (...args: never[]) => unknown>;
11
12
  export interface WorkerGraph {
12
- generated: string;
13
- /** Connects development invocations to a device on `platform` through the relay on `port`. */
14
- relay(port: number, token: string, platform: NativePlatform | undefined): void;
13
+ /** Connects development invocations to a device through `relay`. */
14
+ relay(relay: DeviceRelay): void;
15
15
  /** Each plugin operation's native platforms, by plugin ID. */
16
16
  availability(): Availability;
17
- configure(config: ServiceConfiguration): void;
18
- services(name: string): NonNullable<ServiceConfiguration["services"]>;
17
+ /** Configures the main Worker `config`, returning the generated service bindings it adds. */
18
+ configure(config: ServiceConfiguration): ServiceBindings;
19
+ /** The generated service bindings of the main Worker configuration `config`, once it binds none of their names. */
20
+ serviceBindings(config: ServiceConfiguration): ServiceBindings;
19
21
  native: AuxiliaryCustomizer;
20
22
  names(): {
21
23
  main: string;
22
24
  native: string;
23
25
  app: string;
24
26
  };
25
- contract(): WorkerContract;
26
27
  types: WorkerTypes;
27
28
  cloudflare: PluginConfig;
28
29
  plugin: Plugin & {
@@ -40,4 +41,6 @@ export interface WorkerGraph {
40
41
  /** Configure the two bundles through Cloudflare's public auxiliary-worker API. */
41
42
  export declare function createWorkerGraph(root: string, entry: string | undefined, output: string): WorkerGraph;
42
43
  export declare function workerName(main: string): string;
44
+ /** Writes `source` to `file` unless it already holds it, so watchers see only changes. */
45
+ export declare function writeChanged(file: string, source: string): void;
43
46
  export {};
@@ -6,6 +6,8 @@ import { normalizePath } from "vite";
6
6
  import { WorkerTypes } from "./worker-types.mjs";
7
7
  const NATIVE_CONNECTION = "virtual:tokamak/native-connection";
8
8
  const NATIVE_CALLS = normalizePath(fileURLToPath(new URL("./native.mjs", import.meta.url)));
9
+ /** The two Workers' roles: the app's main Worker and the Tokamak worker. */
10
+ const ROLES = ["main", "native"];
9
11
  /** Configure the two bundles through Cloudflare's public auxiliary-worker API. */
10
12
  export function createWorkerGraph(root, entry, output) {
11
13
  const generated = path.join(output, "worker.mjs");
@@ -56,7 +58,8 @@ export function createWorkerGraph(root, entry, output) {
56
58
  export const TokamakEvents = eventEntrypoint(Worker, nativeContext);\n`);
57
59
  writeChanged(path.join(output, "methods.json"), `${JSON.stringify(contracts.native.methods)}\n`);
58
60
  }
59
- const configure = (config) => {
61
+ /** The name of the main Worker configuration `config`, which binds none of the generated names. */
62
+ const checkedName = (config) => {
60
63
  if (!config.name)
61
64
  throw new Error("Tokamak requires a name in the main Worker's configuration.");
62
65
  const reserved = new Set(["MAIN", "TOKAMAK_RUNTIME", ...services(config.name).map(service => service.binding)]);
@@ -64,15 +67,20 @@ export const TokamakEvents = eventEntrypoint(Worker, nativeContext);\n`);
64
67
  if (collision) {
65
68
  throw new Error(`${collision} is reserved for a generated native worker binding.`);
66
69
  }
67
- mainName = config.name;
70
+ return config.name;
71
+ };
72
+ const configure = (config) => {
73
+ mainName = checkedName(config);
68
74
  nativeName = workerName(mainName);
69
75
  appName = config.topLevelName ?? mainName;
70
76
  contracts.configure(config);
71
77
  if (dev)
72
78
  config.main = serviceModule("main");
73
- config.services = [...config.services ?? [], ...services(mainName)];
79
+ const generated = services(mainName);
80
+ config.services = [...config.services ?? [], ...generated];
74
81
  mainServices = config.services;
75
82
  configuredEntrypoints = entrypoints();
83
+ return generated;
76
84
  };
77
85
  const native = (_config, { entryWorkerConfig }) => {
78
86
  if (!mainName)
@@ -95,17 +103,15 @@ export const TokamakEvents = eventEntrypoint(Worker, nativeContext);\n`);
95
103
  };
96
104
  refresh();
97
105
  return {
98
- generated: normalizePath(generated),
99
- relay(port, token, platform) { relay = { port, token, platform }; },
106
+ relay(configured) { relay = configured; },
100
107
  availability: () => availability(contracts.plugins),
101
108
  configure,
102
- services,
109
+ serviceBindings: config => services(checkedName(config)),
103
110
  native,
104
111
  names: () => ({ main: mainName, native: nativeName, app: appName }),
105
- contract: () => contracts.native,
106
112
  types: contracts,
107
113
  cloudflare: {
108
- config: configure,
114
+ config: config => { configure(config); },
109
115
  auxiliaryWorkers: [{
110
116
  viteEnvironment: { name: "tokamak" },
111
117
  config: native,
@@ -171,20 +177,16 @@ export const TokamakEvents = eventEntrypoint(Worker, nativeContext);\n`);
171
177
  return [];
172
178
  },
173
179
  resolveId(id) {
174
- if (id === NATIVE_CONNECTION)
175
- return `\0${NATIVE_CONNECTION}`;
180
+ if ([NATIVE_CONNECTION, serviceModule("main"), serviceModule("native")].includes(id))
181
+ return `\0${id}`;
176
182
  if (id.startsWith("\0tokamak:") || id.startsWith("\0virtual:tokamak/"))
177
183
  return id;
178
- const role = id === serviceModule("native") ? "native" : id === serviceModule("main") ? "main" : undefined;
179
- if (!role)
180
- return;
181
- return `\0${serviceModule(role)}`;
182
184
  },
183
185
  async load(id) {
184
186
  if (id === `\0${NATIVE_CONNECTION}`)
185
187
  return nativeConnectionModule(dev ? relay : undefined);
186
- const role = id.endsWith(":main") || id.endsWith("/main") ? "main" : "native";
187
- if (id !== bindingsModule(role) && id !== `\0${serviceModule(role)}`)
188
+ const role = ROLES.find(role => id === bindingsModule(role) || id === `\0${serviceModule(role)}`);
189
+ if (!role)
188
190
  return;
189
191
  await contracts.resolve(source => this.resolve(source, undefined, { skipSelf: true }));
190
192
  refresh();
@@ -253,7 +255,7 @@ function nativeConnectionModule(relay) {
253
255
  if (!relay)
254
256
  return "export const connect = () => undefined;";
255
257
  return `import { connectDevice } from ${JSON.stringify(NATIVE_CALLS)};
256
- export const connect = ctx => connectDevice(${String(relay.port)}, ${JSON.stringify(relay.token)}, ctx, ${JSON.stringify(relay.platform ?? null)} ?? undefined);`;
258
+ export const connect = ctx => connectDevice(${JSON.stringify(relay)}, ctx);`;
257
259
  }
258
260
  /** Each plugin operation's native platforms: those its manifest names, or every one the plugin builds for. */
259
261
  function availability(plugins) {
@@ -325,7 +327,8 @@ export default class extends WorkerEntrypoint {
325
327
  ${named.join("")}
326
328
  `;
327
329
  }
328
- function writeChanged(file, source) {
330
+ /** Writes `source` to `file` unless it already holds it, so watchers see only changes. */
331
+ export function writeChanged(file, source) {
329
332
  if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === source)
330
333
  return;
331
334
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -1,13 +1,13 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { readWorkerContracts } from "./worker-contract.mjs";
3
+ import { emptyContract, readWorkerContracts } from "./worker-contract.mjs";
4
4
  import { serviceTypes, writeEnvironmentTypes } from "./service-types.mjs";
5
5
  import { discoverPlugins } from "./plugin-manifest.mjs";
6
6
  /** Shared contracts for the two entrypoints and their generated callers. */
7
7
  export class WorkerTypes {
8
8
  root;
9
9
  entry;
10
- native = { methods: [], files: [], declarations: serviceTypes("{}", []) };
10
+ native = emptyContract();
11
11
  /** The Tokamak worker's named entrypoints. */
12
12
  named = {};
13
13
  main = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamakdev/tok",
3
- "version": "0.1.1-beta.114",
3
+ "version": "0.1.1-beta.116",
4
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",
@@ -43,7 +43,7 @@
43
43
  "node": ">=18"
44
44
  },
45
45
  "dependencies": {
46
- "@tokamakdev/plugin": "0.1.0-beta.114",
46
+ "@tokamakdev/plugin": "0.1.0-beta.116",
47
47
  "typescript": "5.9.3",
48
48
  "ws": "^8.22.0"
49
49
  },
@@ -75,17 +75,17 @@
75
75
  "wrangler": "4.147.0"
76
76
  },
77
77
  "optionalDependencies": {
78
- "@tokamakdev/platform-android-arm64": "0.1.1-beta.114",
79
- "@tokamakdev/platform-ios-arm64": "0.1.1-beta.114",
80
- "@tokamakdev/platform-ios-simulator-arm64": "0.1.1-beta.114",
81
- "@tokamakdev/platform-ios-simulator-x64": "0.1.1-beta.114",
82
- "@tokamakdev/platform-macos-arm64": "0.1.1-beta.114",
83
- "@tokamakdev/platform-macos-x64": "0.1.1-beta.114",
84
- "@tokamakdev/platform-windows-x64": "0.1.1-beta.114",
85
- "@tokamakdev/tok-darwin-arm64": "0.1.1-beta.114",
86
- "@tokamakdev/tok-darwin-x64": "0.1.1-beta.114",
87
- "@tokamakdev/tok-linux-x64": "0.1.1-beta.114",
88
- "@tokamakdev/tok-win32-x64": "0.1.1-beta.114"
78
+ "@tokamakdev/platform-android-arm64": "0.1.1-beta.116",
79
+ "@tokamakdev/platform-ios-arm64": "0.1.1-beta.116",
80
+ "@tokamakdev/platform-ios-simulator-arm64": "0.1.1-beta.116",
81
+ "@tokamakdev/platform-ios-simulator-x64": "0.1.1-beta.116",
82
+ "@tokamakdev/platform-macos-arm64": "0.1.1-beta.116",
83
+ "@tokamakdev/platform-macos-x64": "0.1.1-beta.116",
84
+ "@tokamakdev/platform-windows-x64": "0.1.1-beta.116",
85
+ "@tokamakdev/tok-darwin-arm64": "0.1.1-beta.116",
86
+ "@tokamakdev/tok-darwin-x64": "0.1.1-beta.116",
87
+ "@tokamakdev/tok-linux-x64": "0.1.1-beta.116",
88
+ "@tokamakdev/tok-win32-x64": "0.1.1-beta.116"
89
89
  },
90
90
  "publishConfig": {
91
91
  "access": "public"