@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 +54 -4
- package/dist/index.d.cts +67 -16
- package/dist/index.d.ts +67 -16
- package/dist/index.js +54 -4
- package/dist/installed.d.ts +2 -1
- package/dist/vitest-browser.d.ts +31 -0
- package/dist/vitest-browser.js +37 -0
- package/dist/watch-ignore.d.ts +18 -0
- package/dist/watch-ignore.js +26 -0
- package/package.json +3 -3
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
722
|
+
{ tag: 'script', attrs: { type: 'module', src: connectModuleUrl(base) }, injectTo: 'body' },
|
|
673
723
|
];
|
|
674
724
|
},
|
|
675
725
|
};
|
package/dist/installed.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export declare const OPTIMIZER_OPTIONS_KEY: {
|
|
|
56
56
|
readonly ESBUILD: "esbuildOptions";
|
|
57
57
|
readonly ROLLDOWN: "rolldownOptions";
|
|
58
58
|
};
|
|
59
|
-
|
|
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.
|
|
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/
|
|
45
|
-
"@reticlehq/
|
|
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",
|