@reticlehq/vite-plugin 2.7.0 → 2.9.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/README.md +33 -12
- package/dist/index.cjs +5092 -0
- package/dist/index.d.cts +275 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +134 -10
- package/dist/svelte-source.js +7 -0
- package/package.json +16 -8
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
export declare const RETICLE_VITE_PLUGIN_NAME = "reticle";
|
|
2
|
+
/**
|
|
3
|
+
* Compile-time global carrying the daemon's pairing token, for connects the plugin does not write
|
|
4
|
+
* itself. The bridge requires the token even on localhost, and nothing in a browser can read the
|
|
5
|
+
* file it lives in.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RETICLE_TOKEN_GLOBAL = "__RETICLE_TOKEN__";
|
|
8
|
+
/**
|
|
9
|
+
* The connect code is served as a real module (not an inline <script>) so that Vite's import
|
|
10
|
+
* pipeline resolves the bare `@reticlehq/react` specifier. An inline injected script is NOT run through
|
|
11
|
+
* import resolution, so its bare import would fail in the browser. This path-like id is requested
|
|
12
|
+
* by the injected <script src> and served by the load hook below.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RETICLE_CONNECT_MODULE = "/@reticle-connect";
|
|
15
|
+
/**
|
|
16
|
+
* The pre-hook, as source for an inline <head> script.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately dependency-free ES5 in a try/catch: it runs before anything else on the page, so it
|
|
19
|
+
* must not assume a bundler, a module system, or that React is present at all. It installs a faithful
|
|
20
|
+
* devtools hook (React calls `inject` and expects a renderer id back, and stores the renderer) and
|
|
21
|
+
* counts commits into a buffer the module-side meter adopts later.
|
|
22
|
+
*/
|
|
23
|
+
export declare const RENDER_PREHOOK_SOURCE = "(function(){try{\nvar K='__REACT_DEVTOOLS_GLOBAL_HOOK__',P='__reticleRenderPreHook';\nif(globalThis[P])return;\nvar B={commits:0,sinks:[]};\nglobalThis[P]=B;\nvar fire=function(){B.commits++;for(var i=0;i<B.sinks.length;i++){try{B.sinks[i].apply(null,arguments);}catch(e){}}};\nvar h=globalThis[K];\nif(h===undefined){\nglobalThis[K]={supportsFiber:true,renderers:new Map(),inject:function(r){var id=this.renderers.size+1;this.renderers.set(id,r);return id;},\nonScheduleFiberRoot:function(){},onCommitFiberRoot:fire,onPostCommitFiberRoot:function(){},onCommitFiberUnmount:function(){}};\n}else{var prev=h.onCommitFiberRoot;h.onCommitFiberRoot=function(){try{fire.apply(null,arguments);}catch(e){}\nif(typeof prev==='function')return prev.apply(this,arguments);};}\n}catch(e){}})();";
|
|
24
|
+
/**
|
|
25
|
+
* Said ONCE, and it names the symptom the user is looking at rather than the mechanism, because the
|
|
26
|
+
* mechanism is invisible from a browser: the page reloads and nothing explains why.
|
|
27
|
+
*/
|
|
28
|
+
export declare const connectChurnWarning: () => string;
|
|
29
|
+
export interface ReticleVitePluginOptions {
|
|
30
|
+
/** Bridge WebSocket port. Defaults to the SDK default; only baked into connect when non-default. */
|
|
31
|
+
port?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Project root, so React's absolute `_debugSource.fileName` reports repo-relative. Resolved from
|
|
34
|
+
* the Vite config at injection time; set it only to override.
|
|
35
|
+
*/
|
|
36
|
+
root?: string;
|
|
37
|
+
/**
|
|
38
|
+
* The installed SDK's version, so a pair skewed against the daemon can name itself instead of
|
|
39
|
+
* surfacing as a bare -32000. Read from the installed package; set it only to override.
|
|
40
|
+
*/
|
|
41
|
+
sdkVersion?: string;
|
|
42
|
+
/** Stable session label for the bridge. Defaults to the SDK's auto-generated id. */
|
|
43
|
+
session?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Stable project identity. Defaults to one derived from the app's package.json name + root path,
|
|
46
|
+
* so multi-project session scoping works with zero config. Override only for special setups.
|
|
47
|
+
*/
|
|
48
|
+
projectId?: string;
|
|
49
|
+
/** Auth token forwarded to connect when the bridge requires one. */
|
|
50
|
+
token?: string;
|
|
51
|
+
/** Stamp data-reticle-source for React 19 source mapping. Default true (harmless on React <=18). */
|
|
52
|
+
sourceMapping?: boolean;
|
|
53
|
+
/** Auto-inject the dev-gated reticle.connect call. Default true. */
|
|
54
|
+
inject?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* This build is an Electron/Tauri renderer. Changes two things a desktop shell needs and a web app
|
|
57
|
+
* must not get:
|
|
58
|
+
*
|
|
59
|
+
* - The plugin also applies to `vite build`. A packaged desktop renderer IS a production build
|
|
60
|
+
* loaded from `file://` or a custom protocol — there is no dev server — so the default
|
|
61
|
+
* `apply: 'serve'` drops the plugin entirely and the app ships with no `connect()` at all.
|
|
62
|
+
* - `connect()` is called with `allowInProduction`, because that same renderer reports
|
|
63
|
+
* NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
|
|
64
|
+
*
|
|
65
|
+
* Off by default and never inferred: turning it on means an instrumented production BUNDLE, which
|
|
66
|
+
* is exactly what a web app must never ship. Keep it behind your own dev-only build (a dev target,
|
|
67
|
+
* or `process.env.NODE_ENV !== 'production'` in vite.config) so it cannot reach a release binary.
|
|
68
|
+
*/
|
|
69
|
+
desktop?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Record request/response BODIES on `reticle_network`, not just method/url/status.
|
|
72
|
+
*
|
|
73
|
+
* Off by default because a body is the one part of a request that routinely carries a card
|
|
74
|
+
* number, a token or a customer's address, and the daemon journals what it is told.
|
|
75
|
+
*
|
|
76
|
+
* It matters that this is reachable at all. The SDK has supported `captureNetworkBodies` on
|
|
77
|
+
* `connect()` since bodies existed, but the plugin — the documented one-line integration, and the
|
|
78
|
+
* only `connect()` most apps ever have — had no way to pass it, and calling `connect()` a second
|
|
79
|
+
* time is a no-op. So for every app wired the recommended way, a payload was unreachable: on a
|
|
80
|
+
* real payments dashboard, a refund POSTing `amount: 1187.01` into a paise field (a 100x
|
|
81
|
+
* under-refund) was visible to Playwright's request inspector and invisible here.
|
|
82
|
+
*
|
|
83
|
+
* Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`, so it can be turned on for one debugging
|
|
84
|
+
* session without editing vite.config.
|
|
85
|
+
*/
|
|
86
|
+
captureNetworkBodies?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Let Reticle run when the page or the bridge is not on localhost.
|
|
89
|
+
*
|
|
90
|
+
* Off by default: the SDK refuses outside localhost so a page on the open internet cannot be
|
|
91
|
+
* instrumented by a bridge it happened to reach. Turn it on for a dev server that CANNOT be served
|
|
92
|
+
* on localhost — a host-based multi-tenant frontend, a white-label app resolving the tenant from
|
|
93
|
+
* the `Host` header, anything with cookie-scoped auth on a custom dev hostname. Without it those
|
|
94
|
+
* apps cannot use Reticle at all, because the plugin is the only `connect()` they have and a
|
|
95
|
+
* second, hand-written one is a no-op.
|
|
96
|
+
*
|
|
97
|
+
* NOT SUFFICIENT ON ITS OWN — a pairing token is also required. `connectionPolicy` in
|
|
98
|
+
* `@reticlehq/browser` refuses a non-localhost connect with "a pairing token is required outside
|
|
99
|
+
* localhost" whenever the token is missing or empty, whatever this flag says. The plugin supplies
|
|
100
|
+
* one automatically from the daemon's `~/.reticle/pairing-token` (see readPairingToken), so a
|
|
101
|
+
* started daemon is normally all it takes; pass `token` yourself only when the daemon's file is
|
|
102
|
+
* unreachable. A non-loopback BRIDGE additionally has to be `wss://`.
|
|
103
|
+
*
|
|
104
|
+
* Also settable as `VITE_RETICLE_ALLOW_NON_LOCALHOST=1`, so it can be turned on for one session
|
|
105
|
+
* without editing vite.config.
|
|
106
|
+
*/
|
|
107
|
+
allowNonLocalhost?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Where a diagnostic goes. Defaults to the console; injected so the dev-mode injection check is
|
|
110
|
+
* testable without capturing global console output.
|
|
111
|
+
*/
|
|
112
|
+
onWarn?: (message: string) => void;
|
|
113
|
+
}
|
|
114
|
+
/** Structural Vite plugin shape — avoids a hard dependency on `vite` while staying assignable to its `Plugin`. */
|
|
115
|
+
export interface ReticleVitePlugin {
|
|
116
|
+
name: string;
|
|
117
|
+
/**
|
|
118
|
+
* Vite's `config` hook. Used to declare the SDK's CJS runtime deps for pre-bundling — see the
|
|
119
|
+
* implementation for why omitting them makes the whole SDK fail to load on linked setups.
|
|
120
|
+
*/
|
|
121
|
+
config?: (config: {
|
|
122
|
+
optimizeDeps?: {
|
|
123
|
+
include?: string[];
|
|
124
|
+
/** Whichever key the app used — the plugin reads both and writes the one this Vite wants. */
|
|
125
|
+
esbuildOptions?: Record<string, unknown>;
|
|
126
|
+
rolldownOptions?: Record<string, unknown>;
|
|
127
|
+
};
|
|
128
|
+
define?: Record<string, string>;
|
|
129
|
+
root?: string;
|
|
130
|
+
server?: {
|
|
131
|
+
watch?: {
|
|
132
|
+
ignored?: (string | RegExp)[];
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
}) => {
|
|
136
|
+
optimizeDeps: {
|
|
137
|
+
include: string[];
|
|
138
|
+
[optionsKey: string]: unknown;
|
|
139
|
+
};
|
|
140
|
+
define: Record<string, string>;
|
|
141
|
+
server: {
|
|
142
|
+
watch: {
|
|
143
|
+
ignored: (string | RegExp)[];
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/** Absent in desktop mode, where the plugin must also run for `vite build`. */
|
|
148
|
+
apply?: 'serve';
|
|
149
|
+
enforce: 'pre';
|
|
150
|
+
transform: (code: string, id: string) => {
|
|
151
|
+
code: string;
|
|
152
|
+
map: string | null;
|
|
153
|
+
} | null;
|
|
154
|
+
resolveId: (id: string, importer?: string) => string | null;
|
|
155
|
+
load: (id: string) => string | null;
|
|
156
|
+
transformIndexHtml: (html: string) => HtmlTag[];
|
|
157
|
+
/** Vite hands over the resolved config; used to resolve the HTML entry exactly. */
|
|
158
|
+
configResolved?: (config: {
|
|
159
|
+
root?: string;
|
|
160
|
+
command?: string;
|
|
161
|
+
}) => void;
|
|
162
|
+
/** Dev-server hook: keeps the served connect module from outliving the token it was built without. */
|
|
163
|
+
configureServer?: (server: ViteDevServerLike) => void;
|
|
164
|
+
/** Build-time post-condition: desktop injection must have happened. */
|
|
165
|
+
buildEnd?: () => void;
|
|
166
|
+
/** Runs the dev-mode injection check immediately. Test seam for the deferred timer. */
|
|
167
|
+
checkInjectedForTest?: () => void;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The slice of Vite's dev server this plugin touches, structurally — so `vite` stays a peer the
|
|
171
|
+
* plugin never imports, the same way the Svelte compiler and Playwright are handled elsewhere.
|
|
172
|
+
*/
|
|
173
|
+
export interface ViteDevServerLike {
|
|
174
|
+
middlewares: {
|
|
175
|
+
use(handler: (req: {
|
|
176
|
+
url?: string | undefined;
|
|
177
|
+
}, res: unknown, next: () => void) => void): void;
|
|
178
|
+
};
|
|
179
|
+
moduleGraph: {
|
|
180
|
+
getModuleById(id: string): object | undefined;
|
|
181
|
+
invalidateModule(mod: object): void;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
interface HtmlTag {
|
|
185
|
+
tag: string;
|
|
186
|
+
/** Absent on an inline script, which carries its source in `children` instead. */
|
|
187
|
+
attrs?: Record<string, string>;
|
|
188
|
+
/** Inline source, for a tag that has no `src`. */
|
|
189
|
+
children?: string;
|
|
190
|
+
/** `head-prepend` is required for the render pre-hook: it must run before any module script. */
|
|
191
|
+
injectTo: 'body' | 'head-prepend';
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Name each CJS dep ONLY in the bare form, and only when the app root can resolve it.
|
|
195
|
+
*
|
|
196
|
+
* This used to try three layouts and emit Vite's nested `a > b > c` form when a hoisted lookup
|
|
197
|
+
* failed. The guard asked the wrong question: it tested NODE resolvability, walking the chain
|
|
198
|
+
* segment by segment, and under pnpm that succeeds exactly where Vite fails. Measured on the
|
|
199
|
+
* sveltekit fixture:
|
|
200
|
+
*
|
|
201
|
+
* ['@testing-library/dom'] -> null
|
|
202
|
+
* ['@reticlehq/browser', '@testing-library/dom'] -> null
|
|
203
|
+
* ['@reticlehq/react', '@reticlehq/browser', '@testing-library/dom'] -> emitted
|
|
204
|
+
*
|
|
205
|
+
* So we emitted the three-segment chain, Vite could not follow it, and the boot warning this
|
|
206
|
+
* function exists to prevent appeared anyway — `Failed to resolve dependency: …, present in
|
|
207
|
+
* optimizeDeps.include`, pointing at Reticle, naming a package the developer has never heard of,
|
|
208
|
+
* and forcing a full re-optimization on every cold start. The comment stated the rule correctly and
|
|
209
|
+
* the code broke it.
|
|
210
|
+
*
|
|
211
|
+
* Dropping the nested form loses nothing that matters: the SDK itself is still pre-bundled, and Vite
|
|
212
|
+
* follows its imports when it does that, so these deps are handled as part of it. Naming them
|
|
213
|
+
* separately was belt-and-braces for a locally-aliased SDK, where the bare form resolves anyway.
|
|
214
|
+
*/
|
|
215
|
+
export declare function cjsDepIncludes(appRoot: string, canResolve?: (dep: string) => boolean): string[];
|
|
216
|
+
export declare function readPairingToken(): string | undefined;
|
|
217
|
+
/** The body of the connect module — real imports, resolved by Vite when the module is served. */
|
|
218
|
+
/**
|
|
219
|
+
* The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
|
|
220
|
+
*
|
|
221
|
+
* It is imported by CONVENTION rather than by patching the app's entry file. The connect is injected
|
|
222
|
+
* into a virtual module, so there is nowhere for a user to add these calls without `init` editing
|
|
223
|
+
* `src/main.tsx` — an edit to the file people actually own, for something that is opt-in enrichment.
|
|
224
|
+
* Convention costs one `existsSync` and leaves their entry untouched.
|
|
225
|
+
*/
|
|
226
|
+
export declare const RETICLE_DEV_MODULE_CANDIDATES: readonly ["src/reticle-dev.ts", "src/reticle-dev.js", "src/reticle-dev.tsx", "src/reticle-dev.jsx"];
|
|
227
|
+
/** The app's dev module, as an importable path — or null when the app has none. */
|
|
228
|
+
export declare function findDevModule(root: string, exists: (p: string) => boolean): string | null;
|
|
229
|
+
/**
|
|
230
|
+
* Which SDK package this app actually has, and whether `install()` applies.
|
|
231
|
+
*
|
|
232
|
+
* The injected connect used to name `@reticlehq/react` unconditionally. That is right for a React
|
|
233
|
+
* app and fatal for any other: `reticle init` gives a Vue or Svelte codebase the framework-neutral
|
|
234
|
+
* `@reticlehq/browser` — deliberately, because a package named `@reticlehq/react` with `react` in
|
|
235
|
+
* its peers has no business in a Vue app — and the injected import then names a package that is not
|
|
236
|
+
* installed, so nothing connects and the page reports no session with no obvious cause.
|
|
237
|
+
*
|
|
238
|
+
* Measured end to end on a pristine `npm create vite --template vue` app: init wrote every file
|
|
239
|
+
* correctly and the tab never dialled the daemon, because of this one specifier.
|
|
240
|
+
*
|
|
241
|
+
* The React kit WINS when both resolve: it is a superset (it re-exports the sensor and adds the
|
|
242
|
+
* adapter), so an app that has it wants component identity. `install()` is the adapter's alone and
|
|
243
|
+
* the sensor does not export it — naming it against the sensor would trade a missing module for a
|
|
244
|
+
* missing export.
|
|
245
|
+
*/
|
|
246
|
+
export declare function installedSdk(appRoot: string, canResolve?: (dep: string) => boolean): {
|
|
247
|
+
specifier: string;
|
|
248
|
+
usesInstall: boolean;
|
|
249
|
+
};
|
|
250
|
+
export declare function connectModuleSource(options: ReticleVitePluginOptions, devModule?: string | null): string;
|
|
251
|
+
/**
|
|
252
|
+
* Reticle Vite plugin. Add to your `plugins` array and the entire integration is done:
|
|
253
|
+
*
|
|
254
|
+
* import { reticle } from '@reticlehq/vite-plugin';
|
|
255
|
+
* export default defineConfig({ plugins: [react(), reticle()] });
|
|
256
|
+
*
|
|
257
|
+
* `apply: 'serve'` means Vite drops the plugin entirely from `vite build`, so a web production
|
|
258
|
+
* bundle is never instrumented — gating is the tool's job, not a user-managed env check.
|
|
259
|
+
*
|
|
260
|
+
* `desktop: true` is the ONE documented exception, and it inverts that guarantee deliberately: a
|
|
261
|
+
* packaged Electron/Tauri renderer IS a production build with no dev server, so serve-only gating
|
|
262
|
+
* would ship an app with no connect() at all. The cost is that the flag hands gating back to the
|
|
263
|
+
* caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
|
|
264
|
+
* a release binary.
|
|
265
|
+
*/
|
|
266
|
+
/**
|
|
267
|
+
* The daemon's journal directory, as a matcher every chokidar major honours.
|
|
268
|
+
*
|
|
269
|
+
* Exported so the one regression test can assert on the matcher itself rather than on a string that
|
|
270
|
+
* looked right and matched nothing.
|
|
271
|
+
*/
|
|
272
|
+
export declare const JOURNAL_IGNORE: RegExp;
|
|
273
|
+
export declare function reticle(options?: ReticleVitePluginOptions): ReticleVitePlugin;
|
|
274
|
+
export {};
|
|
275
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export declare const RETICLE_CONNECT_MODULE = "/@reticle-connect";
|
|
|
21
21
|
* counts commits into a buffer the module-side meter adopts later.
|
|
22
22
|
*/
|
|
23
23
|
export declare const RENDER_PREHOOK_SOURCE = "(function(){try{\nvar K='__REACT_DEVTOOLS_GLOBAL_HOOK__',P='__reticleRenderPreHook';\nif(globalThis[P])return;\nvar B={commits:0,sinks:[]};\nglobalThis[P]=B;\nvar fire=function(){B.commits++;for(var i=0;i<B.sinks.length;i++){try{B.sinks[i].apply(null,arguments);}catch(e){}}};\nvar h=globalThis[K];\nif(h===undefined){\nglobalThis[K]={supportsFiber:true,renderers:new Map(),inject:function(r){var id=this.renderers.size+1;this.renderers.set(id,r);return id;},\nonScheduleFiberRoot:function(){},onCommitFiberRoot:fire,onPostCommitFiberRoot:function(){},onCommitFiberUnmount:function(){}};\n}else{var prev=h.onCommitFiberRoot;h.onCommitFiberRoot=function(){try{fire.apply(null,arguments);}catch(e){}\nif(typeof prev==='function')return prev.apply(this,arguments);};}\n}catch(e){}})();";
|
|
24
|
+
/**
|
|
25
|
+
* Said ONCE, and it names the symptom the user is looking at rather than the mechanism, because the
|
|
26
|
+
* mechanism is invisible from a browser: the page reloads and nothing explains why.
|
|
27
|
+
*/
|
|
28
|
+
export declare const connectChurnWarning: () => string;
|
|
24
29
|
export interface ReticleVitePluginOptions {
|
|
25
30
|
/** Bridge WebSocket port. Defaults to the SDK default; only baked into connect when non-default. */
|
|
26
31
|
port?: number;
|
|
@@ -79,6 +84,27 @@ export interface ReticleVitePluginOptions {
|
|
|
79
84
|
* session without editing vite.config.
|
|
80
85
|
*/
|
|
81
86
|
captureNetworkBodies?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Let Reticle run when the page or the bridge is not on localhost.
|
|
89
|
+
*
|
|
90
|
+
* Off by default: the SDK refuses outside localhost so a page on the open internet cannot be
|
|
91
|
+
* instrumented by a bridge it happened to reach. Turn it on for a dev server that CANNOT be served
|
|
92
|
+
* on localhost — a host-based multi-tenant frontend, a white-label app resolving the tenant from
|
|
93
|
+
* the `Host` header, anything with cookie-scoped auth on a custom dev hostname. Without it those
|
|
94
|
+
* apps cannot use Reticle at all, because the plugin is the only `connect()` they have and a
|
|
95
|
+
* second, hand-written one is a no-op.
|
|
96
|
+
*
|
|
97
|
+
* NOT SUFFICIENT ON ITS OWN — a pairing token is also required. `connectionPolicy` in
|
|
98
|
+
* `@reticlehq/browser` refuses a non-localhost connect with "a pairing token is required outside
|
|
99
|
+
* localhost" whenever the token is missing or empty, whatever this flag says. The plugin supplies
|
|
100
|
+
* one automatically from the daemon's `~/.reticle/pairing-token` (see readPairingToken), so a
|
|
101
|
+
* started daemon is normally all it takes; pass `token` yourself only when the daemon's file is
|
|
102
|
+
* unreachable. A non-loopback BRIDGE additionally has to be `wss://`.
|
|
103
|
+
*
|
|
104
|
+
* Also settable as `VITE_RETICLE_ALLOW_NON_LOCALHOST=1`, so it can be turned on for one session
|
|
105
|
+
* without editing vite.config.
|
|
106
|
+
*/
|
|
107
|
+
allowNonLocalhost?: boolean;
|
|
82
108
|
/**
|
|
83
109
|
* Where a diagnostic goes. Defaults to the console; injected so the dev-mode injection check is
|
|
84
110
|
* testable without capturing global console output.
|
|
@@ -101,12 +127,22 @@ export interface ReticleVitePlugin {
|
|
|
101
127
|
};
|
|
102
128
|
define?: Record<string, string>;
|
|
103
129
|
root?: string;
|
|
130
|
+
server?: {
|
|
131
|
+
watch?: {
|
|
132
|
+
ignored?: (string | RegExp)[];
|
|
133
|
+
};
|
|
134
|
+
};
|
|
104
135
|
}) => {
|
|
105
136
|
optimizeDeps: {
|
|
106
137
|
include: string[];
|
|
107
138
|
[optionsKey: string]: unknown;
|
|
108
139
|
};
|
|
109
140
|
define: Record<string, string>;
|
|
141
|
+
server: {
|
|
142
|
+
watch: {
|
|
143
|
+
ignored: (string | RegExp)[];
|
|
144
|
+
};
|
|
145
|
+
};
|
|
110
146
|
};
|
|
111
147
|
/** Absent in desktop mode, where the plugin must also run for `vite build`. */
|
|
112
148
|
apply?: 'serve';
|
|
@@ -190,6 +226,27 @@ export declare function readPairingToken(): string | undefined;
|
|
|
190
226
|
export declare const RETICLE_DEV_MODULE_CANDIDATES: readonly ["src/reticle-dev.ts", "src/reticle-dev.js", "src/reticle-dev.tsx", "src/reticle-dev.jsx"];
|
|
191
227
|
/** The app's dev module, as an importable path — or null when the app has none. */
|
|
192
228
|
export declare function findDevModule(root: string, exists: (p: string) => boolean): string | null;
|
|
229
|
+
/**
|
|
230
|
+
* Which SDK package this app actually has, and whether `install()` applies.
|
|
231
|
+
*
|
|
232
|
+
* The injected connect used to name `@reticlehq/react` unconditionally. That is right for a React
|
|
233
|
+
* app and fatal for any other: `reticle init` gives a Vue or Svelte codebase the framework-neutral
|
|
234
|
+
* `@reticlehq/browser` — deliberately, because a package named `@reticlehq/react` with `react` in
|
|
235
|
+
* its peers has no business in a Vue app — and the injected import then names a package that is not
|
|
236
|
+
* installed, so nothing connects and the page reports no session with no obvious cause.
|
|
237
|
+
*
|
|
238
|
+
* Measured end to end on a pristine `npm create vite --template vue` app: init wrote every file
|
|
239
|
+
* correctly and the tab never dialled the daemon, because of this one specifier.
|
|
240
|
+
*
|
|
241
|
+
* The React kit WINS when both resolve: it is a superset (it re-exports the sensor and adds the
|
|
242
|
+
* adapter), so an app that has it wants component identity. `install()` is the adapter's alone and
|
|
243
|
+
* the sensor does not export it — naming it against the sensor would trade a missing module for a
|
|
244
|
+
* missing export.
|
|
245
|
+
*/
|
|
246
|
+
export declare function installedSdk(appRoot: string, canResolve?: (dep: string) => boolean): {
|
|
247
|
+
specifier: string;
|
|
248
|
+
usesInstall: boolean;
|
|
249
|
+
};
|
|
193
250
|
export declare function connectModuleSource(options: ReticleVitePluginOptions, devModule?: string | null): string;
|
|
194
251
|
/**
|
|
195
252
|
* Reticle Vite plugin. Add to your `plugins` array and the entire integration is done:
|
|
@@ -206,5 +263,12 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
|
|
|
206
263
|
* caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
|
|
207
264
|
* a release binary.
|
|
208
265
|
*/
|
|
266
|
+
/**
|
|
267
|
+
* The daemon's journal directory, as a matcher every chokidar major honours.
|
|
268
|
+
*
|
|
269
|
+
* Exported so the one regression test can assert on the matcher itself rather than on a string that
|
|
270
|
+
* looked right and matched nothing.
|
|
271
|
+
*/
|
|
272
|
+
export declare const JOURNAL_IGNORE: RegExp;
|
|
209
273
|
export declare function reticle(options?: ReticleVitePluginOptions): ReticleVitePlugin;
|
|
210
274
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ export const RETICLE_VITE_PLUGIN_NAME = 'reticle';
|
|
|
15
15
|
// specifier yields both `reticle` (connect) and `install` (the React adapter). NOT `@reticlehq/core`
|
|
16
16
|
// — that is the isomorphic foundation and exports neither.
|
|
17
17
|
const RETICLE_PACKAGE = '@reticlehq/react';
|
|
18
|
+
/** The framework-neutral sensor, which a Vue or Svelte app gets instead. See installedSdk. */
|
|
19
|
+
const RETICLE_SENSOR = '@reticlehq/browser';
|
|
18
20
|
/**
|
|
19
21
|
* Compile-time global carrying the daemon's pairing token, for connects the plugin does not write
|
|
20
22
|
* itself. The bridge requires the token even on localhost, and nothing in a browser can read the
|
|
@@ -61,6 +63,27 @@ if(typeof prev==='function')return prev.apply(this,arguments);};}
|
|
|
61
63
|
* large app can take a moment. A false warning would train people to ignore a real one.
|
|
62
64
|
*/
|
|
63
65
|
const DEV_INJECTION_GRACE_MS = 10_000;
|
|
66
|
+
/**
|
|
67
|
+
* How many times the connect module's source may legitimately change in one dev-server session
|
|
68
|
+
* before the plugin says so.
|
|
69
|
+
*
|
|
70
|
+
* The source is a function of the port, the projectId, the pairing token and whether the app has a
|
|
71
|
+
* `reticle-dev` module. In a healthy session that settles almost immediately: the daemon starting
|
|
72
|
+
* after Vite is one change, a dev module being created is another. Anything past a handful means
|
|
73
|
+
* an input is oscillating, and an oscillating connect module is what makes Vite re-resolve it on
|
|
74
|
+
* every page load — the reload loop this counter exists to make audible instead of mysterious.
|
|
75
|
+
*/
|
|
76
|
+
const CONNECT_CHURN_LIMIT = 5;
|
|
77
|
+
/**
|
|
78
|
+
* Said ONCE, and it names the symptom the user is looking at rather than the mechanism, because the
|
|
79
|
+
* mechanism is invisible from a browser: the page reloads and nothing explains why.
|
|
80
|
+
*/
|
|
81
|
+
export const connectChurnWarning = () => `[${RETICLE_VITE_PLUGIN_NAME}] the injected connect module has changed ${String(CONNECT_CHURN_LIMIT)} ` +
|
|
82
|
+
'times in one dev-server session. Something it depends on (the bridge port, the pairing token, ' +
|
|
83
|
+
'or a reticle-dev module appearing and disappearing) is not settling, and that can make the page ' +
|
|
84
|
+
'reload repeatedly. Reticle will keep serving the newest version. Please report this at ' +
|
|
85
|
+
'https://github.com/ReticleHQ/reticle/issues with your vite.config and whether more than one ' +
|
|
86
|
+
'daemon is running (`npx @reticlehq/server status`).';
|
|
64
87
|
/**
|
|
65
88
|
* Is this resolved module id the one the HTML referenced?
|
|
66
89
|
*
|
|
@@ -220,6 +243,12 @@ function connectArgs(options) {
|
|
|
220
243
|
if (true === options.captureNetworkBodies || '1' === process.env['VITE_RETICLE_CAPTURE_BODIES']) {
|
|
221
244
|
args['captureNetworkBodies'] = true;
|
|
222
245
|
}
|
|
246
|
+
// Same shape, same reason: without it an app that cannot be served on localhost has no way to
|
|
247
|
+
// reach the SDK option at all. The pairing token still applies — see the option's docstring.
|
|
248
|
+
if (true === options.allowNonLocalhost ||
|
|
249
|
+
'1' === process.env['VITE_RETICLE_ALLOW_NON_LOCALHOST']) {
|
|
250
|
+
args['allowNonLocalhost'] = true;
|
|
251
|
+
}
|
|
223
252
|
return Object.keys(args).length > 0 ? JSON.stringify(args) : '';
|
|
224
253
|
}
|
|
225
254
|
/** The body of the connect module — real imports, resolved by Vite when the module is served. */
|
|
@@ -245,9 +274,38 @@ export function findDevModule(root, exists) {
|
|
|
245
274
|
}
|
|
246
275
|
return null;
|
|
247
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Which SDK package this app actually has, and whether `install()` applies.
|
|
279
|
+
*
|
|
280
|
+
* The injected connect used to name `@reticlehq/react` unconditionally. That is right for a React
|
|
281
|
+
* app and fatal for any other: `reticle init` gives a Vue or Svelte codebase the framework-neutral
|
|
282
|
+
* `@reticlehq/browser` — deliberately, because a package named `@reticlehq/react` with `react` in
|
|
283
|
+
* its peers has no business in a Vue app — and the injected import then names a package that is not
|
|
284
|
+
* installed, so nothing connects and the page reports no session with no obvious cause.
|
|
285
|
+
*
|
|
286
|
+
* Measured end to end on a pristine `npm create vite --template vue` app: init wrote every file
|
|
287
|
+
* correctly and the tab never dialled the daemon, because of this one specifier.
|
|
288
|
+
*
|
|
289
|
+
* The React kit WINS when both resolve: it is a superset (it re-exports the sensor and adds the
|
|
290
|
+
* adapter), so an app that has it wants component identity. `install()` is the adapter's alone and
|
|
291
|
+
* the sensor does not export it — naming it against the sensor would trade a missing module for a
|
|
292
|
+
* missing export.
|
|
293
|
+
*/
|
|
294
|
+
export function installedSdk(appRoot, canResolve = (dep) => null !== resolvableChain([dep], appRoot)) {
|
|
295
|
+
if (canResolve(RETICLE_PACKAGE))
|
|
296
|
+
return { specifier: RETICLE_PACKAGE, usesInstall: true };
|
|
297
|
+
if (canResolve(RETICLE_SENSOR))
|
|
298
|
+
return { specifier: RETICLE_SENSOR, usesInstall: false };
|
|
299
|
+
// Neither resolves: keep the historical name so the failure reads as "the SDK is not installed"
|
|
300
|
+
// rather than as a package nobody recognises.
|
|
301
|
+
return { specifier: RETICLE_PACKAGE, usesInstall: true };
|
|
302
|
+
}
|
|
248
303
|
export function connectModuleSource(options, devModule = null) {
|
|
249
304
|
const args = connectArgs(options);
|
|
250
|
-
const
|
|
305
|
+
const sdk = installedSdk(options.root ?? process.cwd());
|
|
306
|
+
const named = sdk.usesInstall ? 'reticle, install' : 'reticle';
|
|
307
|
+
const call = sdk.usesInstall ? 'install();\n' : '';
|
|
308
|
+
const base = `import { ${named} } from '${sdk.specifier}';\n${call}reticle.connect(${args});\n`;
|
|
251
309
|
// AFTER connect: registerStore subscribes through the live SDK, and registering before there is a
|
|
252
310
|
// session to report into drops the first diffs.
|
|
253
311
|
return null === devModule ? base : `${base}import('${devModule}');\n`;
|
|
@@ -267,6 +325,13 @@ export function connectModuleSource(options, devModule = null) {
|
|
|
267
325
|
* caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
|
|
268
326
|
* a release binary.
|
|
269
327
|
*/
|
|
328
|
+
/**
|
|
329
|
+
* The daemon's journal directory, as a matcher every chokidar major honours.
|
|
330
|
+
*
|
|
331
|
+
* Exported so the one regression test can assert on the matcher itself rather than on a string that
|
|
332
|
+
* looked right and matched nothing.
|
|
333
|
+
*/
|
|
334
|
+
export const JOURNAL_IGNORE = new RegExp(`(^|[\\\\/])${ReticleDir.ROOT.replace('.', '\\.')}([\\\\/]|$)`);
|
|
270
335
|
export function reticle(options = {}) {
|
|
271
336
|
const sourceMapping = options.sourceMapping !== false;
|
|
272
337
|
const inject = options.inject !== false;
|
|
@@ -308,6 +373,16 @@ export function reticle(options = {}) {
|
|
|
308
373
|
const sdkVersion = withToken.sdkVersion ?? sdkPackageVersion(appRoot);
|
|
309
374
|
return { ...withToken, root: appRoot, sdkVersion };
|
|
310
375
|
};
|
|
376
|
+
/**
|
|
377
|
+
* The connect module's source as it would be served RIGHT NOW. Recomputed rather than cached: the
|
|
378
|
+
* daemon's token and the app's dev module can both appear after the dev server started, which is
|
|
379
|
+
* the whole reason the module is re-read at all.
|
|
380
|
+
*/
|
|
381
|
+
const currentConnectSource = () => connectModuleSource(resolveLazy(), root === undefined ? null : findDevModule(root, existsSync));
|
|
382
|
+
/** The source `load` last handed to Vite, or undefined before the first serve. */
|
|
383
|
+
let lastServedConnectSource;
|
|
384
|
+
/** How many times the served source has actually changed. See connectChurnWarning. */
|
|
385
|
+
let connectChanges = 0;
|
|
311
386
|
/**
|
|
312
387
|
* The BUILD message. A build always runs every transform, so "my transform never ran" and "the
|
|
313
388
|
* bundle has no connect()" are the same statement there, and stating it as a certainty is correct.
|
|
@@ -369,6 +444,37 @@ export function reticle(options = {}) {
|
|
|
369
444
|
const appRoot = config.root ?? process.cwd();
|
|
370
445
|
const optimizerKey = optimizerOptionsKey(viteMajor(appRoot));
|
|
371
446
|
return {
|
|
447
|
+
// Keep the daemon's journal out of the dev server's watcher.
|
|
448
|
+
//
|
|
449
|
+
// The daemon writes `.reticle/` into the PROJECT root — session journals, and `ambient.json`
|
|
450
|
+
// rewritten atomically as `ambient.json.tmp` + rename on a live session. Vite watches the
|
|
451
|
+
// project root and does not ignore that directory, so every journal write read as a project
|
|
452
|
+
// file changing and Vite answered with a full page reload.
|
|
453
|
+
//
|
|
454
|
+
// That is a loop with no exit: page loads -> SDK connects and streams events -> daemon
|
|
455
|
+
// journals them -> Vite reloads the page -> SDK reconnects -> more events. It ran several
|
|
456
|
+
// times a second for as long as the dev server was up, and the damage was total but
|
|
457
|
+
// misattributed: every ref went stale, every act_and_wait died mid-flight, and the log
|
|
458
|
+
// filled with connect/disconnect pairs that looked like a flapping SDK rather than a
|
|
459
|
+
// watcher chasing its own tail.
|
|
460
|
+
//
|
|
461
|
+
// A RegExp, not a glob, and that is the whole difference between this working and not.
|
|
462
|
+
// chokidar dropped glob support in v4 — Vite 7+ ships v4/v5, where a pattern like
|
|
463
|
+
// `**/.reticle/**` is silently accepted and matches nothing. MEASURED against the chokidar
|
|
464
|
+
// this repo resolves: with the glob, a write to `.reticle/ambient.json` still fires; with
|
|
465
|
+
// this RegExp it does not, while a normal file still does. Vite's own defaults are globs and
|
|
466
|
+
// have the same problem, which is why it is not safe to copy their shape here.
|
|
467
|
+
//
|
|
468
|
+
// Anchored on `^` or a separator so it matches the directory and not a file that merely ends
|
|
469
|
+
// in those characters, and both separators are accepted because chokidar reports the path in
|
|
470
|
+
// the platform's own form.
|
|
471
|
+
//
|
|
472
|
+
// Appends to the app's list rather than replacing it, so nothing it already excluded is lost.
|
|
473
|
+
server: {
|
|
474
|
+
watch: {
|
|
475
|
+
ignored: [...(config.server?.watch?.ignored ?? []), JOURNAL_IGNORE],
|
|
476
|
+
},
|
|
477
|
+
},
|
|
372
478
|
// Expose the daemon's pairing token to hand-written connects in the same Vite app. The
|
|
373
479
|
// plugin's own injected connect gets the token directly, but a connect the USER writes —
|
|
374
480
|
// SvelteKit's client hook, a custom entry — had no way to reach a file only Node can read,
|
|
@@ -406,7 +512,11 @@ export function reticle(options = {}) {
|
|
|
406
512
|
// no WebSocket, no session, no console message. The FIRST load after `reticle init` —
|
|
407
513
|
// the one the whole product is judged on — silently did nothing, and it worked on the
|
|
408
514
|
// next refresh, which is the worst possible shape for a bug like this.
|
|
409
|
-
|
|
515
|
+
//
|
|
516
|
+
// Whichever SDK this app actually has: naming `@reticlehq/react` in a Vue app that was
|
|
517
|
+
// given the sensor produces the exact boot warning the note below is about, for a
|
|
518
|
+
// package that is correctly absent.
|
|
519
|
+
installedSdk(appRoot).specifier,
|
|
410
520
|
// Only in a form that resolves — see above; a name Vite cannot resolve produces a boot
|
|
411
521
|
// warning that blames Reticle, and a forced re-optimization on every cold start.
|
|
412
522
|
...cjsDepIncludes(appRoot),
|
|
@@ -455,9 +565,9 @@ export function reticle(options = {}) {
|
|
|
455
565
|
load(id) {
|
|
456
566
|
if (!inject || id !== RETICLE_CONNECT_MODULE)
|
|
457
567
|
return null;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
return
|
|
568
|
+
const source = currentConnectSource();
|
|
569
|
+
lastServedConnectSource = source;
|
|
570
|
+
return source;
|
|
461
571
|
},
|
|
462
572
|
configResolved(config) {
|
|
463
573
|
root = config.root;
|
|
@@ -475,17 +585,31 @@ export function reticle(options = {}) {
|
|
|
475
585
|
* the dev server cleared it, which is not a step anybody guesses.
|
|
476
586
|
*
|
|
477
587
|
* Dropping the cached module before it is served makes `load` re-read the token, so starting the
|
|
478
|
-
* daemon and reloading the page is enough.
|
|
479
|
-
*
|
|
588
|
+
* daemon and reloading the page is enough.
|
|
589
|
+
*
|
|
590
|
+
* Only when the source would ACTUALLY differ, though. This used to invalidate on every request
|
|
591
|
+
* for the module, forever — and a module that is force-invalidated on every request is
|
|
592
|
+
* re-resolved against Vite's dep optimizer on every page load, which is the shape of a
|
|
593
|
+
* self-sustaining reload loop: reload → request → invalidate → re-resolve → reload. Reported
|
|
594
|
+
* from the field on a Vite + React Router app pinned to a non-default port: every route
|
|
595
|
+
* reloaded the whole page about once a second, `/@reticle-connect` was fetched in every cycle,
|
|
596
|
+
* and removing the plugin stopped it instantly. Comparing the source first costs one string
|
|
597
|
+
* compare, keeps the late-daemon fix intact (the token appearing IS a change), and makes the
|
|
598
|
+
* module inert once it has settled.
|
|
480
599
|
*/
|
|
481
600
|
configureServer(server) {
|
|
482
601
|
if (!inject)
|
|
483
602
|
return;
|
|
484
603
|
server.middlewares.use((req, _res, next) => {
|
|
485
604
|
if ((req.url ?? '').split('?')[0] === RETICLE_CONNECT_MODULE) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
605
|
+
if (currentConnectSource() !== lastServedConnectSource) {
|
|
606
|
+
connectChanges++;
|
|
607
|
+
if (CONNECT_CHURN_LIMIT === connectChanges)
|
|
608
|
+
warn(connectChurnWarning());
|
|
609
|
+
const mod = server.moduleGraph.getModuleById(RETICLE_CONNECT_MODULE);
|
|
610
|
+
if (mod !== undefined)
|
|
611
|
+
server.moduleGraph.invalidateModule(mod);
|
|
612
|
+
}
|
|
489
613
|
}
|
|
490
614
|
next();
|
|
491
615
|
});
|
package/dist/svelte-source.js
CHANGED
|
@@ -49,6 +49,13 @@ function defaultLoadCompiler() {
|
|
|
49
49
|
cachedCompiler = null;
|
|
50
50
|
// From the APP's root first. The plugin may be linked, hoisted, or in a pnpm store far from the
|
|
51
51
|
// project, and the compiler that matters is the one the app's own Svelte plugin will use.
|
|
52
|
+
//
|
|
53
|
+
// `import.meta.url` is this module's own location and is the ESM spelling of it. The package also
|
|
54
|
+
// ships a CJS build (an app without `"type": "module"` can only `require` its Vite config), where
|
|
55
|
+
// `import.meta` is empty — so that build SUBSTITUTES `__filename` here rather than leaving the
|
|
56
|
+
// expression to evaluate to nothing. Both artefacts therefore try the same two origins; see
|
|
57
|
+
// scripts/build-cjs.mjs. Reading it unguarded is deliberate: the build fails loudly if the
|
|
58
|
+
// substitution is ever dropped.
|
|
52
59
|
for (const from of [`${process.cwd()}/package.json`, import.meta.url]) {
|
|
53
60
|
try {
|
|
54
61
|
cachedCompiler = createRequire(from)('svelte/compiler');
|