@reticlehq/vite-plugin 2.13.0 → 2.14.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
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  RETICLE_VITE_PLUGIN_NAME: () => RETICLE_VITE_PLUGIN_NAME,
39
39
  connectChurnWarning: () => connectChurnWarning,
40
40
  connectModuleSource: () => connectModuleSource,
41
+ connectModuleUrl: () => connectModuleUrl,
41
42
  findDevModule: () => findDevModule,
42
43
  installedSdk: () => installedSdk,
43
44
  readPairingToken: () => readPairingToken,
@@ -155,7 +156,9 @@ var ReticleEnv = {
155
156
  * exhaust, so the one spec that proves the proxy SURVIVES exhaustion could not run at all without
156
157
  * shortening it. A budget nobody can reach in a test is a budget nobody tests.
157
158
  */
158
- RECONNECT_ATTEMPTS: "RETICLE_RECONNECT_ATTEMPTS"
159
+ RECONNECT_ATTEMPTS: "RETICLE_RECONNECT_ATTEMPTS",
160
+ /** Quiet window before an abandoned MCP stdio proxy exits; `0` disables the watcher. */
161
+ MCP_PROXY_IDLE: "RETICLE_MCP_PROXY_IDLE_MS"
159
162
  };
160
163
  var TRANSPORT_LIMITS = {
161
164
  MAX_MESSAGE_BYTES: 1024 * 1024,
@@ -311,6 +314,16 @@ var EventType = {
311
314
  * `data: { href }` — the URL the page asked to open, when it named one.
312
315
  */
313
316
  CONTEXT_OPENED: "context.opened",
317
+ /**
318
+ * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
319
+ *
320
+ * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
321
+ * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
322
+ * driven click made the tab permanently unresponsive with no recovery from inside the session.
323
+ * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
324
+ * the answer given, ride out as an event.
325
+ */
326
+ DIALOG_OPENED: "dialog.opened",
314
327
  /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
315
328
  * wasted re-renders show up here without a per-render flood. */
316
329
  RENDER_COMMIT: "render.commit",
@@ -4787,6 +4800,25 @@ function optimizerOptions(key, inherited, add) {
4787
4800
  };
4788
4801
  }
4789
4802
 
4803
+ // src/watch-ignore.ts
4804
+ function mergeIgnored(existing, ours) {
4805
+ if (void 0 === existing || null === existing) return [ours];
4806
+ const listed = Array.isArray(existing) ? existing : [existing];
4807
+ return [...listed.filter(isWatchPattern), ours];
4808
+ }
4809
+ function isWatchPattern(value) {
4810
+ return "string" === typeof value || value instanceof RegExp || "function" === typeof value;
4811
+ }
4812
+
4813
+ // src/vitest-browser.ts
4814
+ function isVitestBrowserServer(config) {
4815
+ const test = config.test;
4816
+ if (null === test || "object" !== typeof test) return false;
4817
+ const browser = test.browser;
4818
+ if (null === browser || "object" !== typeof browser) return false;
4819
+ return true === browser.enabled;
4820
+ }
4821
+
4790
4822
  // src/index.ts
4791
4823
  var RETICLE_VITE_PLUGIN_NAME = "reticle";
4792
4824
  var RETICLE_PACKAGE2 = "@reticlehq/react";
@@ -4796,6 +4828,13 @@ var JSX_FILE = /\.[jt]sx$/;
4796
4828
  var VIRTUAL_PREFIX = "\0";
4797
4829
  var NODE_MODULES = "node_modules";
4798
4830
  var RETICLE_CONNECT_MODULE = "/@reticle-connect";
4831
+ function connectModuleUrl(base) {
4832
+ if (void 0 === base || !base.startsWith("/")) return RETICLE_CONNECT_MODULE;
4833
+ let end = base.length;
4834
+ while (0 < end && "/" === base[end - 1]) end -= 1;
4835
+ const trimmed = base.slice(0, end);
4836
+ return 0 === trimmed.length ? RETICLE_CONNECT_MODULE : `${trimmed}${RETICLE_CONNECT_MODULE}`;
4837
+ }
4799
4838
  var RENDER_PREHOOK_SOURCE = `(function(){try{
4800
4839
  var K='__REACT_DEVTOOLS_GLOBAL_HOOK__',P='${RETICLE_RENDER_PREHOOK}';
4801
4840
  if(globalThis[P])return;
@@ -4880,6 +4919,10 @@ function connectArgs(options) {
4880
4919
  if (true === options.captureNetworkBodies || "1" === process.env["VITE_RETICLE_CAPTURE_BODIES"]) {
4881
4920
  args["captureNetworkBodies"] = true;
4882
4921
  }
4922
+ if (false === options.sourceMapping) args["sourceMapping"] = false;
4923
+ if (false === options.captureErrorBodies || "1" === process.env["VITE_RETICLE_NO_ERROR_BODIES"]) {
4924
+ args["captureErrorBodies"] = false;
4925
+ }
4883
4926
  if (true === options.exposePresenter || "1" === process.env["VITE_RETICLE_EXPOSE_PRESENTER"]) {
4884
4927
  args["exposePresenter"] = true;
4885
4928
  }
@@ -4946,6 +4989,8 @@ function reticle(options = {}) {
4946
4989
  let htmlEntrySpecifier;
4947
4990
  let root;
4948
4991
  let command;
4992
+ let base;
4993
+ let vitestBrowser = false;
4949
4994
  const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
4950
4995
  let injected = false;
4951
4996
  const resolveLazy = () => {
@@ -5012,7 +5057,7 @@ function reticle(options = {}) {
5012
5057
  // Appends to the app's list rather than replacing it, so nothing it already excluded is lost.
5013
5058
  server: {
5014
5059
  watch: {
5015
- ignored: [...config.server?.watch?.ignored ?? [], JOURNAL_IGNORE]
5060
+ ignored: mergeIgnored(config.server?.watch?.ignored, JOURNAL_IGNORE)
5016
5061
  }
5017
5062
  },
5018
5063
  // Expose the daemon's pairing token to hand-written connects in the same Vite app. The
@@ -5096,6 +5141,8 @@ ${code}`;
5096
5141
  configResolved(config) {
5097
5142
  root = config.root;
5098
5143
  command = config.command;
5144
+ base = config.base;
5145
+ vitestBrowser = isVitestBrowserServer(config);
5099
5146
  },
5100
5147
  /**
5101
5148
  * Serve the connect module fresh, every time.
@@ -5148,7 +5195,8 @@ ${code}`;
5148
5195
  announce();
5149
5196
  }
5150
5197
  server.middlewares.use((req, _res, next) => {
5151
- if ((req.url ?? "").split("?")[0] === RETICLE_CONNECT_MODULE) {
5198
+ const requestPath = (req.url ?? "").split("?")[0];
5199
+ if (requestPath === RETICLE_CONNECT_MODULE || requestPath === connectModuleUrl(base)) {
5152
5200
  if (currentConnectSource() !== lastServedConnectSource) {
5153
5201
  connectChanges++;
5154
5202
  if (CONNECT_CHURN_LIMIT === connectChanges) warn(connectChurnWarning());
@@ -5175,6 +5223,7 @@ ${code}`;
5175
5223
  timer.unref?.();
5176
5224
  }
5177
5225
  if (!inject || desktop) return [];
5226
+ if (true !== options.inject && vitestBrowser) return [];
5178
5227
  return [
5179
5228
  // A CLASSIC inline script in <head>, and it has to be both.
5180
5229
  //
@@ -5185,7 +5234,7 @@ ${code}`;
5185
5234
  // `renderers.size === 0`, so the render meter counted zero forever while the docs advertised
5186
5235
  // commit counts. This runs during parse, before any module, and the meter adopts its buffer.
5187
5236
  { tag: "script", children: RENDER_PREHOOK_SOURCE, injectTo: "head-prepend" },
5188
- { tag: "script", attrs: { type: "module", src: RETICLE_CONNECT_MODULE }, injectTo: "body" }
5237
+ { tag: "script", attrs: { type: "module", src: connectModuleUrl(base) }, injectTo: "body" }
5189
5238
  ];
5190
5239
  }
5191
5240
  };
@@ -5200,6 +5249,7 @@ ${code}`;
5200
5249
  RETICLE_VITE_PLUGIN_NAME,
5201
5250
  connectChurnWarning,
5202
5251
  connectModuleSource,
5252
+ connectModuleUrl,
5203
5253
  findDevModule,
5204
5254
  installedSdk,
5205
5255
  readPairingToken,
package/dist/index.d.cts CHANGED
@@ -12,6 +12,21 @@ export declare const RETICLE_TOKEN_GLOBAL = "__RETICLE_TOKEN__";
12
12
  * by the injected <script src> and served by the load hook below.
13
13
  */
14
14
  export declare const RETICLE_CONNECT_MODULE = "/@reticle-connect";
15
+ /**
16
+ * The URL the injected `<script src>` must actually point at: `base` + the module id.
17
+ *
18
+ * {@link RETICLE_CONNECT_MODULE} is a SERVER-ROOT path, and emitting it verbatim is only correct
19
+ * when Vite is serving from the root. Under `base: '/playground/'` the browser asked for
20
+ * `/@reticle-connect`, Vite answered 404 with its own "did you mean /playground/@reticle-connect"
21
+ * hint, and the page rendered perfectly while never connecting (#676) — the exact failure shape
22
+ * Reticle exists to catch, in Reticle's own setup path.
23
+ *
24
+ * Vite does not prefix tags returned from `transformIndexHtml`, so the prefix has to be applied
25
+ * here. Only a path base is joined: Vite serves the dev app from the root when `base` is an
26
+ * external URL, so prefixing a CDN origin onto a dev-server module would point the tag off-host.
27
+ */
28
+ import { type WatchPattern } from './watch-ignore.js';
29
+ export declare function connectModuleUrl(base: string | undefined): string;
15
30
  /**
16
31
  * The pre-hook, as source for an inline <head> script.
17
32
  *
@@ -84,6 +99,14 @@ export interface ReticleVitePluginOptions {
84
99
  * session without editing vite.config.
85
100
  */
86
101
  captureNetworkBodies?: boolean;
102
+ /**
103
+ * Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
104
+ *
105
+ * Reachable here for the reason `captureNetworkBodies` is: the plugin is the only `connect()`
106
+ * most apps ever have. Also settable as `VITE_RETICLE_NO_ERROR_BODIES=1`, which turns it OFF --
107
+ * the inverse of the other env vars, because this is the one that defaults on (#800).
108
+ */
109
+ captureErrorBodies?: boolean;
87
110
  /**
88
111
  * Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
89
112
  *
@@ -125,28 +148,53 @@ export interface ReticleVitePluginOptions {
125
148
  */
126
149
  onWarn?: (message: string) => void;
127
150
  }
