@allwright.dev/core 0.0.34 → 0.0.36

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.
@@ -0,0 +1,2 @@
1
+ export declare function ensureRuntimeReady(serverAddr: string): Promise<void>;
2
+ export declare function shutdownManagedServer(): Promise<void>;
@@ -0,0 +1,240 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import grpc from "@grpc/grpc-js";
7
+ import protoLoader from "@grpc/proto-loader";
8
+ const ALLWRIGHT_AUTO_INSTALL_ENV_VAR = "ALLWRIGHT_AUTO_INSTALL";
9
+ const ALLWRIGHT_CLI_PATH_ENV_VAR = "ALLWRIGHT_CLI_PATH";
10
+ const ALLWRIGHT_HOME_ENV_VAR = "ALLWRIGHT_HOME";
11
+ const ALLWRIGHT_REPOSITORY_ENV_VAR = "ALLWRIGHT_REPOSITORY";
12
+ const ALLWRIGHT_VERSION_ENV_VAR = "ALLWRIGHT_VERSION";
13
+ const DEFAULT_RELEASE_REPOSITORY = "allwright-dev/allwright";
14
+ const DEFAULT_RELEASE_VERSION = "0.0.1";
15
+ const STARTUP_TIMEOUT_MS = 20_000;
16
+ const PING_TIMEOUT_MS = 1_000;
17
+ const PROTO_ROOT = fileURLToPath(new URL("../proto/", import.meta.url));
18
+ const ENGINE_PROTO_PATH = fileURLToPath(new URL("../proto/engine/v1/engine.proto", import.meta.url));
19
+ let managedServer = null;
20
+ let managedServerAddr = null;
21
+ export async function ensureRuntimeReady(serverAddr) {
22
+ if (await pingServer(serverAddr)) {
23
+ return;
24
+ }
25
+ if (!isLocalServerAddr(serverAddr)) {
26
+ throw new Error(`allwright could not reach engine server at ${serverAddr}. Automatic startup is only supported for local addresses.`);
27
+ }
28
+ if (managedServer && !managedServer.killed && managedServer.exitCode === null && managedServerAddr === serverAddr) {
29
+ await waitForServer(serverAddr);
30
+ return;
31
+ }
32
+ const cliPath = await ensureCliAvailable();
33
+ ensureWebPlugin(cliPath);
34
+ managedServer = spawn(cliPath, ["serve", "--listen-addr", cliListenAddr(serverAddr)], {
35
+ stdio: "ignore",
36
+ });
37
+ managedServerAddr = serverAddr;
38
+ await waitForServer(serverAddr);
39
+ }
40
+ export async function shutdownManagedServer() {
41
+ if (managedServer && managedServer.exitCode === null && !managedServer.killed) {
42
+ managedServer.kill("SIGTERM");
43
+ }
44
+ managedServer = null;
45
+ managedServerAddr = null;
46
+ }
47
+ async function waitForServer(serverAddr) {
48
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
49
+ while (Date.now() < deadline) {
50
+ if (await pingServer(serverAddr)) {
51
+ return;
52
+ }
53
+ await new Promise((resolve) => setTimeout(resolve, 250));
54
+ }
55
+ await shutdownManagedServer();
56
+ throw new Error(`timed out waiting for allwright server at ${serverAddr} to become ready`);
57
+ }
58
+ async function pingServer(serverAddr) {
59
+ const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
60
+ includeDirs: [PROTO_ROOT],
61
+ keepCase: false,
62
+ longs: String,
63
+ enums: String,
64
+ defaults: true,
65
+ oneofs: true,
66
+ });
67
+ const proto = grpc.loadPackageDefinition(loaded);
68
+ const client = new proto.allwright.engine.v1.EngineService(serverAddr, grpc.credentials.createInsecure());
69
+ return await new Promise((resolve) => {
70
+ client.Ping({}, new grpc.Metadata(), { deadline: new Date(Date.now() + PING_TIMEOUT_MS) }, (error) => {
71
+ client.close();
72
+ resolve(!error);
73
+ });
74
+ });
75
+ }
76
+ async function ensureCliAvailable() {
77
+ const envPath = process.env[ALLWRIGHT_CLI_PATH_ENV_VAR]?.trim();
78
+ if (envPath && isFile(envPath)) {
79
+ return envPath;
80
+ }
81
+ const bundled = path.join(allwrightHome(), "bin", cliFilename());
82
+ if (isFile(bundled)) {
83
+ return bundled;
84
+ }
85
+ const fromPath = resolveFromPath(cliFilename());
86
+ if (fromPath) {
87
+ return fromPath;
88
+ }
89
+ if (!autoInstallEnabled()) {
90
+ throw new Error("allwright CLI was not found. Install it first or set ALLWRIGHT_CLI_PATH.");
91
+ }
92
+ return await installCli();
93
+ }
94
+ async function installCli() {
95
+ const installDir = path.join(allwrightHome(), "bin");
96
+ fs.mkdirSync(installDir, { recursive: true });
97
+ const cliPath = path.join(installDir, cliFilename());
98
+ const versionTag = await resolveReleaseTag();
99
+ const assetName = cliAssetName(versionTag);
100
+ const assetPath = path.join(os.tmpdir(), assetName);
101
+ const response = await fetch(`https://github.com/${releaseRepository()}/releases/download/${versionTag}/${assetName}`, { headers: { "user-agent": `allwright-ts/${DEFAULT_RELEASE_VERSION}` } });
102
+ if (!response.ok) {
103
+ throw new Error(`failed to download allwright CLI asset ${assetName}: ${response.status} ${response.statusText}`);
104
+ }
105
+ fs.writeFileSync(assetPath, Buffer.from(await response.arrayBuffer()));
106
+ extractCliArchive(assetPath, cliPath);
107
+ fs.chmodSync(cliPath, 0o755);
108
+ fs.rmSync(assetPath, { force: true });
109
+ return cliPath;
110
+ }
111
+ function ensureWebPlugin(cliPath) {
112
+ const pluginPath = path.join(allwrightHome(), "plugins", "web", "lib", webPluginFilename());
113
+ if (isFile(pluginPath)) {
114
+ return;
115
+ }
116
+ const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
117
+ const result = spawnSync(cliPath, ["plugin", "install", "web", "--version", normalizeReleaseVersion(version)], {
118
+ stdio: "ignore",
119
+ });
120
+ if (result.status !== 0 || !isFile(pluginPath)) {
121
+ throw new Error("allwright attempted to install the `web` plugin automatically, but the install did not complete successfully");
122
+ }
123
+ }
124
+ async function resolveReleaseTag() {
125
+ const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
126
+ if (version !== "latest") {
127
+ return normalizeReleaseTag(version);
128
+ }
129
+ const response = await fetch(`https://api.github.com/repos/${releaseRepository()}/releases/latest`, {
130
+ headers: { "user-agent": `allwright-ts/${DEFAULT_RELEASE_VERSION}` },
131
+ });
132
+ if (!response.ok) {
133
+ throw new Error(`failed to resolve latest allwright release: ${response.status} ${response.statusText}`);
134
+ }
135
+ const payload = (await response.json());
136
+ if (!payload.tag_name?.trim()) {
137
+ throw new Error("latest allwright release metadata did not include tag_name");
138
+ }
139
+ return payload.tag_name;
140
+ }
141
+ function extractCliArchive(archivePath, cliPath) {
142
+ if (archivePath.endsWith(".zip")) {
143
+ const result = spawnSync("powershell", [
144
+ "-NoProfile",
145
+ "-Command",
146
+ `Expand-Archive -Path '${archivePath.replaceAll("'", "''")}' -DestinationPath '${path.dirname(cliPath).replaceAll("'", "''")}' -Force`,
147
+ ], { stdio: "ignore" });
148
+ if (result.status !== 0) {
149
+ throw new Error("failed to extract allwright CLI zip archive");
150
+ }
151
+ const extracted = path.join(path.dirname(cliPath), "bin", cliFilename());
152
+ fs.copyFileSync(extracted, cliPath);
153
+ fs.rmSync(path.join(path.dirname(cliPath), "bin"), { recursive: true, force: true });
154
+ return;
155
+ }
156
+ const result = spawnSync("tar", [
157
+ "-xzf",
158
+ archivePath,
159
+ "-C",
160
+ path.dirname(cliPath),
161
+ `bin/${cliFilename()}`,
162
+ ], { stdio: "ignore" });
163
+ if (result.status !== 0) {
164
+ throw new Error("failed to extract allwright CLI tar archive");
165
+ }
166
+ const extracted = path.join(path.dirname(cliPath), "bin", cliFilename());
167
+ fs.copyFileSync(extracted, cliPath);
168
+ fs.rmSync(path.join(path.dirname(cliPath), "bin"), { recursive: true, force: true });
169
+ }
170
+ function cliAssetName(versionTag) {
171
+ const targets = new Map([
172
+ ["darwin/arm64", "aarch64-apple-darwin"],
173
+ ["darwin/x64", "x86_64-apple-darwin"],
174
+ ["linux/arm64", "aarch64-unknown-linux-gnu"],
175
+ ["linux/x64", "x86_64-unknown-linux-gnu"],
176
+ ["win32/arm64", "aarch64-pc-windows-msvc"],
177
+ ["win32/x64", "x86_64-pc-windows-msvc"],
178
+ ]);
179
+ const target = targets.get(`${process.platform}/${process.arch}`);
180
+ if (!target) {
181
+ throw new Error(`automatic allwright CLI install is not supported on ${process.platform}/${process.arch}`);
182
+ }
183
+ const extension = process.platform === "win32" ? "zip" : "tar.gz";
184
+ return `allwright-${versionTag}-${target}.${extension}`;
185
+ }
186
+ function normalizeReleaseTag(version) {
187
+ return version.startsWith("v") ? version : `v${version}`;
188
+ }
189
+ function normalizeReleaseVersion(version) {
190
+ return version.replace(/^v/, "");
191
+ }
192
+ function cliListenAddr(serverAddr) {
193
+ return serverAddr.replace(/^https?:\/\//, "");
194
+ }
195
+ function isLocalServerAddr(serverAddr) {
196
+ const host = cliListenAddr(serverAddr).split(":")[0]?.replace(/^\[|\]$/g, "") ?? "";
197
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
198
+ }
199
+ function allwrightHome() {
200
+ return process.env[ALLWRIGHT_HOME_ENV_VAR]?.trim() || path.join(os.homedir(), ".allwright");
201
+ }
202
+ function cliFilename() {
203
+ return process.platform === "win32" ? "allwright.exe" : "allwright";
204
+ }
205
+ function webPluginFilename() {
206
+ if (process.platform === "darwin") {
207
+ return "liballwright_surface_web.dylib";
208
+ }
209
+ if (process.platform === "win32") {
210
+ return "allwright_surface_web.dll";
211
+ }
212
+ return "liballwright_surface_web.so";
213
+ }
214
+ function autoInstallEnabled() {
215
+ const raw = process.env[ALLWRIGHT_AUTO_INSTALL_ENV_VAR]?.trim().toLowerCase();
216
+ return raw !== "0" && raw !== "false" && raw !== "no";
217
+ }
218
+ function resolveFromPath(filename) {
219
+ for (const entry of (process.env.PATH ?? "").split(path.delimiter)) {
220
+ if (!entry) {
221
+ continue;
222
+ }
223
+ const candidate = path.join(entry, filename);
224
+ if (isFile(candidate)) {
225
+ return candidate;
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+ function releaseRepository() {
231
+ return process.env[ALLWRIGHT_REPOSITORY_ENV_VAR]?.trim() || DEFAULT_RELEASE_REPOSITORY;
232
+ }
233
+ function isFile(candidate) {
234
+ try {
235
+ return fs.statSync(candidate).isFile();
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ }
package/dist/browser.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PageImpl } from "./page.js";
2
+ import { formatActionError } from "./errors.js";
2
3
  export class BrowserTypeImpl {
3
4
  #browserKind;
4
5
  constructor(browserKind = "chromium") {
@@ -61,7 +62,7 @@ export class BrowserImpl {
61
62
  return this.#createPage(event.tabOpened.tabSessionId);
62
63
  }
63
64
  if (event.error?.message) {
64
- throw new Error(`browser session error while opening tab: ${event.error.message}`);
65
+ throw formatActionError("open tab", event.error.message);
65
66
  }
66
67
  }
67
68
  }
@@ -80,7 +81,7 @@ export class BrowserImpl {
80
81
  return;
81
82
  }
82
83
  if (event.error?.message) {
83
- throw new Error(`browser session error while closing: ${event.error.message}`);
84
+ throw formatActionError("close browser", event.error.message);
84
85
  }
85
86
  }
86
87
  }
@@ -97,7 +98,7 @@ export class BrowserImpl {
97
98
  return event.pong.message;
98
99
  }
99
100
  if (event.error?.message) {
100
- throw new Error(`browser session error while pinging: ${event.error.message}`);
101
+ throw formatActionError("ping browser", event.error.message);
101
102
  }
102
103
  }
103
104
  }
