@reticlehq/vite-plugin 3.2.0 → 3.4.0

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/index.cjs CHANGED
@@ -80,8 +80,6 @@ function ensurePairingToken(dir) {
80
80
  // src/index.ts
81
81
  var import_node_os2 = require("node:os");
82
82
  var import_node_path8 = require("node:path");
83
- var import_core6 = require("@babel/core");
84
- var import_babel_plugin = __toESM(require("@reticlehq/babel-plugin"), 1);
85
83
 
86
84
  // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
87
85
  var external_exports = {};
@@ -4124,6 +4122,119 @@ var coerce = {
4124
4122
  };
4125
4123
  var NEVER = INVALID;
4126
4124
 
4125
+ // ../../../core/dist/wire/constants/event-types.js
4126
+ var EventType = {
4127
+ DOM_ADDED: "dom.added",
4128
+ DOM_REMOVED: "dom.removed",
4129
+ DOM_ATTR: "dom.attr",
4130
+ DOM_TEXT: "dom.text",
4131
+ NET_REQUEST: "net.request",
4132
+ NET_PENDING: "net.pending",
4133
+ /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
4134
+ NET_STREAM: "net.stream",
4135
+ /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
4136
+ PERF: "perf",
4137
+ ROUTE_CHANGE: "route.change",
4138
+ CONSOLE_LOG: "console.log",
4139
+ CONSOLE_WARN: "console.warn",
4140
+ CONSOLE_ERROR: "console.error",
4141
+ CONSOLE_INFO: "console.info",
4142
+ CONSOLE_DEBUG: "console.debug",
4143
+ ERROR_UNCAUGHT: "error.uncaught",
4144
+ VISIBLE_SHOWN: "visible.shown",
4145
+ ANIM_START: "anim.start",
4146
+ ANIM_END: "anim.end",
4147
+ SCROLL_POSITION: "scroll.position",
4148
+ REVEAL_SHOWN: "reveal.shown",
4149
+ SIGNAL: "signal",
4150
+ STATE_CHANGE: "state.change",
4151
+ /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
4152
+ STORAGE_CHANGE: "storage.change",
4153
+ /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
4154
+ PAGE_HEALTH: "page.health",
4155
+ /**
4156
+ * synthetic: the page called window.open, so the consequence of what was just clicked may live in
4157
+ * another browsing context this one cannot observe (an OAuth popup is the archetype).
4158
+ * `data: { href }` — the URL the page asked to open, when it named one.
4159
+ */
4160
+ CONTEXT_OPENED: "context.opened",
4161
+ /**
4162
+ * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
4163
+ *
4164
+ * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
4165
+ * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
4166
+ * driven click made the tab permanently unresponsive with no recovery from inside the session.
4167
+ * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
4168
+ * the answer given, ride out as an event.
4169
+ */
4170
+ DIALOG_OPENED: "dialog.opened",
4171
+ /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
4172
+ * wasted re-renders show up here without a per-render flood. */
4173
+ RENDER_COMMIT: "render.commit",
4174
+ /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
4175
+ FOCUS_CHANGE: "focus.change",
4176
+ /**
4177
+ * a form field's value moved — `data: { field, kind, value?, redacted?, length }`.
4178
+ *
4179
+ * The DOM observer never saw this: `value` is in its attribute allowlist, but React and every
4180
+ * controlled input set the PROPERTY, so `MutationObserver` does not fire. `value` is omitted and
4181
+ * `redacted` set for a password, a sensitive name, or a payment autocomplete hint; `length` is
4182
+ * always present, because "it was wiped" is assertable without carrying anybody's data.
4183
+ */
4184
+ FIELD_CHANGE: "field.change",
4185
+ /** browser → bridge: a human recording compiled in-page. */
4186
+ FLOW_RECORDED: "flow.recorded",
4187
+ /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
4188
+ TRANSPORT_OVERFLOW: "transport.overflow",
4189
+ /**
4190
+ * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
4191
+ * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
4192
+ * than no ledger, so truncation is never silent.
4193
+ */
4194
+ TRUNCATED: "truncated",
4195
+ /**
4196
+ * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
4197
+ * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
4198
+ * implies it saw everything.
4199
+ */
4200
+ BLIND_SPOT: "blind-spot",
4201
+ /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
4202
+ * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
4203
+ SDK_FAILED: "sdk.failed",
4204
+ /**
4205
+ * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
4206
+ * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
4207
+ * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
4208
+ * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
4209
+ */
4210
+ NET_DETAIL: "net.detail",
4211
+ /**
4212
+ * Live-control: browser → bridge. A human acted on the presenter panel.
4213
+ * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
4214
+ */
4215
+ HUMAN_CONTROL: "human.control",
4216
+ /**
4217
+ * Human review: browser → bridge. A human pinned a mistake to an element on the running page
4218
+ * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
4219
+ * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
4220
+ * the agent that drains the mark knows exactly which element and which source to fix.
4221
+ */
4222
+ HUMAN_MARK: "human.mark",
4223
+ /**
4224
+ * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
4225
+ * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
4226
+ * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
4227
+ * request to intercept. See `observers/download.ts` for the defect that motivated it.
4228
+ */
4229
+ DOWNLOAD: "download",
4230
+ /**
4231
+ * Browser -> bridge: a person used the HUD — pressed a control, or the HUD changed how it sits.
4232
+ * `data` narrows to HudUseDataSchema: names only. Telemetry, not evidence: the session takes it
4233
+ * off the wire before the buffer, so no verdict, settle or agent read ever sees it.
4234
+ */
4235
+ HUD_USED: "hud.used"
4236
+ };
4237
+
4127
4238
  // ../../../core/dist/wire/constants/constants.js
4128
4239
  var RETICLE_DEFAULT_PORT = 4400;
4129
4240
  var RETICLE_WS_PATH = "/reticle";
@@ -4248,7 +4359,14 @@ var ReticleEnv = {
4248
4359
  API_KEY: "RETICLE_API_KEY",
4249
4360
  /** The name this key had until 2026-09. Still honoured; see `apiKeyFrom()`. */
4250
4361
  CLOUD_KEY: "RETICLE_CLOUD_KEY",
4251
- CLOUD_URL: "RETICLE_CLOUD_URL"
4362
+ /** The platform host. Read through `cloudUrlFrom()`, which also accepts `URL`. */
4363
+ CLOUD_URL: "RETICLE_CLOUD_URL",
4364
+ /** The short name for `CLOUD_URL`, which people type first. `CLOUD_URL` wins when both are set. */
4365
+ URL: "RETICLE_URL"
4366
+ };
4367
+ var SYNC_BATCH_LIMITS = {
4368
+ MAX_RUNS: 50,
4369
+ MAX_BYTES: 4 * 1024 * 1024
4252
4370
  };
4253
4371
  var TRANSPORT_LIMITS = {
4254
4372
  MAX_MESSAGE_BYTES: 1024 * 1024,
@@ -4349,6 +4467,14 @@ var ReticleDir = {
4349
4467
  RUNS_SUBDIR: "runs",
4350
4468
  /** fail-to-pass bug capsules —.reticle/capsules/<id>.json (a minimal failing flow + its evidence). */
4351
4469
  CAPSULES_SUBDIR: "capsules",
4470
+ /**
4471
+ * Where a feedback report refused delivery is copied — .reticle/feedback/.
4472
+ *
4473
+ * Named here rather than at the writer because the partition over this object is what decides
4474
+ * whether a name Reticle creates is committed, ignored, or swept. It was spelled as a free string
4475
+ * in three separate files, so the guard could not see it and nobody was ever asked.
4476
+ */
4477
+ FEEDBACK_SUBDIR: "feedback",
4352
4478
  /** durable causal journal —.reticle/sessions/<id>/{events,actions}.jsonl (the substrate). */
4353
4479
  SESSIONS_SUBDIR: "sessions",
4354
4480
  /** append-only event ledger inside a session dir (one ReticleEvent per line). */
@@ -4370,6 +4496,8 @@ var ReticleDir = {
4370
4496
  AMBIENT_FILE: "ambient.json",
4371
4497
  /** per-flow flake ledger — replay outcomes that decide intermittent-failure quarantine. */
4372
4498
  FLAKE_FILE: "flake.json",
4499
+ /** The app-wide coverage ledger — see server features/exhaust/ledger.ts. */
4500
+ COVERAGE_FILE: "coverage.json",
4373
4501
  /**
4374
4502
  * the project's cloud binding — .reticle/cloud.json, written by `reticle link`. Git-checked and
4375
4503
  * non-secret: the project id, the API origin, and where its dashboard lives. The KEY lives in
@@ -4414,102 +4542,6 @@ var RING_BUFFER_DEFAULTS = {
4414
4542
  MAX_AGE_MS: 6e4,
4415
4543
  MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
4416
4544
  };
4417
- var EventType = {
4418
- DOM_ADDED: "dom.added",
4419
- DOM_REMOVED: "dom.removed",
4420
- DOM_ATTR: "dom.attr",
4421
- DOM_TEXT: "dom.text",
4422
- NET_REQUEST: "net.request",
4423
- NET_PENDING: "net.pending",
4424
- /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
4425
- NET_STREAM: "net.stream",
4426
- /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
4427
- PERF: "perf",
4428
- ROUTE_CHANGE: "route.change",
4429
- CONSOLE_LOG: "console.log",
4430
- CONSOLE_WARN: "console.warn",
4431
- CONSOLE_ERROR: "console.error",
4432
- CONSOLE_INFO: "console.info",
4433
- CONSOLE_DEBUG: "console.debug",
4434
- ERROR_UNCAUGHT: "error.uncaught",
4435
- VISIBLE_SHOWN: "visible.shown",
4436
- ANIM_START: "anim.start",
4437
- ANIM_END: "anim.end",
4438
- SCROLL_POSITION: "scroll.position",
4439
- REVEAL_SHOWN: "reveal.shown",
4440
- SIGNAL: "signal",
4441
- STATE_CHANGE: "state.change",
4442
- /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
4443
- STORAGE_CHANGE: "storage.change",
4444
- /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
4445
- PAGE_HEALTH: "page.health",
4446
- /**
4447
- * synthetic: the page called window.open, so the consequence of what was just clicked may live in
4448
- * another browsing context this one cannot observe (an OAuth popup is the archetype).
4449
- * `data: { href }` — the URL the page asked to open, when it named one.
4450
- */
4451
- CONTEXT_OPENED: "context.opened",
4452
- /**
4453
- * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
4454
- *
4455
- * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
4456
- * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
4457
- * driven click made the tab permanently unresponsive with no recovery from inside the session.
4458
- * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
4459
- * the answer given, ride out as an event.
4460
- */
4461
- DIALOG_OPENED: "dialog.opened",
4462
- /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
4463
- * wasted re-renders show up here without a per-render flood. */
4464
- RENDER_COMMIT: "render.commit",
4465
- /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
4466
- FOCUS_CHANGE: "focus.change",
4467
- /** browser → bridge: a human recording compiled in-page. */
4468
- FLOW_RECORDED: "flow.recorded",
4469
- /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
4470
- TRANSPORT_OVERFLOW: "transport.overflow",
4471
- /**
4472
- * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
4473
- * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
4474
- * than no ledger, so truncation is never silent.
4475
- */
4476
- TRUNCATED: "truncated",
4477
- /**
4478
- * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
4479
- * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
4480
- * implies it saw everything.
4481
- */
4482
- BLIND_SPOT: "blind-spot",
4483
- /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
4484
- * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
4485
- SDK_FAILED: "sdk.failed",
4486
- /**
4487
- * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
4488
- * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
4489
- * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
4490
- * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
4491
- */
4492
- NET_DETAIL: "net.detail",
4493
- /**
4494
- * Live-control: browser → bridge. A human acted on the presenter panel.
4495
- * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
4496
- */
4497
- HUMAN_CONTROL: "human.control",
4498
- /**
4499
- * Human review: browser → bridge. A human pinned a mistake to an element on the running page
4500
- * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
4501
- * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
4502
- * the agent that drains the mark knows exactly which element and which source to fix.
4503
- */
4504
- HUMAN_MARK: "human.mark",
4505
- /**
4506
- * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
4507
- * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
4508
- * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
4509
- * request to intercept. See `observers/download.ts` for the defect that motivated it.
4510
- */
4511
- DOWNLOAD: "download"
4512
- };
4513
4545
  var RETICLE_RENDER_PREHOOK = "__reticleRenderPreHook";
4514
4546
  var CONSOLE_LEVEL_PREFIX = "console.";
4515
4547
  var CONSOLE_LEVELS = [
@@ -4523,6 +4555,7 @@ var CONSOLE_LEVELS = [
4523
4555
  var DATA_RETICLE_SOURCE_ATTR = "data-reticle-source";
4524
4556
  var RETICLE_ROOT_GLOBAL = "__RETICLE_ROOT__";
4525
4557
  var RETICLE_SDK_VERSION_GLOBAL = "__RETICLE_SDK_VERSION__";
4558
+ var RETICLE_IGNORE_MARKER = "@reticle-ignore";
4526
4559
 
4527
4560
  // ../../../core/dist/identity/project-id.js
4528
4561
  var PROJECT_ID_HASH_LENGTH = 8;
@@ -4571,7 +4604,10 @@ var DaemonRegistryEntrySchema = external_exports.object({
4571
4604
  projectId: external_exports.string().optional(),
4572
4605
  startedAt: external_exports.number()
4573
4606
  });
4574
- function pickDaemonPort(entries, projectId, isAlive2) {
4607
+ function pickDaemonPort(entries, projectId, isAlive2, configuredPort) {
4608
+ if (void 0 !== configuredPort && entries.some((e) => e.port === configuredPort && isAlive2(e.pid) && (e.projectId === void 0 || projectId === void 0 || e.projectId === projectId))) {
4609
+ return configuredPort;
4610
+ }
4575
4611
  if (projectId === void 0 || 0 === projectId.length)
4576
4612
  return null;
4577
4613
  const matches = entries.filter((e) => e.projectId === projectId && isAlive2(e.pid)).map((e) => e.port).sort((a, b) => a - b);
@@ -4621,14 +4657,17 @@ function deriveProjectId(pkgName, rootPath) {
4621
4657
  var readFileOrThrow = (path) => (0, import_node_fs2.readFileSync)(path, "utf8");
4622
4658
  var MAX_CONFIG_SEARCH_DEPTH = 6;
4623
4659
  var MAX_PACKAGE_SEARCH_DEPTH = 50;
4624
- function readNearestField(startDir, basename, field, readFile, maxDepth) {
4660
+ var nonEmptyString = (value) => "string" === typeof value && value.length > 0 ? value : void 0;
4661
+ var MAX_TCP_PORT = 65535;
4662
+ var tcpPort = (value) => Number.isInteger(value) && value > 0 && value <= MAX_TCP_PORT ? value : void 0;
4663
+ function readNearestField(startDir, basename, field, readFile, maxDepth, accept) {
4625
4664
  let dir = startDir;
4626
4665
  for (let depth = 0; depth <= maxDepth; depth++) {
4627
4666
  try {
4628
4667
  const parsed = JSON.parse(readFile((0, import_node_path2.join)(dir, basename)));
4629
4668
  if ("object" === typeof parsed && parsed !== null) {
4630
- const value = parsed[field];
4631
- if ("string" === typeof value && value.length > 0) return value;
4669
+ const value = accept(parsed[field]);
4670
+ if (value !== void 0) return value;
4632
4671
  }
4633
4672
  } catch {
4634
4673
  }
@@ -4639,7 +4678,14 @@ function readNearestField(startDir, basename, field, readFile, maxDepth) {
4639
4678
  return void 0;
4640
4679
  }
4641
4680
  function readNearestPackageName(startDir, readFile = readFileOrThrow) {
4642
- return readNearestField(startDir, "package.json", "name", readFile, MAX_PACKAGE_SEARCH_DEPTH);
4681
+ return readNearestField(
4682
+ startDir,
4683
+ "package.json",
4684
+ "name",
4685
+ readFile,
4686
+ MAX_PACKAGE_SEARCH_DEPTH,
4687
+ nonEmptyString
4688
+ );
4643
4689
  }
4644
4690
  function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
4645
4691
  return readNearestField(
@@ -4647,7 +4693,18 @@ function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
4647
4693
  RETICLE_CONFIG_BASENAME,
4648
4694
  "projectId",
4649
4695
  readFile,
4650
- MAX_CONFIG_SEARCH_DEPTH
4696
+ MAX_CONFIG_SEARCH_DEPTH,
4697
+ nonEmptyString
4698
+ );
4699
+ }
4700
+ function readConfiguredPort(startDir, readFile = readFileOrThrow) {
4701
+ return readNearestField(
4702
+ startDir,
4703
+ RETICLE_CONFIG_BASENAME,
4704
+ "port",
4705
+ readFile,
4706
+ MAX_CONFIG_SEARCH_DEPTH,
4707
+ tcpPort
4651
4708
  );
4652
4709
  }
4653
4710
  function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName, readConfiguredId = readConfiguredProjectId) {
@@ -4669,6 +4726,9 @@ function stateHome(env = process.env) {
4669
4726
  return override !== void 0 && override.length > 0 ? override : (0, import_node_path3.join)((0, import_node_os.homedir)(), ReticleDir.ROOT);
4670
4727
  }
4671
4728
 
4729
+ // src/plugin-name.ts
4730
+ var RETICLE_VITE_PLUGIN_NAME = "reticle";
4731
+
4672
4732
  // src/discover-port.ts
4673
4733
  function isAlive(pid) {
4674
4734
  try {
@@ -4678,7 +4738,7 @@ function isAlive(pid) {
4678
4738
  return false;
4679
4739
  }
4680
4740
  }
4681
- function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive) {
4741
+ function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive, configured) {
4682
4742
  const entries = [];
4683
4743
  let files;
4684
4744
  try {
@@ -4696,7 +4756,27 @@ function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive) {
4696
4756
  } catch {
4697
4757
  }
4698
4758
  }
4699
- return pickDaemonPort(entries, projectId, alive) ?? void 0;
4759
+ return pickDaemonPort(entries, projectId, alive, configured) ?? void 0;
4760
+ }
4761
+ var portConflictWarning = (explicit, configured, used) => `[${RETICLE_VITE_PLUGIN_NAME}] reticle({ port: ${String(explicit)} }) and "port": ${String(configured)} in .reticle.json disagree; connecting to ${String(used)}. The daemon and the CLI follow .reticle.json, so drop the port option from reticle() or make the two match.`;
4762
+ function chooseDaemonPort(sources) {
4763
+ const { discovered, configured, explicit } = sources;
4764
+ const port = discovered ?? configured ?? explicit;
4765
+ if (explicit === void 0 || configured === void 0 || port === void 0) {
4766
+ return { port, warning: void 0 };
4767
+ }
4768
+ return {
4769
+ port,
4770
+ warning: explicit === configured ? void 0 : portConflictWarning(explicit, configured, port)
4771
+ };
4772
+ }
4773
+ function resolveDaemonPort(explicit, projectId, cwd) {
4774
+ const configured = readConfiguredPort(cwd);
4775
+ return chooseDaemonPort({
4776
+ discovered: discoverDaemonPort(projectId, stateHome(), isAlive, configured),
4777
+ configured,
4778
+ explicit
4779
+ });
4700
4780
  }
4701
4781
 
4702
4782
  // src/announce.ts
@@ -4811,6 +4891,51 @@ function stampSvelte(code, id, load = defaultLoadCompiler) {
4811
4891
  return out;
4812
4892
  }
4813
4893
 
4894
+ // src/stamping.ts
4895
+ var import_core6 = require("@babel/core");
4896
+ var import_babel_plugin = __toESM(require("@reticlehq/babel-plugin"), 1);
4897
+ var import_ignore = require("@reticlehq/babel-plugin/ignore");
4898
+ var JSX_FILE = /\.[jt]sx$/;
4899
+ var VIRTUAL_PREFIX = "\0";
4900
+ var NODE_MODULES = "node_modules";
4901
+ function stampableId(id) {
4902
+ if (id.startsWith(VIRTUAL_PREFIX)) return null;
4903
+ if (id.includes(NODE_MODULES)) return null;
4904
+ return id.split("?")[0] ?? id;
4905
+ }
4906
+ var TEST_FILE = /(?:\.(?:test|spec)\.[^./]+$)|(?:^|\/)__tests__\//;
4907
+ function shouldStamp(id) {
4908
+ const clean = stampableId(id);
4909
+ if (null === clean) return false;
4910
+ if (TEST_FILE.test(clean)) return false;
4911
+ return JSX_FILE.test(clean);
4912
+ }
4913
+ function shouldStampSvelte(id) {
4914
+ const clean = stampableId(id);
4915
+ return clean !== null && SVELTE_FILE.test(clean);
4916
+ }
4917
+ function stamp(code, id) {
4918
+ const out = (0, import_core6.transformSync)(code, {
4919
+ filename: id,
4920
+ plugins: [import_babel_plugin.default],
4921
+ parserOpts: { plugins: ["jsx", "typescript"] },
4922
+ sourceMaps: true,
4923
+ configFile: false,
4924
+ babelrc: false
4925
+ });
4926
+ if (out?.code === void 0 || null === out.code) return null;
4927
+ return {
4928
+ code: out.code,
4929
+ map: out.map === void 0 || null === out.map ? null : JSON.stringify(out.map)
4930
+ };
4931
+ }
4932
+ function optsOutOfStamping(code, id) {
4933
+ return (shouldStamp(id) || shouldStampSvelte(id)) && (0, import_ignore.isSourceStampingIgnored)(code);
4934
+ }
4935
+ function ignoredFileNotice(id) {
4936
+ return `[reticle] ${id} opts out of source stamping (${RETICLE_IGNORE_MARKER} on its first line) \u2014 elements in it will have no file:line.`;
4937
+ }
4938
+
4814
4939
  // src/installed.ts
4815
4940
  var import_node_fs5 = require("node:fs");
4816
4941
  var import_node_path7 = require("node:path");
@@ -4909,14 +5034,234 @@ function optimizerOptions(key, inherited, add) {
4909
5034
  };
4910
5035
  }
4911
5036
 
4912
- // src/plugin-name.ts
4913
- var RETICLE_VITE_PLUGIN_NAME = "reticle";
5037
+ // src/disabled-browser-stub.ts
5038
+ var RETICLE_DISABLED_STUB = "\0reticle-disabled-stub";
5039
+ var BUILD_COMMAND = "build";
5040
+ var PRODUCTION_MODE = "production";
5041
+ var isReticleDisabledBuild = (desktop, command, mode) => BUILD_COMMAND === command && (!desktop || mode === void 0 || PRODUCTION_MODE === mode);
5042
+ var RETICLE_DISABLED_STUB_CODE = `
5043
+ export class Reticle {
5044
+ connect() {}
5045
+ signal() {}
5046
+ state() {}
5047
+ renderCommit() {}
5048
+ describe() {}
5049
+ endSession() {}
5050
+ observeHotUpdates() {}
5051
+ disconnect() {}
5052
+
5053
+ get connected() {
5054
+ return false;
5055
+ }
5056
+ }
5057
+
5058
+ export const reticle = new Reticle();
5059
+
5060
+ export const SESSION_AUTO = 'auto';
5061
+
5062
+ export function setIgnoreSelectors() {}
5063
+
5064
+ export function registerAdapter() {}
5065
+ export function identifyComponent() {
5066
+ return null;
5067
+ }
5068
+ export function readComponentState() {
5069
+ return undefined;
5070
+ }
5071
+ export function elementHasHoverHandlers() {
5072
+ return false;
5073
+ }
5074
+ export function adapterNames() {
5075
+ return [];
5076
+ }
5077
+
5078
+ export function registerStore() {}
5079
+ export function unregisterStore() {}
5080
+ export function storeNames() {
5081
+ return [];
5082
+ }
5083
+ export function readStores() {
5084
+ return {};
5085
+ }
5086
+ export function readStoresWithTruncation() {
5087
+ return { stores: {} };
5088
+ }
5089
+ export function sourceOwner() {
5090
+ return undefined;
5091
+ }
5092
+ export function markAdapterSource() {}
5093
+
5094
+ export function tanstackQueryStore() {
5095
+ return undefined;
5096
+ }
5097
+ export function jotaiStore() {
5098
+ return undefined;
5099
+ }
5100
+ export function xstateStore() {
5101
+ return undefined;
5102
+ }
5103
+ export function valtioStore() {
5104
+ return undefined;
5105
+ }
5106
+ export function mobxStore() {
5107
+ return undefined;
5108
+ }
5109
+ export function recoilStore() {
5110
+ return undefined;
5111
+ }
5112
+ export function svelteStore() {
5113
+ return undefined;
5114
+ }
5115
+ export function piniaStore() {
5116
+ return undefined;
5117
+ }
5118
+ export function pushStore() {
5119
+ return undefined;
5120
+ }
5121
+
5122
+ export function registerCapabilities() {}
5123
+ export function getCapabilities() {
5124
+ return {};
5125
+ }
5126
+ export function hasCapabilities() {
5127
+ return false;
5128
+ }
5129
+ export function setCapabilitiesListener() {}
5130
+
5131
+ export function createReticleEmitter() {
5132
+ return {
5133
+ signal() {},
5134
+ state() {},
5135
+ };
5136
+ }
5137
+
5138
+ export function commitAndSignal() {}
5139
+ export function registerReticleDomain() {}
5140
+
5141
+ export function buildSnapshot() {
5142
+ return {};
5143
+ }
5144
+
5145
+ export function inspectChart() {
5146
+ return {};
5147
+ }
5148
+ export function canvasChartData() {
5149
+ return {};
5150
+ }
5151
+
5152
+ export function matchQuery() {
5153
+ return false;
5154
+ }
5155
+ export function runQuery() {
5156
+ return undefined;
5157
+ }
5158
+
5159
+ export function executeAction() {
5160
+ return undefined;
5161
+ }
5162
+ export function executeSequence() {
5163
+ return undefined;
5164
+ }
5165
+
5166
+ export function describe() {
5167
+ return undefined;
5168
+ }
5169
+ export function getRole() {
5170
+ return undefined;
5171
+ }
5172
+ export function getAccessibleName() {
5173
+ return undefined;
5174
+ }
5175
+ export function getStates() {
5176
+ return {};
5177
+ }
5178
+ export function isVisible() {
5179
+ return false;
5180
+ }
5181
+
5182
+ export class RefRegistry {
5183
+ get size() {
5184
+ return 0;
5185
+ }
5186
+
5187
+ markEdited() {}
5188
+
5189
+ mintedBeforeLastEdit() {
5190
+ return false;
5191
+ }
5192
+
5193
+ refFor() {
5194
+ return undefined;
5195
+ }
5196
+
5197
+ resolve() {
5198
+ return null;
5199
+ }
5200
+ }
5201
+
5202
+ export const refs = new RefRegistry();
5203
+
5204
+ export class Annotator {
5205
+ constructor() {}
5206
+
5207
+ get active() {
5208
+ return false;
5209
+ }
5210
+
5211
+ get markCount() {
5212
+ return 0;
5213
+ }
5214
+
5215
+ mount() {}
5216
+ unmount() {}
5217
+ }
5218
+
5219
+ export function installAnnotator() {}
5220
+
5221
+ export function resolveMarkAnchor() {
5222
+ return undefined;
5223
+ }
5224
+ `;
5225
+
5226
+ // src/connect-args.ts
5227
+ function connectArgs(options) {
5228
+ const args = {};
5229
+ const port = options.port ?? RETICLE_DEFAULT_PORT;
5230
+ if (port !== RETICLE_DEFAULT_PORT) args["url"] = bridgeWsUrl(port);
5231
+ if (options.session !== void 0) args["session"] = options.session;
5232
+ if (options.projectId !== void 0) args["projectId"] = options.projectId;
5233
+ if (options.token !== void 0) args["token"] = options.token;
5234
+ if (options.root !== void 0 && options.root.length > 0) args["root"] = options.root;
5235
+ if (options.sdkVersion !== void 0 && options.sdkVersion.length > 0) {
5236
+ args["sdkVersion"] = options.sdkVersion;
5237
+ }
5238
+ if (true === options.desktop) args["allowInProduction"] = true;
5239
+ if (true === options.captureNetworkBodies || "1" === process.env["VITE_RETICLE_CAPTURE_BODIES"]) {
5240
+ args["captureNetworkBodies"] = true;
5241
+ }
5242
+ if (false === options.sourceMapping) args["sourceMapping"] = false;
5243
+ if (false === options.captureErrorBodies || "1" === process.env["VITE_RETICLE_NO_ERROR_BODIES"]) {
5244
+ args["captureErrorBodies"] = false;
5245
+ }
5246
+ const envBodyChars = Number(process.env["VITE_RETICLE_BODY_MAX_CHARS"]);
5247
+ const bodyChars = options.networkBodyMaxChars ?? (Number.isFinite(envBodyChars) && envBodyChars > 0 ? envBodyChars : void 0);
5248
+ if (bodyChars !== void 0) {
5249
+ args["networkBodyMaxChars"] = bodyChars;
5250
+ }
5251
+ if (true === options.exposePresenter || "1" === process.env["VITE_RETICLE_EXPOSE_PRESENTER"]) {
5252
+ args["exposePresenter"] = true;
5253
+ }
5254
+ if (true === options.allowNonLocalhost || "1" === process.env["VITE_RETICLE_ALLOW_NON_LOCALHOST"]) {
5255
+ args["allowNonLocalhost"] = true;
5256
+ }
5257
+ return Object.keys(args).length > 0 ? JSON.stringify(args) : "";
5258
+ }
4914
5259
 
4915
5260
  // src/injection-postcondition.ts
4916
5261
  var DEV_INJECTION_GRACE_MS = 1e4;
4917
5262
  var notInjectedMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not inject reticle.connect(): the HTML entry module was never matched, so this app carries no instrumentation and will never connect. Check that index.html references your entry with a <script type="module" src="...">, or pass \`inject: false\` and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself. The plugin still inlines that define; a connect without it is refused.`;
4918
5263
  var unconfirmedInjectionMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not confirm reticle.connect() was injected: the HTML entry module was not transformed this session. That is expected when Vite served it from its transform cache. If the app does not appear in \`reticle status\`, restart the dev server with \`--force\` to bypass the cache, then check that index.html references your entry with a <script type="module" src="...">.`;
4919
- var htmlHookNeverRanMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] this app will never connect: the dev server never asked this plugin to transform any HTML, so reticle.connect() was never added to the page. That usually means your framework renders its own HTML instead of serving index.html \u2014 SvelteKit, Nuxt, Astro, React Router (framework mode) and TanStack Start all do. Fix: import '@reticlehq/browser' and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself from your app entry file, and pass \`inject: false\` to this plugin so the two do not both try.`;
5264
+ var htmlHookNeverRanMessage = (connectModule) => `[${RETICLE_VITE_PLUGIN_NAME}] this app will never connect: the dev server never asked this plugin to transform any HTML and nothing loaded its connect module, so reticle.connect() never reached the page. That usually means your framework renders its own HTML instead of serving index.html \u2014 SvelteKit, Nuxt, Astro, React Router (framework mode) and TanStack Start all do. Fix: in dev, dynamically import '${connectModule}' from your client entry file \u2014 this plugin serves that module, so leave \`inject\` on \u2014 or import '@reticlehq/browser' there and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself.`;
4920
5265
  var defaultSchedule = (run, ms) => {
4921
5266
  const timer = setTimeout(run, ms);
4922
5267
  timer.unref?.();
@@ -4925,9 +5270,9 @@ function createInjectionWatch(deps) {
4925
5270
  let htmlRequested = false;
4926
5271
  const isDocumentRequest = (req) => true === req.headers?.accept?.includes("text/html");
4927
5272
  const checkHtmlHookRan = () => {
4928
- if (deps.desktop || !deps.inject || deps.htmlTransformed()) return;
5273
+ if (deps.desktop || !deps.inject || deps.connectDelivered()) return;
4929
5274
  if (!htmlRequested) return;
4930
- deps.warn(htmlHookNeverRanMessage());
5275
+ deps.warn(htmlHookNeverRanMessage(deps.connectModule()));
4931
5276
  };
4932
5277
  const noteHtmlRequest = () => {
4933
5278
  if (htmlRequested) return;
@@ -4967,9 +5312,6 @@ function isVitestBrowserServer(config) {
4967
5312
  var RETICLE_PACKAGE2 = "@reticlehq/react";
4968
5313
  var RETICLE_SENSOR = "@reticlehq/browser";
4969
5314
  var RETICLE_TOKEN_GLOBAL = "__RETICLE_TOKEN__";
4970
- var JSX_FILE = /\.[jt]sx$/;
4971
- var VIRTUAL_PREFIX = "\0";
4972
- var NODE_MODULES = "node_modules";
4973
5315
  var RETICLE_CONNECT_MODULE = "/@reticle-connect";
4974
5316
  function connectModuleUrl(base) {
4975
5317
  if (void 0 === base || !base.startsWith("/")) return RETICLE_CONNECT_MODULE;
@@ -5004,34 +5346,6 @@ function isHtmlEntry(id, specifier, root) {
5004
5346
  }
5005
5347
  return candidate.endsWith(target.startsWith("/") ? target : `/${target}`);
5006
5348
  }
5007
- function stampableId(id) {
5008
- if (id.startsWith(VIRTUAL_PREFIX)) return null;
5009
- if (id.includes(NODE_MODULES)) return null;
5010
- return id.split("?")[0] ?? id;
5011
- }
5012
- function shouldStamp(id) {
5013
- const clean = stampableId(id);
5014
- return clean !== null && JSX_FILE.test(clean);
5015
- }
5016
- function shouldStampSvelte(id) {
5017
- const clean = stampableId(id);
5018
- return clean !== null && SVELTE_FILE.test(clean);
5019
- }
5020
- function stamp(code, id) {
5021
- const out = (0, import_core6.transformSync)(code, {
5022
- filename: id,
5023
- plugins: [import_babel_plugin.default],
5024
- parserOpts: { plugins: ["jsx", "typescript"] },
5025
- sourceMaps: true,
5026
- configFile: false,
5027
- babelrc: false
5028
- });
5029
- if (out?.code === void 0 || null === out.code) return null;
5030
- return {
5031
- code: out.code,
5032
- map: out.map === void 0 || null === out.map ? null : JSON.stringify(out.map)
5033
- };
5034
- }
5035
5349
  function readPairingToken() {
5036
5350
  const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
5037
5351
  const dir = override !== void 0 && override.length > 0 ? override : (0, import_node_path8.join)((0, import_node_os2.homedir)(), ReticleDir.ROOT);
@@ -5046,33 +5360,6 @@ function warnIfTokenMissing(token) {
5046
5360
  }
5047
5361
  return token;
5048
5362
  }
5049
- function connectArgs(options) {
5050
- const args = {};
5051
- const port = options.port ?? RETICLE_DEFAULT_PORT;
5052
- if (port !== RETICLE_DEFAULT_PORT) args["url"] = bridgeWsUrl(port);
5053
- if (options.session !== void 0) args["session"] = options.session;
5054
- if (options.projectId !== void 0) args["projectId"] = options.projectId;
5055
- if (options.token !== void 0) args["token"] = options.token;
5056
- if (options.root !== void 0 && options.root.length > 0) args["root"] = options.root;
5057
- if (options.sdkVersion !== void 0 && options.sdkVersion.length > 0) {
5058
- args["sdkVersion"] = options.sdkVersion;
5059
- }
5060
- if (true === options.desktop) args["allowInProduction"] = true;
5061
- if (true === options.captureNetworkBodies || "1" === process.env["VITE_RETICLE_CAPTURE_BODIES"]) {
5062
- args["captureNetworkBodies"] = true;
5063
- }
5064
- if (false === options.sourceMapping) args["sourceMapping"] = false;
5065
- if (false === options.captureErrorBodies || "1" === process.env["VITE_RETICLE_NO_ERROR_BODIES"]) {
5066
- args["captureErrorBodies"] = false;
5067
- }
5068
- if (true === options.exposePresenter || "1" === process.env["VITE_RETICLE_EXPOSE_PRESENTER"]) {
5069
- args["exposePresenter"] = true;
5070
- }
5071
- if (true === options.allowNonLocalhost || "1" === process.env["VITE_RETICLE_ALLOW_NON_LOCALHOST"]) {
5072
- args["allowNonLocalhost"] = true;
5073
- }
5074
- return Object.keys(args).length > 0 ? JSON.stringify(args) : "";
5075
- }
5076
5363
  var RETICLE_DEV_MODULE_CANDIDATES = [
5077
5364
  "src/reticle-dev.ts",
5078
5365
  "src/reticle-dev.js",
@@ -5131,13 +5418,22 @@ function reticle(options = {}) {
5131
5418
  let htmlEntrySpecifier;
5132
5419
  let root;
5133
5420
  let command;
5421
+ let mode;
5422
+ const disabledBuild = () => isReticleDisabledBuild(desktop, command, mode);
5134
5423
  let base;
5135
5424
  let vitestBrowser = false;
5136
5425
  const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
5137
5426
  let injected = false;
5427
+ const announcedIgnored = /* @__PURE__ */ new Set();
5138
5428
  let htmlTransformed = false;
5429
+ let connectDelivered = false;
5430
+ let lastPortWarning;
5139
5431
  const resolveLazy = () => {
5140
- const port = resolved.port ?? discoverDaemonPort(resolved.projectId);
5432
+ const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, process.cwd());
5433
+ if (warning !== void 0 && warning !== lastPortWarning) {
5434
+ lastPortWarning = warning;
5435
+ warn(warning);
5436
+ }
5141
5437
  const withPort = port !== void 0 ? { ...resolved, port } : resolved;
5142
5438
  const token = withPort.token ?? readPairingToken();
5143
5439
  const withToken = token !== void 0 ? { ...withPort, token } : withPort;
@@ -5152,15 +5448,12 @@ function reticle(options = {}) {
5152
5448
  desktop,
5153
5449
  inject,
5154
5450
  injected: () => injected,
5155
- htmlTransformed: () => htmlTransformed,
5451
+ connectDelivered: () => htmlTransformed || connectDelivered,
5452
+ connectModule: () => connectModuleUrl(base),
5156
5453
  warn
5157
5454
  });
5158
5455
  return {
5159
5456
  name: RETICLE_VITE_PLUGIN_NAME,
5160
- // Web: serve-only, so a production bundle can never carry the SDK — gating is the tool's job.
5161
- // Desktop: a packaged renderer IS a production build with no dev server, so the plugin must also
5162
- // run for `vite build` or the shipped app has no connect() at all.
5163
- ...true === options.desktop ? {} : { apply: "serve" },
5164
5457
  enforce: "pre",
5165
5458
  /**
5166
5459
  * Declare the SDK itself and the optimizer cache fingerprint.
@@ -5244,6 +5537,10 @@ function reticle(options = {}) {
5244
5537
  };
5245
5538
  },
5246
5539
  transform(code, id) {
5540
+ if (disabledBuild()) return null;
5541
+ if (!connectDelivered && code.includes(RETICLE_TOKEN_GLOBAL) && !id.includes(NODE_MODULES)) {
5542
+ connectDelivered = true;
5543
+ }
5247
5544
  if (desktop && inject && isHtmlEntry(id, htmlEntrySpecifier, root)) {
5248
5545
  injected = true;
5249
5546
  const withConnect = `${connectModuleSource(resolveLazy())}
@@ -5252,6 +5549,13 @@ ${code}`;
5252
5549
  return stamped ?? { code: withConnect, map: null };
5253
5550
  }
5254
5551
  if (!sourceMapping) return null;
5552
+ if (optsOutOfStamping(code, id)) {
5553
+ if (!announcedIgnored.has(id)) {
5554
+ announcedIgnored.add(id);
5555
+ warn(ignoredFileNotice(id));
5556
+ }
5557
+ return null;
5558
+ }
5255
5559
  if (shouldStampSvelte(id)) {
5256
5560
  const stamped = stampSvelte(code, id);
5257
5561
  return null === stamped ? null : { code: stamped, map: null };
@@ -5260,20 +5564,28 @@ ${code}`;
5260
5564
  return stamp(code, id);
5261
5565
  },
5262
5566
  resolveId(id, importer) {
5567
+ if (disabledBuild() && id === RETICLE_SENSOR) {
5568
+ return RETICLE_DISABLED_STUB;
5569
+ }
5263
5570
  if (desktop && inject && importer !== void 0 && importer.includes(".html")) {
5264
5571
  htmlEntrySpecifier = id;
5265
5572
  }
5266
5573
  return inject && id === RETICLE_CONNECT_MODULE ? RETICLE_CONNECT_MODULE : null;
5267
5574
  },
5268
5575
  load(id) {
5576
+ if (disabledBuild() && id === RETICLE_DISABLED_STUB) {
5577
+ return RETICLE_DISABLED_STUB_CODE;
5578
+ }
5269
5579
  if (!inject || id !== RETICLE_CONNECT_MODULE) return null;
5270
5580
  const source = currentConnectSource();
5271
5581
  lastServedConnectSource = source;
5582
+ connectDelivered = true;
5272
5583
  return source;
5273
5584
  },
5274
5585
  configResolved(config) {
5275
5586
  root = config.root;
5276
5587
  command = config.command;
5588
+ mode = config.mode;
5277
5589
  base = config.base;
5278
5590
  vitestBrowser = isVitestBrowserServer(config);
5279
5591
  },
@@ -5298,7 +5610,6 @@ ${code}`;
5298
5610
  * appearing IS a change), and makes the module inert once it has settled.
5299
5611
  */
5300
5612
  configureServer(server) {
5301
- if (!inject) return;
5302
5613
  const announce = () => {
5303
5614
  const address = server.httpServer?.address();
5304
5615
  const port = "object" === typeof address && null !== address && void 0 !== address ? address.port : void 0;
@@ -5323,6 +5634,7 @@ ${code}`;
5323
5634
  } else {
5324
5635
  announce();
5325
5636
  }
5637
+ if (!inject) return;
5326
5638
  server.middlewares.use((req, _res, next) => {
5327
5639
  if (!desktop && watch.isDocumentRequest(req)) watch.noteHtmlRequest();
5328
5640
  const requestPath = (req.url ?? "").split("?")[0];
@@ -5343,13 +5655,14 @@ ${code}`;
5343
5655
  * build that could not instrument must fail loudly instead of shipping a binary that lies.
5344
5656
  */
5345
5657
  buildEnd() {
5346
- if (!desktop || !inject || injected) return;
5658
+ if (!desktop || !inject || injected || disabledBuild()) return;
5347
5659
  throw new Error(notInjectedMessage());
5348
5660
  },
5349
5661
  checkInjectedForTest: watch.checkInjected,
5350
5662
  checkHtmlHookForTest: watch.checkHtmlHookRan,
5351
5663
  injectionWatchForTest: watch,
5352
5664
  transformIndexHtml() {
5665
+ if (disabledBuild()) return [];
5353
5666
  htmlTransformed = true;
5354
5667
  if (desktop && inject && "serve" === command) watch.armDesktopCheck();
5355
5668
  if (!inject || desktop) return [];