151
+ /**
152
+ * The slice of Vite's `UserConfig` the `config` hook reads. Everything is optional AND nullable
153
+ * because that is what Vite declares — a stand-in that is narrower than the real value is not a
154
+ * looser type, it is a stricter one, and it makes the whole plugin unassignable to `Plugin`.
155
+ */
156
+ export interface ViteUserConfigLike {
157
+ optimizeDeps?: {
158
+ include?: string[] | undefined;
159
+ /** Whichever key the app used — the plugin reads both and writes the one this Vite wants. */
160
+ esbuildOptions?: Record<string, unknown> | undefined;
161
+ rolldownOptions?: Record<string, unknown> | undefined;
162
+ } | undefined;
163
+ define?: Record<string, unknown> | undefined;
164
+ root?: string | undefined;
165
+ /**
166
+ * The app's own watcher config. `watch` is nullable because `null` is how a config switches the
167
+ * watcher off, and `ignored` is `unknown` because Vite's `AnymatchMatcher` is not an array — it is
168
+ * a string, a RegExp, a predicate function, or an array of those. Typing it as an array here is
169
+ * the narrowing that made the whole plugin unassignable to Vite's `Plugin`, and it also invited a
170
+ * runtime defect: see `mergeIgnored`.
171
+ */
172
+ server?: {
173
+ watch?: {
174
+ ignored?: unknown;
175
+ } | null | undefined;
176
+ } | undefined;
177
+ /**
178
+ * Vitest's block, when this config belongs to a Vitest run. Read ONLY to spot browser mode — see
179
+ * `isVitestBrowserServer`. Typed as `unknown` because it is Vitest's shape, not Vite's, and this
180
+ * plugin has no business asserting anything about the rest of it.
181
+ */
182
+ test?: unknown;
183
+ }
128
184
  /** Structural Vite plugin shape — avoids a hard dependency on `vite` while staying assignable to its `Plugin`. */