@@ -131,7 +132,7 @@ export class BrowserImpl {
131
132
  }
132
133
  #ensureOpen() {
133
134
  if (this.#closed) {
134
- throw new Error(`browser session ${this.sessionId} is closed`);
135
+ throw formatActionError("use browser", `browser session ${this.sessionId} is closed`);
135
136
  }
136
137
  }
137
138
  }
@@ -0,0 +1,6 @@
1
+ export declare class AllwrightError extends Error {
2
+ readonly debugDetails?: string;
3
+ constructor(message: string, debugDetails?: string);
4
+ }
5
+ export declare function formatStreamError(raw: string): AllwrightError;
6
+ export declare function formatActionError(action: string, raw: string, locator?: string): AllwrightError;
package/dist/errors.js ADDED
@@ -0,0 +1,120 @@
1
+ export class AllwrightError extends Error {
2
+ debugDetails;
3
+ constructor(message, debugDetails) {
4
+ super(message);
5
+ this.name = "AllwrightError";
6
+ this.debugDetails = debugDetails;
7
+ }
8
+ }
9
+ export function formatStreamError(raw) {
10
+ const normalized = stripTransportPrefix(raw);
11
+ const evaluated = extractEvaluateMessage(normalized);
12
+ return createError(evaluated.userMessage, raw, evaluated.debugDetails);
13
+ }
14
+ export function formatActionError(action, raw, locator) {
15
+ const normalized = stripTransportPrefix(raw);
16
+ const evaluated = extractEvaluateMessage(normalized);
17
+ const locatorLabel = locator ? ` for locator ${formatLocator(locator)}` : "";
18
+ const message = evaluated.userMessage
19
+ ? `${capitalize(action)} failed${locatorLabel}: ${evaluated.userMessage}`
20
+ : `${capitalize(action)} failed${locatorLabel}.`;
21
+ return createError(message, raw, evaluated.debugDetails);
22
+ }
23
+ function createError(message, raw, debugDetails) {
24
+ if (debugEnabled()) {
25
+ return new AllwrightError(`${message}\n\nDebug details: ${debugDetails ?? raw}`, debugDetails ?? raw);
26
+ }
27
+ return new AllwrightError(message, debugDetails ?? raw);
28
+ }
29
+ function extractEvaluateMessage(raw) {
30
+ const exceptionMarker = "Runtime.evaluate failed with exception details:";
31
+ const mapperMarker = "mapper Runtime.evaluate raised exception details:";
32
+ const marker = raw.includes(exceptionMarker)
33
+ ? exceptionMarker
34
+ : raw.includes(mapperMarker)
35
+ ? mapperMarker
36
+ : null;
37
+ if (!marker) {
38
+ return { userMessage: cleanupUserMessage(raw) };
39
+ }
40
+ const payload = raw.slice(raw.indexOf(marker) + marker.length).trim();
41
+ const parsed = tryParseJson(payload);
42
+ const message = pickString(parsed, ["exception", "message"]) ??
43
+ pickPreviewProperty(parsed, "message") ??
44
+ pickString(parsed, ["exception", "description"]) ??
45
+ raw;
46
+ return {
47
+ userMessage: cleanupUserMessage(message),
48
+ debugDetails: payload,
49
+ };
50
+ }
51
+ function cleanupUserMessage(message) {
52
+ const compact = message.replace(/\s+/g, " ").trim();
53
+ const invalidQuerySelector = compact.match(/Failed to execute 'querySelector(All)?' on 'Document': '(.+?)' is not a valid selector\.?/);
54
+ if (invalidQuerySelector) {
55
+ return `invalid selector ${invalidQuerySelector[2]}`;
56
+ }
57
+ return compact
58
+ .replace(/^SyntaxError:\s*/i, "")
59
+ .replace(/^DOMException:\s*/i, "")
60
+ .replace(/^Error:\s*/i, "");
61
+ }
62
+ function stripTransportPrefix(raw) {
63
+ return raw
64
+ .replace(/^grpc stream error:\s*/i, "")
65
+ .replace(/^\d+\s+INTERNAL:\s*/i, "")
66
+ .trim();
67
+ }
68
+ function capitalize(value) {
69
+ return value.charAt(0).toUpperCase() + value.slice(1);
70
+ }
71
+ function formatLocator(locator) {
72
+ return JSON.stringify(locator.trim());
73
+ }
74
+ function debugEnabled() {
75
+ const raw = process.env.ALLWRIGHT_DEBUG?.trim().toLowerCase();
76
+ return raw === "1" || raw === "true" || raw === "yes";
77
+ }
78
+ function tryParseJson(value) {
79
+ try {
80
+ return JSON.parse(value);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ function pickString(root, path) {
87
+ let current = root;
88
+ for (const segment of path) {
89
+ if (!current || typeof current !== "object" || !(segment in current)) {
90
+ return null;
91
+ }
92
+ current = current[segment];
93
+ }
94
+ return typeof current === "string" && current.trim() ? current.trim() : null;
95
+ }
96
+ function pickPreviewProperty(root, name) {
97
+ const properties = pickUnknown(root, ["exception", "preview", "properties"]);
98
+ if (!Array.isArray(properties)) {
99
+ return null;
100
+ }
101
+ for (const property of properties) {
102
+ if (property &&
103
+ typeof property === "object" &&
104
+ property.name === name &&
105
+ typeof property.value === "string") {
106
+ return (property.value ?? "").trim() || null;
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ function pickUnknown(root, path) {
112
+ let current = root;
113
+ for (const segment of path) {
114
+ if (!current || typeof current !== "object" || !(segment in current)) {
115
+ return null;
116
+ }
117
+ current = current[segment];
118
+ }
119
+ return current;
120
+ }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BrowserImpl, BrowserTypeImpl } from "./browser.js";
2
+ import { formatActionError } from "./errors.js";
2
3
  import { findConfigFile, loadConfigFile, resolveConfig } from "./config.js";
3
4
  import { PageImpl } from "./page.js";
4
5
  import { createBrowserSessionHandle, getRuntime, launchConfiguredBrowser as launchConfiguredBrowserWithResolver, ping as runtimePing, resolveLaunchBrowserArgs, setServerAddr, shutdown, } from "./runtime.js";
@@ -42,7 +43,7 @@ export async function launchBrowser(browserKindOrOptions, options = {}) {
42
43
  });
43
44
  }
44
45
  if (event.error?.message) {
45
- throw new Error(`browser session error during launch: ${event.error.message}`);
46
+ throw formatActionError("launch browser", event.error.message);
46
47
  }
47
48
  }
48
49
  }
