@codeyam-editor/codeyam-editor 0.1.15 → 0.1.16

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,349 @@
1
+ // Launching the capture browser: the Chromium flags that keep a headless
2
+ // launch alive on dbus-less and containerized hosts, and the self-heal around
3
+ // `chromium.launch()` — install a missing browser, retry a transient crash, and
4
+ // diagnose (never heal) a host that lacks Chromium's system libraries. Split
5
+ // out of scenario-check.js, which owns what happens once the browser is up.
6
+
7
+ const path = require("path");
8
+ const { execSync } = require("child_process");
9
+ const { chromium } = require("playwright");
10
+
11
+ // Substring Playwright has emitted across every 1.x release when the
12
+ // browser cache is empty. Match the substring (not the full string)
13
+ // because Playwright includes the offending path inline.
14
+ const PLAYWRIGHT_MISSING_BROWSER_PATTERN = "Executable doesn't exist";
15
+ const PLAYWRIGHT_INSTALL_COMMAND = "npx playwright install chromium";
16
+
17
+ // Substrings Playwright/Chromium emit when the browser process dies on a
18
+ // transient launch crash. On the cloud VM the base image has no dbus session,
19
+ // so Chromium intermittently aborts at launch with `Received signal 11`
20
+ // (SIGSEGV) — an environmental fault, not a scenario fault. Match either form
21
+ // because the wording varies across Playwright releases and the OS signal
22
+ // reporter.
23
+ const PLAYWRIGHT_LAUNCH_CRASH_PATTERNS = ["SIGSEGV", "Received signal 11"];
24
+ // A transient SIGSEGV launch crash is environmental, so a bounded retry clears
25
+ // it. Keep it small — a genuinely broken environment must still surface loudly
26
+ // rather than spin.
27
+ const CAPTURE_LAUNCH_SIGSEGV_RETRIES = 2;
28
+ // Short backoff between SIGSEGV retries so the transient fault has a moment to
29
+ // clear; small enough not to eat the per-slug recapture budget.
30
+ const CAPTURE_LAUNCH_SIGSEGV_BACKOFF_MS = 250;
31
+
32
+ // Pin the headless capture browser's `localhost` resolution to the IPv4
33
+ // loopback the editor's listeners bind. The browser-facing preview origin is
34
+ // now `localhost` (secure-context apps refuse a bare IP — see
35
+ // `BROWSER_FACING_HOST`), but on a dual-stack host `localhost` can resolve to
36
+ // `::1` first, which nothing answers on — stalling the capture or paying a
37
+ // happy-eyeballs fallback delay. Forcing the map removes that intermittency
38
+ // deterministically for capture. The operator's OWN preview browser is covered
39
+ // separately by the editor binding `::1` alongside `127.0.0.1` (see start.rs).
40
+ const CAPTURE_HOST_RESOLVER_RULES = "MAP localhost 127.0.0.1";
41
+ const CAPTURE_LAUNCH_ARGS = [
42
+ `--host-resolver-rules=${CAPTURE_HOST_RESOLVER_RULES}`,
43
+ // Cloud-VM-no-dbus / containerized-launch hardening. Each flag below is
44
+ // inert on a healthy desktop, so the laptop capture path is unchanged; on
45
+ // the dbus-less cloud base image they stop Chromium from SIGSEGV-ing at
46
+ // launch (signal 11). See plan chromium-capture-dbus-crash-hardening.
47
+ // No setuid sandbox in the container (the cloud VM runs capture without the
48
+ // kernel namespaces the sandbox needs).
49
+ "--no-sandbox",
50
+ // /dev/shm is tiny in many containers; route shared memory to /tmp so the
51
+ // renderer doesn't crash allocating it.
52
+ "--disable-dev-shm-usage",
53
+ // No GPU on the headless cloud VM — avoid the GL init path that aborts.
54
+ "--disable-gpu",
55
+ // Software WebGL2, asked for EXPLICITLY rather than inherited. Playwright
56
+ // adds this itself on Linux, which is why `--disable-gpu` left canvas apps
57
+ // with working WebGL2 there — and why it did not on macOS, where Playwright
58
+ // does not add it: `getContext("webgl2")` returned null and the WebGL2
59
+ // capture test failed on every mac dev machine while passing in CI. That is
60
+ // the host-capability dependency CLAUDE.md forbids, so the flag is now
61
+ // stated here for every platform. A duplicate on Linux is harmless
62
+ // (last-wins, same value). WebGPU is still unavailable either way.
63
+ "--enable-unsafe-swiftshader",
64
+ ];
65
+ // Features the capture browser disables on top of Playwright's own list.
66
+ // DBus: stub out the dbus integration Chromium reaches for at launch; the cloud
67
+ // base image has no dbus session bus, which is the proximate cause of the
68
+ // signal-11 abort.
69
+ //
70
+ // These are NOT a second `--disable-features=` switch. Chromium keeps only the
71
+ // LAST value of a repeated switch (verified empirically: `--disable-features=
72
+ // WebHID` hides `navigator.hid`, and a later `--disable-features=DBus` brings it
73
+ // back), so appending our own switch silently re-enabled every feature
74
+ // Playwright disables. `captureLaunchOptions` merges them into one value.
75
+ const CAPTURE_DISABLED_FEATURES = ["DBus"];
76
+ const DISABLE_FEATURES_PREFIX = "--disable-features=";
77
+
78
+ // Playwright's own default `--disable-features=<list>` switch, exactly as it
79
+ // will pass it, or null when it cannot be read. Read from playwright-core's
80
+ // switch table (not part of its public exports, so it is required by file path
81
+ // and any failure degrades to null rather than throwing).
82
+ function playwrightDefaultDisableFeaturesArg() {
83
+ try {
84
+ const playwrightDir = path.dirname(require.resolve("playwright/package.json"));
85
+ const coreDir = path.dirname(
86
+ require.resolve("playwright-core/package.json", { paths: [playwrightDir] }),
87
+ );
88
+ const { chromiumSwitches } = require(
89
+ path.join(coreDir, "lib", "server", "chromium", "chromiumSwitches.js"),
90
+ );
91
+ const found = chromiumSwitches(false).find(
92
+ (arg) => typeof arg === "string" && arg.startsWith(DISABLE_FEATURES_PREFIX),
93
+ );
94
+ return found || null;
95
+ } catch (_) {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ // One `--disable-features=` value carrying the default list plus `extra`,
101
+ // de-duplicated and order-preserving.
102
+ function mergeDisableFeaturesArg(defaultArg, extra) {
103
+ const base =
104
+ typeof defaultArg === "string" && defaultArg.startsWith(DISABLE_FEATURES_PREFIX)
105
+ ? defaultArg.slice(DISABLE_FEATURES_PREFIX.length).split(",")
106
+ : [];
107
+ const merged = [];
108
+ for (const feature of base.concat(extra)) {
109
+ if (feature && !merged.includes(feature)) merged.push(feature);
110
+ }
111
+ return DISABLE_FEATURES_PREFIX + merged.join(",");
112
+ }
113
+
114
+ // The `chromium.launch` args for capture. Playwright's default
115
+ // `--disable-features` switch is suppressed via `ignoreDefaultArgs` and
116
+ // re-supplied merged with ours. Should the suppression ever miss (its match is
117
+ // exact-string), our merged switch is still the later one and a superset, so
118
+ // the last-wins rule still lands every feature.
119
+ function captureLaunchOptions({
120
+ defaultDisableFeaturesArg = playwrightDefaultDisableFeaturesArg(),
121
+ } = {}) {
122
+ const args = [
123
+ ...CAPTURE_LAUNCH_ARGS,
124
+ mergeDisableFeaturesArg(defaultDisableFeaturesArg, CAPTURE_DISABLED_FEATURES),
125
+ ];
126
+ return defaultDisableFeaturesArg
127
+ ? { args, ignoreDefaultArgs: [defaultDisableFeaturesArg] }
128
+ : { args };
129
+ }
130
+ // Point Chromium's dbus client at a dead address so it never blocks on (or
131
+ // crashes against) a missing session bus. Spread over the real env in the
132
+ // default launcher so the laptop path keeps its normal environment.
133
+ const CAPTURE_LAUNCH_DBUS_ENV = { DBUS_SESSION_BUS_ADDRESS: "/dev/null" };
134
+
135
+ // Does this launch error look like a transient SIGSEGV browser crash?
136
+ // (environmental — e.g. the cloud VM's missing dbus session bus — not a
137
+ // scenario fault, so it is safe to retry).
138
+ function isTransientLaunchCrash(error) {
139
+ return (
140
+ error &&
141
+ typeof error.message === "string" &&
142
+ PLAYWRIGHT_LAUNCH_CRASH_PATTERNS.some((pattern) =>
143
+ error.message.includes(pattern),
144
+ )
145
+ );
146
+ }
147
+
148
+ // Stable first token of the error a missing-system-library launch failure is
149
+ // rethrown as. The Rust side keys on it (`scenario_check.rs`
150
+ // `CAPTURE_BROWSER_DEPS_MISSING_MARKER`) to turn the failure into a blocked
151
+ // precondition, so the two spellings must stay greppable from each other.
152
+ const CAPTURE_BROWSER_DEPS_MISSING_MARKER = "CODEYAM_CAPTURE_BROWSER_DEPS_MISSING";
153
+ // The two shapes a missing shared library takes in a launch error: the dynamic
154
+ // loader's own line (Chromium starts and dies), and Playwright's host
155
+ // validation box listing every missing library before it even spawns.
156
+ const MISSING_SHARED_LIBRARY_PATTERNS = [
157
+ "error while loading shared libraries",
158
+ "Host system is missing dependencies",
159
+ ];
160
+ const SHARED_LIBRARY_NAME_PATTERN = /\blib[\w+-]+(?:\.[\w+-]+)*?\.so(?:\.\d+)*/g;
161
+ const PLAYWRIGHT_INSTALL_DEPS_COMMAND = "npx playwright install-deps chromium";
162
+ // Nix package for the libraries Chromium most often lacks on Replit, keyed by
163
+ // the stem before `.so`. A library outside this map still gets a remedy — it is
164
+ // just named without a package suggestion.
165
+ const REPLIT_NIX_PACKAGE_FOR_LIBRARY = {
166
+ "libglib-2.0": "pkgs.glib",
167
+ "libgobject-2.0": "pkgs.glib",
168
+ "libgio-2.0": "pkgs.glib",
169
+ libnss3: "pkgs.nss",
170
+ libnssutil3: "pkgs.nss",
171
+ libsmime3: "pkgs.nss",
172
+ libnspr4: "pkgs.nspr",
173
+ "libatk-1.0": "pkgs.atk",
174
+ "libatk-bridge-2.0": "pkgs.at-spi2-atk",
175
+ "libatspi": "pkgs.at-spi2-core",
176
+ libcups: "pkgs.cups",
177
+ "libdbus-1": "pkgs.dbus",
178
+ libdrm: "pkgs.libdrm",
179
+ libgbm: "pkgs.mesa",
180
+ libxkbcommon: "pkgs.libxkbcommon",
181
+ libasound: "pkgs.alsa-lib",
182
+ "libpango-1.0": "pkgs.pango",
183
+ libcairo: "pkgs.cairo",
184
+ libexpat: "pkgs.expat",
185
+ libX11: "pkgs.xorg.libX11",
186
+ libXcomposite: "pkgs.xorg.libXcomposite",
187
+ libXdamage: "pkgs.xorg.libXdamage",
188
+ libXext: "pkgs.xorg.libXext",
189
+ libXfixes: "pkgs.xorg.libXfixes",
190
+ libXrandr: "pkgs.xorg.libXrandr",
191
+ libxcb: "pkgs.xorg.libxcb",
192
+ };
193
+
194
+ // Does this launch error say the host is missing a shared library Chromium
195
+ // needs? Not retryable and not fixed by downloading the browser again.
196
+ function isMissingSystemLibrary(error) {
197
+ return (
198
+ !!error &&
199
+ typeof error.message === "string" &&
200
+ MISSING_SHARED_LIBRARY_PATTERNS.some((pattern) =>
201
+ error.message.includes(pattern),
202
+ )
203
+ );
204
+ }
205
+
206
+ // Every distinct `lib*.so*` name a missing-library launch error mentions, in
207
+ // first-seen order. Covers both the loader line and Playwright's list.
208
+ function missingLibraryNames(error) {
209
+ const message = (error && error.message) || "";
210
+ return [...new Set(message.match(SHARED_LIBRARY_NAME_PATTERN) || [])];
211
+ }
212
+
213
+ // The fix for the platform the capture is running on. Replit is detected by the
214
+ // same env signals `preview_host_provider/replit.rs` uses; there system
215
+ // packages come from replit.nix. Any other Linux host gets Playwright's own
216
+ // dependency installer; anything else gets the library names and a hint.
217
+ function missingLibraryRemedy(libraries, { env = process.env, platform = process.platform } = {}) {
218
+ const named = libraries.length > 0 ? libraries.join(", ") : "the missing libraries";
219
+ if (env.REPL_ID || env.REPLIT_DEV_DOMAIN) {
220
+ const packages = [
221
+ ...new Set(
222
+ libraries
223
+ .map((lib) => REPLIT_NIX_PACKAGE_FOR_LIBRARY[lib.split(".so")[0]])
224
+ .filter(Boolean),
225
+ ),
226
+ ];
227
+ const suggestion =
228
+ packages.length > 0 ? ` (${packages.join(", ")})` : "";
229
+ return `add the Nix package(s) providing ${named}${suggestion} to the \`deps\` list in replit.nix, then restart the Repl and re-run the capture`;
230
+ }
231
+ if (platform === "linux") {
232
+ return `install Chromium's system libraries with \`${PLAYWRIGHT_INSTALL_DEPS_COMMAND}\` (may need sudo), then re-run the capture`;
233
+ }
234
+ return `install the system package(s) that provide ${named} for this OS, then re-run the capture`;
235
+ }
236
+
237
+ // The rethrown error for a missing-library launch failure: marker line naming
238
+ // the libraries, a `remedy:` line, then the original Playwright message as
239
+ // diagnostic. Line-oriented so the Rust side can lift each part out.
240
+ function missingSystemLibraryError(error, { env, platform } = {}) {
241
+ const libraries = missingLibraryNames(error);
242
+ const remedy = missingLibraryRemedy(libraries, { env, platform });
243
+ const wrapped = new Error(
244
+ `${CAPTURE_BROWSER_DEPS_MISSING_MARKER} ${libraries.join(",")}\n` +
245
+ `remedy: ${remedy}\n` +
246
+ error.message,
247
+ );
248
+ wrapped.cause = error;
249
+ return wrapped;
250
+ }
251
+
252
+ // Self-heal around `chromium.launch()`. Two recoverable classes:
253
+ //
254
+ // 1. "missing browser" — run `npx playwright install chromium` synchronously
255
+ // (with `stdio: "inherit"` so the user sees progress) and retry once.
256
+ // 2. transient SIGSEGV launch crash — retry the launch up to
257
+ // `CAPTURE_LAUNCH_SIGSEGV_RETRIES` times with a short backoff, since the
258
+ // crash is environmental (dbus-less cloud VM) and clears on a retry.
259
+ //
260
+ // A third class is diagnosed but deliberately NOT healed: a host missing a
261
+ // system library Chromium links against. Installing system packages changes
262
+ // the user's environment (replit.nix, apt, often root), so it is rethrown —
263
+ // with no retry and no browser install — as `missingSystemLibraryError`,
264
+ // naming the library and the platform's remedy.
265
+ //
266
+ // For any unrecognized error, or after exhausting retries, rethrow the
267
+ // ORIGINAL Playwright error so the existing `Scenario check failed: <stderr>`
268
+ // path keeps showing the actionable message — looping would hide a real ops
269
+ // failure under a slow timeout.
270
+ async function launchChromiumWithSelfHeal({
271
+ launch = () =>
272
+ chromium.launch({
273
+ ...captureLaunchOptions(),
274
+ env: { ...process.env, ...CAPTURE_LAUNCH_DBUS_ENV },
275
+ }),
276
+ install = () => execSync(PLAYWRIGHT_INSTALL_COMMAND, { stdio: "inherit" }),
277
+ stderr = process.stderr,
278
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
279
+ env = process.env,
280
+ platform = process.platform,
281
+ } = {}) {
282
+ try {
283
+ return await launch();
284
+ } catch (error) {
285
+ if (isMissingSystemLibrary(error)) {
286
+ throw missingSystemLibraryError(error, { env, platform });
287
+ }
288
+ const isMissingBrowser =
289
+ error &&
290
+ typeof error.message === "string" &&
291
+ error.message.includes(PLAYWRIGHT_MISSING_BROWSER_PATTERN);
292
+ const isTransientCrash = isTransientLaunchCrash(error);
293
+ if (!isMissingBrowser && !isTransientCrash) throw error;
294
+
295
+ if (isTransientCrash) {
296
+ for (let attempt = 1; attempt <= CAPTURE_LAUNCH_SIGSEGV_RETRIES; attempt++) {
297
+ stderr.write(
298
+ `Chromium crashed at launch (transient, likely a dbus-less environment) — retry ${attempt}/${CAPTURE_LAUNCH_SIGSEGV_RETRIES}.\n`,
299
+ );
300
+ await sleep(CAPTURE_LAUNCH_SIGSEGV_BACKOFF_MS);
301
+ try {
302
+ return await launch();
303
+ } catch (retryError) {
304
+ if (!isTransientLaunchCrash(retryError)) throw retryError;
305
+ }
306
+ }
307
+ throw error;
308
+ }
309
+
310
+ stderr.write(
311
+ "Playwright's Chromium browser is missing — installing it now (one-time ~150 MB download). Subsequent runs will be instant.\n",
312
+ );
313
+ try {
314
+ install();
315
+ } catch (_installError) {
316
+ throw error;
317
+ }
318
+ try {
319
+ return await launch();
320
+ } catch (retryError) {
321
+ // A fresh download onto a host without Chromium's system libraries
322
+ // (the common first-capture shape on Replit) fails HERE, and the
323
+ // original "missing browser" error would now be a lie.
324
+ if (isMissingSystemLibrary(retryError)) {
325
+ throw missingSystemLibraryError(retryError, { env, platform });
326
+ }
327
+ throw error;
328
+ }
329
+ }
330
+ }
331
+
332
+ module.exports = {
333
+ launchChromiumWithSelfHeal,
334
+ isTransientLaunchCrash,
335
+ isMissingSystemLibrary,
336
+ missingLibraryNames,
337
+ missingLibraryRemedy,
338
+ CAPTURE_BROWSER_DEPS_MISSING_MARKER,
339
+ CAPTURE_LAUNCH_ARGS,
340
+ CAPTURE_DISABLED_FEATURES,
341
+ captureLaunchOptions,
342
+ mergeDisableFeaturesArg,
343
+ playwrightDefaultDisableFeaturesArg,
344
+ CAPTURE_HOST_RESOLVER_RULES,
345
+ CAPTURE_LAUNCH_SIGSEGV_RETRIES,
346
+ PLAYWRIGHT_INSTALL_COMMAND,
347
+ PLAYWRIGHT_MISSING_BROWSER_PATTERN,
348
+ PLAYWRIGHT_LAUNCH_CRASH_PATTERNS,
349
+ };