129
185
  export interface ReticleVitePlugin {
130
186
  name: string;
131
187
  /**
132
188
  * Vite's `config` hook. Used to declare the SDK's CJS runtime deps for pre-bundling — see the
133
189
  * implementation for why omitting them makes the whole SDK fail to load on linked setups.
190
+ *
191
+ * METHOD syntax, not a property, and every field it reads is optional-and-nullable. Both halves
192
+ * are load-bearing, and both are the contravariance trap this file's sibling test documents:
193
+ * a property's parameter is checked strictly, so a narrow stand-in REJECTS the wider `UserConfig`
194
+ * Vite actually passes. `server.watch` is where it bit — Vite types it `WatchOptions | null`,
195
+ * `null` being how a config turns the watcher off, and SvelteKit's template does exactly that.
134
196
  */
135
- config?: (config: {
136
- optimizeDeps?: {
137
- include?: string[];
138
- /** Whichever key the app used — the plugin reads both and writes the one this Vite wants. */
139
- esbuildOptions?: Record<string, unknown>;
140
- rolldownOptions?: Record<string, unknown>;
141
- };
142
- define?: Record<string, string>;
143
- root?: string;
144
- server?: {
145
- watch?: {
146
- ignored?: (string | RegExp)[];
147
- };
148
- };
149
- }) => {
197
+ config?(config: ViteUserConfigLike): {
150
198
  optimizeDeps: {
151
199
  include: string[];
152
200
  [optionsKey: string]: unknown;
@@ -154,7 +202,7 @@ export interface ReticleVitePlugin {
154
202
  define: Record<string, string>;
155
203
  server: {
156
204
  watch: {
157
- ignored: (string | RegExp)[];
205
+ ignored: WatchPattern[];
158
206
  };
159
207
  };
160
208
  };
@@ -172,6 +220,9 @@ export interface ReticleVitePlugin {
172
220
  configResolved?: (config: {
173
221
  root?: string;
174
222
  command?: string;
223
+ base?: string;
224
+ /** Vitest's block, read only to spot browser mode. See isVitestBrowserServer. */
225
+ test?: unknown;
175
226
  }) => void;
176
227
  /** Dev-server hook: keeps the served connect module from outliving the token it was built without. */
177
228
  configureServer?: (server: ViteDevServerLike) => void;
package/dist/index.d.ts CHANGED
@@ -12,6 +12,21 @@ export declare const RETICLE_TOKEN_GLOBAL = "__RETICLE_TOKEN__";
12
12
  * by the injected <script src> and served by the load hook below.
13
13
  */
14
14
  export declare const RETICLE_CONNECT_MODULE = "/@reticle-connect";
15
+ /**
16
+ * The URL the injected `<script src>` must actually point at: `base` + the module id.
17
+ *
18
+ * {@link RETICLE_CONNECT_MODULE} is a SERVER-ROOT path, and emitting it verbatim is only correct
19
+ * when Vite is serving from the root. Under `base: '/playground/'` the browser asked for
20
+ * `/@reticle-connect`, Vite answered 404 with its own "did you mean /playground/@reticle-connect"
21
+ * hint, and the page rendered perfectly while never connecting (#676) — the exact failure shape
22
+ * Reticle exists to catch, in Reticle's own setup path.
23
+ *
24
+ * Vite does not prefix tags returned from `transformIndexHtml`, so the prefix has to be applied
25
+ * here. Only a path base is joined: Vite serves the dev app from the root when `base` is an
26
+ * external URL, so prefixing a CDN origin onto a dev-server module would point the tag off-host.
27
+ */
28
+ import { type WatchPattern } from './watch-ignore.js';
29
+ export declare function connectModuleUrl(base: string | undefined): string;
15
30
  /**
16
31
  * The pre-hook, as source for an inline <head> script.
17
32
  *
@@ -84,6 +99,14 @@ export interface ReticleVitePluginOptions {
84
99
  * session without editing vite.config.
85
100
  */
86
101
  captureNetworkBodies?: boolean;
102
+ /**
103
+ * Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
104
+ *
105
+ * Reachable here for the reason `captureNetworkBodies` is: the plugin is the only `connect()`
106
+ * most apps ever have. Also settable as `VITE_RETICLE_NO_ERROR_BODIES=1`, which turns it OFF --
107
+ * the inverse of the other env vars, because this is the one that defaults on (#800).
108
+ */
109
+ captureErrorBodies?: boolean;
87
110
  /**
88
111
  * Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
89
112
  *
@@ -125,28 +148,53 @@ export interface ReticleVitePluginOptions {
125
148
  */
126
149
  onWarn?: (message: string) => void;
127
150
  }
151
+ /**
152
+ * The slice of Vite's `UserConfig` the `config` hook reads. Everything is optional AND nullable
153
+ * because that is what Vite declares — a stand-in that is narrower than the real value is not a
154
+ * looser type, it is a stricter one, and it makes the whole plugin unassignable to `Plugin`.
155
+ */
156
+ export interface ViteUserConfigLike {
157
+ optimizeDeps?: {
158
+ include?: string[] | undefined;
159
+ /** Whichever key the app used — the plugin reads both and writes the one this Vite wants. */
160
+ esbuildOptions?: Record<string, unknown> | undefined;
161
+ rolldownOptions?: Record<string, unknown> | undefined;
162
+ } | undefined;
163
+ define?: Record<string, unknown> | undefined;
164
+ root?: string | undefined;
165
+ /**
166
+ * The app's own watcher config. `watch` is nullable because `null` is how a config switches the
167
+ * watcher off, and `ignored` is `unknown` because Vite's `AnymatchMatcher` is not an array — it is
168
+ * a string, a RegExp, a predicate function, or an array of those. Typing it as an array here is
169
+ * the narrowing that made the whole plugin unassignable to Vite's `Plugin`, and it also invited a
170
+ * runtime defect: see `mergeIgnored`.
171
+ */
172
+ server?: {
173
+ watch?: {
174
+ ignored?: unknown;
175
+ } | null | undefined;
176
+ } | undefined;
177
+ /**
178
+ * Vitest's block, when this config belongs to a Vitest run. Read ONLY to spot browser mode — see
179
+ * `isVitestBrowserServer`. Typed as `unknown` because it is Vitest's shape, not Vite's, and this
180
+ * plugin has no business asserting anything about the rest of it.
181
+ */
182
+ test?: unknown;
183
+ }
128
184
  /** Structural Vite plugin shape — avoids a hard dependency on `vite` while staying assignable to its `Plugin`. */
129
185
  export interface ReticleVitePlugin {
130
186
  name: string;
131
187
  /**
132
188
  * Vite's `config` hook. Used to declare the SDK's CJS runtime deps for pre-bundling — see the
133
189
  * implementation for why omitting them makes the whole SDK fail to load on linked setups.
190
+ *
191
+ * METHOD syntax, not a property, and every field it reads is optional-and-nullable. Both halves
192
+ * are load-bearing, and both are the contravariance trap this file's sibling test documents:
193
+ * a property's parameter is checked strictly, so a narrow stand-in REJECTS the wider `UserConfig`
194
+ * Vite actually passes. `server.watch` is where it bit — Vite types it `WatchOptions | null`,
195
+ * `null` being how a config turns the watcher off, and SvelteKit's template does exactly that.
134
196
  */
135
- config?: (config: {
136
- optimizeDeps?: {
137
- include?: string[];
138
- /** Whichever key the app used — the plugin reads both and writes the one this Vite wants. */
139
- esbuildOptions?: Record<string, unknown>;
140
- rolldownOptions?: Record<string, unknown>;
141
- };
142
- define?: Record<string, string>;
143
- root?: string;
144
- server?: {
145
- watch?: {
146
- ignored?: (string | RegExp)[];
147
- };
148
- };
149
- }) => {
197
+ config?(config: ViteUserConfigLike): {
150
198
  optimizeDeps: {
151
199
  include: string[];
152
200
  [optionsKey: string]: unknown;
@@ -154,7 +202,7 @@ export interface ReticleVitePlugin {
154
202
  define: Record<string, string>;
155
203
  server: {
156
204
  watch: {
157
- ignored: (string | RegExp)[];
205
+ ignored: WatchPattern[];
158
206
  };
159
207
  };
160
208
  };
@@ -172,6 +220,9 @@ export interface ReticleVitePlugin {
172
220
  configResolved?: (config: {
173
221
  root?: string;
174
222
  command?: string;
223
+ base?: string;
224
+ /** Vitest's block, read only to spot browser mode. See isVitestBrowserServer. */
225
+ test?: unknown;
175
226
  }) => void;
176
227
  /** Dev-server hook: keeps the served connect module from outliving the token it was built without. */
177
228
  configureServer?: (server: ViteDevServerLike) => void;
package/dist/index.js CHANGED
@@ -36,6 +36,33 @@ const NODE_MODULES = 'node_modules';
36
36
  * by the injected <script src> and served by the load hook below.
37
37
  */
38
38
  export const RETICLE_CONNECT_MODULE = '/@reticle-connect';
39
+ /**
40
+ * The URL the injected `<script src>` must actually point at: `base` + the module id.
41
+ *
42
+ * {@link RETICLE_CONNECT_MODULE} is a SERVER-ROOT path, and emitting it verbatim is only correct
43
+ * when Vite is serving from the root. Under `base: '/playground/'` the browser asked for
44
+ * `/@reticle-connect`, Vite answered 404 with its own "did you mean /playground/@reticle-connect"
45
+ * hint, and the page rendered perfectly while never connecting (#676) — the exact failure shape
46
+ * Reticle exists to catch, in Reticle's own setup path.
47
+ *
48
+ * Vite does not prefix tags returned from `transformIndexHtml`, so the prefix has to be applied
49
+ * here. Only a path base is joined: Vite serves the dev app from the root when `base` is an
50
+ * external URL, so prefixing a CDN origin onto a dev-server module would point the tag off-host.
51
+ */
52
+ import { mergeIgnored } from './watch-ignore.js';
53
+ import { isVitestBrowserServer } from './vitest-browser.js';
54
+ export function connectModuleUrl(base) {
55
+ if (undefined === base || !base.startsWith('/'))
56
+ return RETICLE_CONNECT_MODULE;
57
+ // Trimmed by slicing rather than with `/\/+$/`: a trailing-slash-run regex is a polynomial
58
+ // backtracking shape over a value that comes out of the user's config, and CodeQL is right to
59
+ // flag it. This is linear and says the same thing.
60
+ let end = base.length;
61
+ while (0 < end && '/' === base[end - 1])
62
+ end -= 1;
63
+ const trimmed = base.slice(0, end);
64
+ return 0 === trimmed.length ? RETICLE_CONNECT_MODULE : `${trimmed}${RETICLE_CONNECT_MODULE}`;
65
+ }
39
66
  /**
40
67
  * The pre-hook, as source for an inline <head> script.
41
68
  *
@@ -207,6 +234,16 @@ function connectArgs(options) {
207
234
  if (true === options.captureNetworkBodies || '1' === process.env['VITE_RETICLE_CAPTURE_BODIES']) {
208
235
  args['captureNetworkBodies'] = true;
209
236
  }
237
+ // Only the OPT-OUT is announced, never the default. The daemon reads absence as "unknown", so
238
+ // sending `true` here would say nothing it does not already assume — while sending `false` is the
239
+ // one fact a red verdict needs to stop prescribing a plugin this project has deliberately muted.
240
+ if (false === options.sourceMapping)
241
+ args['sourceMapping'] = false;
242
+ // The one option that defaults ON, so the env var and the config flag both DISABLE rather than
243
+ // enable. Emitted only when switched off; the default stays implicit in the SDK.
244
+ if (false === options.captureErrorBodies || '1' === process.env['VITE_RETICLE_NO_ERROR_BODIES']) {
245
+ args['captureErrorBodies'] = false;
246
+ }
210
247
  // Same shape, same reason. Off unless asked for, in a config or for one session.
211
248
  if (true === options.exposePresenter || '1' === process.env['VITE_RETICLE_EXPOSE_PRESENTER']) {
212
249
  args['exposePresenter'] = true;
@@ -344,6 +381,10 @@ export function reticle(options = {}) {
344
381
  let root;
345
382
  /** 'serve' | 'build'. The dev check only applies to serve; buildEnd covers the other. */
346
383
  let command;
384
+ /** Vite's resolved `base`. Undefined until configResolved, which is before any HTML is served. */
385
+ let base;
386
+ /** True only when THIS server is Vitest's browser-mode runner — see isVitestBrowserServer. */
387
+ let vitestBrowser = false;
347
388
  const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
348
389
  /** Whether connect() actually reached a module — asserted at buildEnd, never assumed. */
349
390
  let injected = false;
@@ -453,7 +494,7 @@ export function reticle(options = {}) {
453
494
  // Appends to the app's list rather than replacing it, so nothing it already excluded is lost.
454
495
  server: {
455
496
  watch: {
456
- ignored: [...(config.server?.watch?.ignored ?? []), JOURNAL_IGNORE],
497
+ ignored: mergeIgnored(config.server?.watch?.ignored, JOURNAL_IGNORE),
457
498
  },
458
499
  },
459
500
  // Expose the daemon's pairing token to hand-written connects in the same Vite app. The
@@ -550,6 +591,8 @@ export function reticle(options = {}) {
550
591
  configResolved(config) {
551
592
  root = config.root;
552
593
  command = config.command;
594
+ base = config.base;
595
+ vitestBrowser = isVitestBrowserServer(config);
553
596
  },
554
597
  /**
555
598
  * Serve the connect module fresh, every time.
@@ -623,7 +666,11 @@ export function reticle(options = {}) {
623
666
  announce();
624
667
  }
625
668
  server.middlewares.use((req, _res, next) => {
626
- if ((req.url ?? '').split('?')[0] === RETICLE_CONNECT_MODULE) {
669
+ // Matched against BOTH forms: plugin middlewares run ahead of Vite's own base
670
+ // middleware, so the request still carries `base` here, while a middleware-mode host may
671
+ // have stripped it already.
672
+ const requestPath = (req.url ?? '').split('?')[0];
673
+ if (requestPath === RETICLE_CONNECT_MODULE || requestPath === connectModuleUrl(base)) {
627
674
  if (currentConnectSource() !== lastServedConnectSource) {
628
675
  connectChanges++;
629
676
  if (CONNECT_CHURN_LIMIT === connectChanges)
@@ -656,9 +703,12 @@ export function reticle(options = {}) {
656
703
  timer.unref?.();
657
704
  }
658
705
  // Desktop injects via the entry module instead (see transform) — a tag here would be a dead
659
- // URL in a packaged build.
706
+ // URL in a packaged build. A Vitest run gets nothing unless `inject: true` says otherwise —
707
+ // see isVitestBrowserServer.
660
708
  if (!inject || desktop)
661
709
  return [];
710
+ if (true !== options.inject && vitestBrowser)
711
+ return [];
662
712
  return [
663
713
  // A CLASSIC inline script in <head>, and it has to be both.
664
714
  //
@@ -669,7 +719,7 @@ export function reticle(options = {}) {
669
719
  // `renderers.size === 0`, so the render meter counted zero forever while the docs advertised
670
720
  // commit counts. This runs during parse, before any module, and the meter adopts its buffer.
671
721
  { tag: 'script', children: RENDER_PREHOOK_SOURCE, injectTo: 'head-prepend' },
672
- { tag: 'script', attrs: { type: 'module', src: RETICLE_CONNECT_MODULE }, injectTo: 'body' },
722
+ { tag: 'script', attrs: { type: 'module', src: connectModuleUrl(base) }, injectTo: 'body' },
673
723
  ];
674
724
  },
675
725
  };
@@ -56,7 +56,7 @@ export declare const OPTIMIZER_OPTIONS_KEY: {
56
56
  readonly ESBUILD: "esbuildOptions";
57
57
  readonly ROLLDOWN: "rolldownOptions";
58
58
  };
59
- export type OptimizerOptionsKey = (typeof OPTIMIZER_OPTIONS_KEY)[keyof typeof OPTIMIZER_OPTIONS_KEY];
59
+ type OptimizerOptionsKey = (typeof OPTIMIZER_OPTIONS_KEY)[keyof typeof OPTIMIZER_OPTIONS_KEY];
60
60
  /**
61
61
  * Which key carries optimizer options on this Vite.
62
62
  *
@@ -84,3 +84,4 @@ export declare function optimizerOptionsKey(major: number | null): OptimizerOpti
84
84
  * app's own, it meant something, and passing it through unchanged is what produced the warning.
85
85
  */
86
86
  export declare function optimizerOptions(key: OptimizerOptionsKey, inherited: Record<string, unknown>, add: Record<string, string>): Record<string, unknown>;
87
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Telling Vitest's browser-mode runner apart from every other Vite server.
3
+ *
4
+ * Split out of index.ts at the file cap. Small, but it is the discriminator a wrong first cut got
5
+ * wrong in production, so it earns its own file and its own name.
6
+ */
7
+ /**
8
+ * Is THIS server Vitest's browser-mode runner?
9
+ *
10
+ * Vitest browser mode renders each component test into its own iframe served by the same Vite dev
11
+ * server, so `transformIndexHtml` injects into every one of them. The HUD then sits in the test
12
+ * document's hit-test path and intercepts pointer events, and a user who adds Reticle watches their
13
+ * unrelated component tests start timing out on clicks.
14
+ *
15
+ * The signal is `test.browser.enabled` on the RESOLVED config. The first cut used the `VITEST`
16
+ * environment variable and was wrong in a way its own unit tests could not show: `VITEST` means
17
+ * "Vitest is running somewhere in this process", which is ALSO true when a Vitest suite boots an app
18
+ * in order to test it. `frameworks.integration.test.ts` starts a real Vite dev server per example
19
+ * app and asserts Reticle connects; under the env check it injected nothing and the suite failed
20
+ * with "@reticlehq/example-react never connected an Reticle session". CI caught that; three local
21
+ * battery runs did not, because that suite is not the battery.
22
+ *
23
+ * A bare `test` key is wrong the other way — most projects configure Vitest and are not under it.
24
+ * `test.browser.enabled` is true only for the server actually serving browser-mode test iframes,
25
+ * which is the one case with a HUD to suppress.
26
+ *
27
+ * An explicit `inject: true` still beats it: the check is a default chosen on the user's behalf.
28
+ */
29
+ export declare function isVitestBrowserServer(config: {
30
+ test?: unknown;
31
+ }): boolean;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Telling Vitest's browser-mode runner apart from every other Vite server.
3
+ *
4
+ * Split out of index.ts at the file cap. Small, but it is the discriminator a wrong first cut got
5
+ * wrong in production, so it earns its own file and its own name.
6
+ */
7
+ /**
8
+ * Is THIS server Vitest's browser-mode runner?
9
+ *
10
+ * Vitest browser mode renders each component test into its own iframe served by the same Vite dev
11
+ * server, so `transformIndexHtml` injects into every one of them. The HUD then sits in the test
12
+ * document's hit-test path and intercepts pointer events, and a user who adds Reticle watches their
13
+ * unrelated component tests start timing out on clicks.
14
+ *
15
+ * The signal is `test.browser.enabled` on the RESOLVED config. The first cut used the `VITEST`
16
+ * environment variable and was wrong in a way its own unit tests could not show: `VITEST` means
17
+ * "Vitest is running somewhere in this process", which is ALSO true when a Vitest suite boots an app
18
+ * in order to test it. `frameworks.integration.test.ts` starts a real Vite dev server per example
19
+ * app and asserts Reticle connects; under the env check it injected nothing and the suite failed
20
+ * with "@reticlehq/example-react never connected an Reticle session". CI caught that; three local
21
+ * battery runs did not, because that suite is not the battery.
22
+ *
23
+ * A bare `test` key is wrong the other way — most projects configure Vitest and are not under it.
24
+ * `test.browser.enabled` is true only for the server actually serving browser-mode test iframes,
25
+ * which is the one case with a HUD to suppress.
26
+ *
27
+ * An explicit `inject: true` still beats it: the check is a default chosen on the user's behalf.
28
+ */
29
+ export function isVitestBrowserServer(config) {
30
+ const test = config.test;
31
+ if (null === test || 'object' !== typeof test)
32
+ return false;
33
+ const browser = test.browser;
34
+ if (null === browser || 'object' !== typeof browser)
35
+ return false;
36
+ return true === browser.enabled;
37
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Normalising Vite's watcher `ignored` list, which is not a list.
3
+ *
4
+ * Split out of index.ts at the file cap. Small but cohesive: one third-party type shape, one
5
+ * runtime hazard it hides, and the guard that makes the plugin's return assignable without a cast.
6
+ */
7
+ /**
8
+ * Append our journal pattern to whatever the app already ignored, without assuming it is an array.
9
+ *
10
+ * Vite's `ignored` is `AnymatchMatcher`, so `watch: { ignored: '**\/fixtures/**' }` is a legal
11
+ * config. Spreading that string would explode it into one pattern PER CHARACTER — every one of
12
+ * which matches nothing, so the app's own exclusion is silently dropped and no error is raised. A
13
+ * function matcher is worse: it is not iterable at all, so the spread throws at config time and
14
+ * takes the dev server down, blaming the last plugin to touch the config.
15
+ */
16
+ export declare function mergeIgnored(existing: unknown, ours: RegExp): WatchPattern[];
17
+ /** Vite's `AnymatchPattern`, restated so the return type needs no cast. */
18
+ export type WatchPattern = string | RegExp | ((path: string) => boolean);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Normalising Vite's watcher `ignored` list, which is not a list.
3
+ *
4
+ * Split out of index.ts at the file cap. Small but cohesive: one third-party type shape, one
5
+ * runtime hazard it hides, and the guard that makes the plugin's return assignable without a cast.
6
+ */
7
+ /**
8
+ * Append our journal pattern to whatever the app already ignored, without assuming it is an array.
9
+ *
10
+ * Vite's `ignored` is `AnymatchMatcher`, so `watch: { ignored: '**\/fixtures/**' }` is a legal
11
+ * config. Spreading that string would explode it into one pattern PER CHARACTER — every one of
12
+ * which matches nothing, so the app's own exclusion is silently dropped and no error is raised. A
13
+ * function matcher is worse: it is not iterable at all, so the spread throws at config time and
14
+ * takes the dev server down, blaming the last plugin to touch the config.
15
+ */
16
+ export function mergeIgnored(existing, ours) {
17
+ if (undefined === existing || null === existing)
18
+ return [ours];
19
+ const listed = Array.isArray(existing) ? existing : [existing];
20
+ // Filtered rather than cast: an entry that is none of the three legal matcher shapes could never
21
+ // have excluded anything, so dropping it loses nothing and keeps the return honest without `any`.
22
+ return [...listed.filter(isWatchPattern), ours];
23
+ }
24
+ function isWatchPattern(value) {
25
+ return 'string' === typeof value || value instanceof RegExp || 'function' === typeof value;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/vite-plugin",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "Vite plugin for Reticle: dev-only source-map stamping plus auto-injected reticle.connect(). apply:'serve' guarantees it never ships to production.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -41,8 +41,8 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@babel/core": "^7.26.0",
44
- "@reticlehq/core": "2.13.0",
45
- "@reticlehq/babel-plugin": "2.13.0"
44
+ "@reticlehq/babel-plugin": "2.14.0",
45
+ "@reticlehq/core": "2.14.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/babel__core": "^7.20.5",