package/dist/page.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { LocatorImpl } from "./locator.js";
2
+ import { formatActionError } from "./errors.js";
2
3
  import { normalizeSelectorForTransport } from "./selectors.js";
3
4
  import { createPageHandle } from "./runtime.js";
4
5
  export class PageImpl {
@@ -36,7 +37,7 @@ export class PageImpl {
36
37
  injection = event.chromiumBidiInjection;
37
38
  }
38
39
  if (event.error?.message) {
39
- throw new Error(`page session error while navigating: ${event.error.message}`);
40
+ throw formatActionError("navigate", event.error.message);
40
41
  }
41
42
  if (event.closed) {
42
43
  handle.closed = true;
@@ -76,7 +77,7 @@ export class PageImpl {
76
77
  };
77
78
  }
78
79
  if (event.error?.message) {
79
- throw new Error(`page session error while clicking: ${event.error.message}`);
80
+ throw formatActionError("click", event.error.message, selector);
80
81
  }
81
82
  if (event.closed) {
82
83
  handle.closed = true;
@@ -106,7 +107,7 @@ export class PageImpl {
106
107
  };
107
108
  }
108
109
  if (event.error?.message) {
109
- throw new Error(`page session error while counting elements: ${event.error.message}`);
110
+ throw formatActionError("count elements", event.error.message, selector);
110
111
  }
111
112
  if (event.closed) {
112
113
  handle.closed = true;
@@ -137,7 +138,7 @@ export class PageImpl {
137
138
  };
138
139
  }
139
140
  if (event.error?.message) {
140
- throw new Error(`page session error while highlighting elements: ${event.error.message}`);
141
+ throw formatActionError("highlight elements", event.error.message, selector);
141
142
  }
142
143
  if (event.closed) {
143
144
  handle.closed = true;
@@ -166,7 +167,7 @@ export class PageImpl {
166
167
  };
167
168
  }
168
169
  if (event.error?.message) {
169
- throw new Error(`page session error while focusing: ${event.error.message}`);
170
+ throw formatActionError("focus", event.error.message, selector);
170
171
  }
171
172
  if (event.closed) {
172
173
  handle.closed = true;
@@ -197,7 +198,7 @@ export class PageImpl {
197
198
  };
198
199
  }
199
200
  if (event.error?.message) {
200
- throw new Error(`page session error while filling: ${event.error.message}`);
201
+ throw formatActionError("fill", event.error.message, selector);
201
202
  }
202
203
  if (event.closed) {
203
204
  handle.closed = true;
@@ -226,7 +227,7 @@ export class PageImpl {
226
227
  };
227
228
  }
228
229
  if (event.error?.message) {
229
- throw new Error(`page session error while hovering: ${event.error.message}`);
230
+ throw formatActionError("hover", event.error.message, selector);
230
231
  }
231
232
  if (event.closed) {
232
233
  handle.closed = true;
@@ -258,7 +259,7 @@ export class PageImpl {
258
259
  };
259
260
  }
260
261
  if (event.error?.message) {
261
- throw new Error(`page session error while pressing key: ${event.error.message}`);
262
+ throw formatActionError("press key", event.error.message, selector);
262
263
  }
263
264
  if (event.closed) {
264
265
  handle.closed = true;
@@ -295,7 +296,7 @@ export class PageImpl {
295
296
  };
296
297
  }
297
298
  if (event.error?.message) {
298
- throw new Error(`page session error while waiting for selector: ${event.error.message}`);
299
+ throw formatActionError("wait for selector", event.error.message, selector);
299
300
  }
300
301
  if (event.closed) {
301
302
  handle.closed = true;
@@ -321,7 +322,7 @@ export class PageImpl {
321
322
  return;
322
323
  }
323
324
  if (event.error?.message) {
324
- throw new Error(`page session error while closing: ${event.error.message}`);
325
+ throw formatActionError("close page", event.error.message);
325
326
  }
326
327
  }
327
328
  }
