@allwright.dev/core 0.0.37 → 0.0.38

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.
@@ -1,2 +1,2 @@
1
- export declare function ensureRuntimeReady(serverAddr: string): Promise<void>;
1
+ export declare function ensureRuntimeReady(serverAddr: string): Promise<string>;
2
2
  export declare function shutdownManagedServer(): Promise<void>;
package/dist/bootstrap.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import net from "node:net";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { spawn, spawnSync } from "node:child_process";
@@ -18,24 +19,42 @@ const PROTO_ROOT = fileURLToPath(new URL("../proto/", import.meta.url));
18
19
  const ENGINE_PROTO_PATH = fileURLToPath(new URL("../proto/engine/v1/engine.proto", import.meta.url));
19
20
  let managedServer = null;
20
21
  let managedServerAddr = null;
22
+ let managedServerBaseAddr = null;
21
23
  export async function ensureRuntimeReady(serverAddr) {
22
- if (await pingServer(serverAddr)) {
23
- return;
24
+ const expectedVersion = expectedRuntimeVersion();
25
+ const status = await pingServer(serverAddr);
26
+ if (status) {
27
+ if (status.version === expectedVersion) {
28
+ return serverAddr;
29
+ }
30
+ if (!isLocalServerAddr(serverAddr)) {
31
+ throw new Error(`allwright server at ${serverAddr} is running version ${displayVersion(status.version)} but this client expects ${expectedVersion}`);
32
+ }
24
33
  }
25
34
  if (!isLocalServerAddr(serverAddr)) {
26
35
  throw new Error(`allwright could not reach engine server at ${serverAddr}. Automatic startup is only supported for local addresses.`);
27
36
  }
28
- if (managedServer && !managedServer.killed && managedServer.exitCode === null && managedServerAddr === serverAddr) {
29
- await waitForServer(serverAddr);
30
- return;
37
+ if (managedServer && !managedServer.killed && managedServer.exitCode === null && managedServerBaseAddr === serverAddr && managedServerAddr) {
38
+ return waitForServer(managedServerAddr, expectedVersion);
39
+ }
40
+ if (managedServer && !managedServer.killed && managedServer.exitCode === null) {
41
+ managedServer.kill("SIGTERM");
42
+ managedServer = null;
43
+ managedServerAddr = null;
44
+ managedServerBaseAddr = null;
45
+ }
46
+ const cliPath = await ensureCliAvailable(expectedVersion);
47
+ ensureWebPlugin(cliPath, expectedVersion);
48
+ let resolvedServerAddr = serverAddr;
49
+ if (status && status.version !== expectedVersion) {
50
+ resolvedServerAddr = await allocateManagedServerAddr(serverAddr);
31
51
  }
32
- const cliPath = await ensureCliAvailable();
33
- ensureWebPlugin(cliPath);
34
- managedServer = spawn(cliPath, ["serve", "--listen-addr", cliListenAddr(serverAddr)], {
52
+ managedServer = spawn(cliPath, ["serve", "--listen-addr", cliListenAddr(resolvedServerAddr)], {
35
53
  stdio: "ignore",
36
54
  });
37
- managedServerAddr = serverAddr;
38
- await waitForServer(serverAddr);
55
+ managedServerAddr = resolvedServerAddr;
56
+ managedServerBaseAddr = serverAddr;
57
+ return waitForServer(resolvedServerAddr, expectedVersion);
39
58
  }
40
59
  export async function shutdownManagedServer() {
41
60
  if (managedServer && managedServer.exitCode === null && !managedServer.killed) {
@@ -43,17 +62,19 @@ export async function shutdownManagedServer() {
43
62
  }
44
63
  managedServer = null;
45
64
  managedServerAddr = null;
65
+ managedServerBaseAddr = null;
46
66
  }
47
- async function waitForServer(serverAddr) {
67
+ async function waitForServer(serverAddr, expectedVersion) {
48
68
  const deadline = Date.now() + STARTUP_TIMEOUT_MS;
49
69
  while (Date.now() < deadline) {
50
- if (await pingServer(serverAddr)) {
51
- return;
70
+ const status = await pingServer(serverAddr);
71
+ if (status?.version === expectedVersion) {
72
+ return serverAddr;
52
73
  }
53
74
  await new Promise((resolve) => setTimeout(resolve, 250));
54
75
  }
55
76
  await shutdownManagedServer();
56
- throw new Error(`timed out waiting for allwright server at ${serverAddr} to become ready`);
77
+ throw new Error(`timed out waiting for allwright server at ${serverAddr} to become ready with version ${expectedVersion}`);
57
78
  }
58
79
  async function pingServer(serverAddr) {
59
80
  const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
@@ -67,23 +88,31 @@ async function pingServer(serverAddr) {
67
88
  const proto = grpc.loadPackageDefinition(loaded);
68
89
  const client = new proto.allwright.engine.v1.EngineService(serverAddr, grpc.credentials.createInsecure());
69
90
  return await new Promise((resolve) => {
70
- client.Ping({}, new grpc.Metadata(), { deadline: new Date(Date.now() + PING_TIMEOUT_MS) }, (error) => {
91
+ client.Ping({}, new grpc.Metadata(), { deadline: new Date(Date.now() + PING_TIMEOUT_MS) }, (error, response) => {
71
92
  client.close();
72
- resolve(!error);
93
+ if (error) {
94
+ resolve(null);
95
+ return;
96
+ }
97
+ resolve({ version: normalizeReleaseVersion(response.version ?? "") });
73
98
  });
74
99
  });
75
100
  }
76
- async function ensureCliAvailable() {
101
+ async function ensureCliAvailable(expectedVersion) {
77
102
  const envPath = process.env[ALLWRIGHT_CLI_PATH_ENV_VAR]?.trim();
78
- if (envPath && isFile(envPath)) {
103
+ if (envPath && isFile(envPath) && cliVersionMatches(envPath, expectedVersion)) {
79
104
  return envPath;
80
105
  }
81
106
  const bundled = path.join(allwrightHome(), "bin", cliFilename());
82
- if (isFile(bundled)) {
107
+ if (isFile(bundled) && cliVersionMatches(bundled, expectedVersion)) {
83
108
  return bundled;
84
109
  }
110
+ const repoLocal = repoLocalCliPath();
111
+ if (repoLocal && cliVersionMatches(repoLocal, expectedVersion)) {
112
+ return repoLocal;
113
+ }
85
114
  const fromPath = resolveFromPath(cliFilename());
86
- if (fromPath) {
115
+ if (fromPath && cliVersionMatches(fromPath, expectedVersion)) {
87
116
  return fromPath;
88
117
  }
89
118
  if (!autoInstallEnabled()) {
@@ -108,18 +137,20 @@ async function installCli() {
108
137
  fs.rmSync(assetPath, { force: true });
109
138
  return cliPath;
110
139
  }
111
- function ensureWebPlugin(cliPath) {
140
+ function ensureWebPlugin(cliPath, expectedVersion) {
112
141
  const pluginPath = path.join(allwrightHome(), "plugins", "web", "lib", webPluginFilename());
113
- if (isFile(pluginPath)) {
142
+ if (isFile(pluginPath) && installedPluginVersion("web") === expectedVersion) {
114
143
  return;
115
144
  }
116
- const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
117
- const result = spawnSync(cliPath, ["plugin", "install", "web", "--version", normalizeReleaseVersion(version)], {
145
+ const result = spawnSync(cliPath, ["plugin", "install", "web", "--version", expectedVersion], {
118
146
  stdio: "ignore",
119
147
  });
120
148
  if (result.status !== 0 || !isFile(pluginPath)) {
121
149
  throw new Error("allwright attempted to install the `web` plugin automatically, but the install did not complete successfully");
122
150
  }
151
+ if (installedPluginVersion("web") !== expectedVersion) {
152
+ throw new Error(`allwright attempted to install the \`web\` plugin automatically, but version ${expectedVersion} is still not active`);
153
+ }
123
154
  }
124
155
  async function resolveReleaseTag() {
125
156
  const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
@@ -189,13 +220,96 @@ function normalizeReleaseTag(version) {
189
220
  function normalizeReleaseVersion(version) {
190
221
  return version.replace(/^v/, "");
191
222
  }
223
+ function expectedRuntimeVersion() {
224
+ return normalizeReleaseVersion(process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION);
225
+ }
192
226
  function cliListenAddr(serverAddr) {
193
227
  return serverAddr.replace(/^https?:\/\//, "");
194
228
  }
195
229
  function isLocalServerAddr(serverAddr) {
196
- const host = cliListenAddr(serverAddr).split(":")[0]?.replace(/^\[|\]$/g, "") ?? "";
230
+ const host = parseServerHost(serverAddr);
197
231
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
198
232
  }
233
+ function installedPluginVersion(pluginId) {
234
+ const manifestPath = path.join(allwrightHome(), "plugins.txt");
235
+ if (!isFile(manifestPath)) {
236
+ return null;
237
+ }
238
+ for (const line of fs.readFileSync(manifestPath, "utf8").split(/\r?\n/)) {
239
+ const trimmed = line.trim();
240
+ if (!trimmed || trimmed.startsWith("#")) {
241
+ continue;
242
+ }
243
+ const [id, , version] = trimmed.split("\t", 3);
244
+ if (id === pluginId && version) {
245
+ return normalizeReleaseVersion(version);
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+ async function allocateManagedServerAddr(serverAddr) {
251
+ const host = localBindingHost(serverAddr);
252
+ const port = await new Promise((resolve, reject) => {
253
+ const server = net.createServer();
254
+ server.unref();
255
+ server.on("error", reject);
256
+ server.listen(0, host, () => {
257
+ const address = server.address();
258
+ if (!address || typeof address === "string") {
259
+ server.close(() => reject(new Error("failed to reserve a local port for allwright")));
260
+ return;
261
+ }
262
+ const { port } = address;
263
+ server.close((error) => {
264
+ if (error) {
265
+ reject(error);
266
+ return;
267
+ }
268
+ resolve(port);
269
+ });
270
+ });
271
+ });
272
+ return host.includes(":") ? `http://[${host}]:${port}` : `http://${host}:${port}`;
273
+ }
274
+ function localBindingHost(serverAddr) {
275
+ const host = parseServerHost(serverAddr);
276
+ return host === "::1" ? "::1" : "127.0.0.1";
277
+ }
278
+ function displayVersion(version) {
279
+ return version || "unknown";
280
+ }
281
+ function repoLocalCliPath() {
282
+ const repoRoot = fileURLToPath(new URL("../../..", import.meta.url));
283
+ for (const candidate of [
284
+ path.join(repoRoot, "target", "debug", cliFilename()),
285
+ path.join(repoRoot, "target", "release", cliFilename()),
286
+ ]) {
287
+ if (isFile(candidate)) {
288
+ return candidate;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+ function cliVersionMatches(cliPath, expectedVersion) {
294
+ const result = spawnSync(cliPath, ["--version"], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
295
+ if (result.status !== 0) {
296
+ return false;
297
+ }
298
+ for (const token of result.stdout.split(/\s+/)) {
299
+ if (/^\d/.test(token)) {
300
+ return normalizeReleaseVersion(token) === expectedVersion;
301
+ }
302
+ }
303
+ return false;
304
+ }
305
+ function parseServerHost(serverAddr) {
306
+ const listenAddr = cliListenAddr(serverAddr);
307
+ const ipv6Match = listenAddr.match(/^\[([^\]]+)\]/);
308
+ if (ipv6Match) {
309
+ return ipv6Match[1] ?? "";
310
+ }
311
+ return listenAddr.split(":")[0] ?? "";
312
+ }
199
313
  function allwrightHome() {
200
314
  return process.env[ALLWRIGHT_HOME_ENV_VAR]?.trim() || path.join(os.homedir(), ".allwright");
201
315
  }
package/dist/runtime.js CHANGED
@@ -78,7 +78,7 @@ export function resolveLaunchBrowserArgs(browserKindOrOptions) {
78
78
  }
79
79
  async function createRuntime() {
80
80
  const serverAddr = configuredServerAddr();
81
- await ensureRuntimeReady(serverAddr);
81
+ const resolvedServerAddr = await ensureRuntimeReady(serverAddr);
82
82
  const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
83
83
  includeDirs: [PROTO_ROOT],
84
84
  keepCase: false,
@@ -89,7 +89,7 @@ async function createRuntime() {
89
89
  });
90
90
  const proto = grpc.loadPackageDefinition(loaded);
91
91
  const ClientCtor = proto.allwright.engine.v1.EngineService;
92
- const client = new ClientCtor(serverAddr, grpc.credentials.createInsecure());
92
+ const client = new ClientCtor(resolvedServerAddr, grpc.credentials.createInsecure());
93
93
  return { client };
94
94
  }
95
95
  function configuredServerAddr() {
package/dist/types.d.ts CHANGED
@@ -173,6 +173,7 @@ export interface ResolveConfigOptions {
173
173
  }
174
174
  export interface PingResponse {
175
175
  message?: string;
176
+ version?: string;
176
177
  }
177
178
  export interface ChromeLaunchedPayload {
178
179
  browser?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allwright.dev/core",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "High-level TypeScript client for the allwright automation engine.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -7,6 +7,7 @@ message PingRequest {}
7
7
 
8
8
  message PingResponse {
9
9
  string message = 1;
10
+ string version = 2;
10
11
  }
11
12
 
12
13
  message CommandRetryOptions {