@@ -341,7 +342,7 @@ export class PageImpl {
341
342
  return event.pong.message;
342
343
  }
343
344
  if (event.error?.message) {
344
- throw new Error(`page session error while pinging: ${event.error.message}`);
345
+ throw formatActionError("ping page", event.error.message);
345
346
  }
346
347
  if (event.closed) {
347
348
  handle.closed = true;
@@ -396,7 +397,7 @@ export class PageImpl {
396
397
  };
397
398
  }
398
399
  if (event.error?.message) {
399
- throw new Error(`page session error while reading text: ${event.error.message}`);
400
+ throw formatActionError("read text", event.error.message, selector);
400
401
  }
401
402
  if (event.closed) {
402
403
  handle.closed = true;
@@ -406,7 +407,7 @@ export class PageImpl {
406
407
  }
407
408
  #ensureOpen(handle) {
408
409
  if (handle.closed) {
409
- throw new Error(`page session ${this.sessionId} is closed`);
410
+ throw formatActionError("use page", `page session ${this.sessionId} is closed`);
410
411
  }
411
412
  }
412
413
  async #getHandle() {
package/dist/runtime.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import grpc from "@grpc/grpc-js";
3
3
  import protoLoader from "@grpc/proto-loader";
4
+ import { ensureRuntimeReady, shutdownManagedServer } from "./bootstrap.js";
5
+ import { formatStreamError } from "./errors.js";
4
6
  import { EventQueue } from "./types.js";
5
7
  const DEFAULT_SERVER_ADDR = "127.0.0.1:50051";
6
8
  const SERVER_ADDR_ENV_VAR = "ALLWRIGHT_SERVER_ADDR";
@@ -11,6 +13,7 @@ let serverAddrOverride = null;
11
13
  export function setServerAddr(serverAddr) {
12
14
  serverAddrOverride = normalizeServerAddr(serverAddr);
13
15
  runtimePromise = null;
16
+ void shutdownManagedServer();
14
17
  }
15
18
  export async function shutdown() {
16
19
  if (!runtimePromise) {
@@ -19,6 +22,7 @@ export async function shutdown() {
19
22
  const runtime = await runtimePromise;
20
23
  runtime.client.close();
21
24
  runtimePromise = null;
25
+ await shutdownManagedServer();
22
26
  }
23
27
  export async function ping() {
24
28
  const runtime = await getRuntime();
@@ -34,7 +38,7 @@ export async function ping() {
34
38
  }
35
39
  export async function getRuntime() {
36
40
  if (!runtimePromise) {
37
- runtimePromise = Promise.resolve(createRuntime());
41
+ runtimePromise = createRuntime();
38
42
  }
39
43
  return runtimePromise;
40
44
  }
@@ -72,7 +76,9 @@ export function normalizeServerAddr(raw) {
72
76
  export function resolveLaunchBrowserArgs(browserKindOrOptions) {
73
77
  return browserKindOrOptions === undefined || typeof browserKindOrOptions !== "string";
74
78
  }
75
- function createRuntime() {
79
+ async function createRuntime() {
80
+ const serverAddr = configuredServerAddr();
81
+ await ensureRuntimeReady(serverAddr);
76
82
  const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
77
83
  includeDirs: [PROTO_ROOT],
78
84
  keepCase: false,
@@ -83,7 +89,7 @@ function createRuntime() {
83
89
  });
84
90
  const proto = grpc.loadPackageDefinition(loaded);
85
91
  const ClientCtor = proto.allwright.engine.v1.EngineService;
86
- const client = new ClientCtor(configuredServerAddr(), grpc.credentials.createInsecure());
92
+ const client = new ClientCtor(serverAddr, grpc.credentials.createInsecure());
87
93
  return { client };
88
94
  }
89
95
  function configuredServerAddr() {
@@ -98,7 +104,7 @@ function bindStreamQueue(stream) {
98
104
  queue.push(event);
99
105
  });
100
106
  stream.on("error", (error) => {
101
- queue.fail(new Error(`grpc stream error: ${error.message}`));
107
+ queue.fail(formatStreamError(`grpc stream error: ${error.message}`));
102
108
  });
103
109
  stream.on("end", () => {
104
110
  queue.fail(new Error("grpc stream ended"));
package/dist/selectors.js CHANGED
@@ -1,11 +1,92 @@
1
+ const SELECTOR_PREFIXES = ["xpath=", "xpath:", "css=", "css:"];
2
+ function decodeSelectorBody(body) {
3
+ const candidate = body.trim();
4
+ if (candidate.startsWith("\"") && candidate.endsWith("\"")) {
5
+ try {
6
+ return JSON.parse(candidate);
7
+ }
8
+ catch {
9
+ return candidate;
10
+ }
11
+ }
12
+ return candidate;
13
+ }
14
+ function parseExplicitSelectorPrefix(selector) {
15
+ const lowered = selector.toLowerCase();
16
+ if (lowered.startsWith("xpath=") || lowered.startsWith("xpath:")) {
17
+ return { flavor: "xpath", prefixLength: 6 };
18
+ }
19
+ if (lowered.startsWith("css=") || lowered.startsWith("css:")) {
20
+ return { flavor: "css", prefixLength: 4 };
21
+ }
22
+ return null;
23
+ }
24
+ function findJsonStringEnd(value) {
25
+ if (!value.startsWith("\"")) {
26
+ return null;
27
+ }
28
+ let escaped = false;
29
+ for (let index = 1; index < value.length; index += 1) {
30
+ const char = value[index];
31
+ if (escaped) {
32
+ escaped = false;
33
+ continue;
34
+ }
35
+ if (char === "\\") {
36
+ escaped = true;
37
+ continue;
38
+ }
39
+ if (char === "\"") {
40
+ return index + 1;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ function isNormalizedTransportSelector(selector) {
46
+ const trimmed = selector.trim();
47
+ if (!trimmed) {
48
+ return false;
49
+ }
50
+ let index = 0;
51
+ while (index < trimmed.length) {
52
+ const prefix = parseExplicitSelectorPrefix(trimmed.slice(index));
53
+ if (!prefix) {
54
+ return false;
55
+ }
56
+ index += prefix.prefixLength;
57
+ const remainder = trimmed.slice(index);
58
+ if (!remainder.startsWith("\"")) {
59
+ return false;
60
+ }
61
+ const jsonEnd = findJsonStringEnd(remainder);
62
+ if (!jsonEnd) {
63
+ return false;
64
+ }
65
+ index += jsonEnd;
66
+ const tail = trimmed.slice(index);
67
+ if (!tail) {
68
+ return true;
69
+ }
70
+ if (!/^\s+/.test(tail)) {
71
+ return false;
72
+ }
73
+ const nextIndex = index + tail.match(/^\s+/)?.[0].length;
74
+ const nextSegment = trimmed.slice(nextIndex).toLowerCase();
75
+ if (!SELECTOR_PREFIXES.some((prefixValue) => nextSegment.startsWith(prefixValue))) {
76
+ return false;
77
+ }
78
+ index = nextIndex;
79
+ }
80
+ return true;
81
+ }
1
82
  export function parseSelectorForTransport(selector) {
2
83
  const trimmed = selector.trim();
3
84
  const lower = trimmed.toLowerCase();
4
85
  if (lower.startsWith("xpath=") || lower.startsWith("xpath:")) {
5
- return { flavor: "xpath", body: trimmed.slice(6).trim() };
86
+ return { flavor: "xpath", body: decodeSelectorBody(trimmed.slice(6)) };
6
87
  }
7
88
  if (lower.startsWith("css=") || lower.startsWith("css:")) {
8
- return { flavor: "css", body: trimmed.slice(4).trim() };
89
+ return { flavor: "css", body: decodeSelectorBody(trimmed.slice(4)) };
9
90
  }
10
91
  if (trimmed.startsWith("//") ||
11
92
  trimmed.startsWith(".//") ||
@@ -17,6 +98,10 @@ export function parseSelectorForTransport(selector) {
17
98
  return { flavor: "css", body: trimmed };
18
99
  }
19
100
  export function normalizeSelectorForTransport(selector) {
101
+ const trimmed = selector.trim();
102
+ if (isNormalizedTransportSelector(trimmed)) {
103
+ return trimmed;
104
+ }
20
105
  const parsed = parseSelectorForTransport(selector);
21
106
  return `${parsed.flavor}=${JSON.stringify(parsed.body)}`;
22
107
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import grpc from "@grpc/grpc-js";
2
+ export { AllwrightError } from "./errors.js";
2
3
  export interface LaunchOptions {
3
4
  browserBinary?: string;
4
5
  timeoutMs?: number;
package/dist/types.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { AllwrightError } from "./errors.js";
1
2
  export class EventQueue {
2
3
  #items = [];
3
4
  #waiters = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allwright.dev/core",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "High-level TypeScript client for the allwright automation engine.",
5
5
  "license": "MIT",
6
6
  "type": "module",