@reticlehq/vite-plugin 2.14.0 → 3.1.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 +540 -472
- package/dist/index.d.cts +21 -31
- package/dist/index.d.ts +21 -31
- package/dist/index.js +75 -102
- package/dist/injection-postcondition.d.ts +81 -0
- package/dist/injection-postcondition.js +123 -0
- package/dist/plugin-name.d.ts +7 -0
- package/dist/plugin-name.js +7 -0
- package/dist/project-id.d.ts +24 -6
- package/dist/project-id.js +67 -22
- package/dist/vitest-browser.d.ts +2 -2
- package/dist/vitest-browser.js +2 -2
- package/dist/watch-ignore.d.ts +3 -3
- package/dist/watch-ignore.js +3 -3
- package/package.json +7 -6
- /package/dist/{ensure-token.d.ts → token/ensure-token.d.ts} +0 -0
- /package/dist/{ensure-token.js → token/ensure-token.js} +0 -0
- /package/dist/{missing-token.d.ts → token/missing-token.d.ts} +0 -0
- /package/dist/{missing-token.js → token/missing-token.js} +0 -0
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type InjectionWatch } from './injection-postcondition.js';
|
|
2
|
+
export { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
2
3
|
/**
|
|
3
4
|
* Compile-time global carrying the daemon's pairing token, for connects the plugin does not write
|
|
4
5
|
* itself. The bridge requires the token even on localhost, and nothing in a browser can read the
|
|
@@ -88,31 +89,22 @@ export interface ReticleVitePluginOptions {
|
|
|
88
89
|
* Off by default because a body is the one part of a request that routinely carries a card
|
|
89
90
|
* number, a token or a customer's address, and the daemon journals what it is told.
|
|
90
91
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* only `connect()` most apps ever have — had no way to pass it, and calling `connect()` a second
|
|
94
|
-
* time is a no-op. So for every app wired the recommended way, a payload was unreachable: on a
|
|
95
|
-
* real payments dashboard, a refund POSTing `amount: 1187.01` into a paise field (a 100x
|
|
96
|
-
* under-refund) was visible to Playwright's request inspector and invisible here.
|
|
92
|
+
* Exposed HERE because the plugin is the only `connect()` most apps have and a second `connect()`
|
|
93
|
+
* is a no-op, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
97
94
|
*
|
|
98
|
-
* Also settable as `VITE_RETICLE_CAPTURE_BODIES=1
|
|
99
|
-
* session without editing vite.config.
|
|
95
|
+
* Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`.
|
|
100
96
|
*/
|
|
101
97
|
captureNetworkBodies?: boolean;
|
|
102
98
|
/**
|
|
103
99
|
* Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
|
|
104
100
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* the inverse of the other env vars, because this is the one that defaults on (#800).
|
|
101
|
+
* Also settable as `VITE_RETICLE_NO_ERROR_BODIES=1`, which turns it OFF -- the inverse of the
|
|
102
|
+
* other env vars, because this is the one that defaults on (#800).
|
|
108
103
|
*/
|
|
109
104
|
captureErrorBodies?: boolean;
|
|
110
105
|
/**
|
|
111
106
|
* Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
|
|
112
107
|
*
|
|
113
|
-
* Reachable here for the same reason `captureNetworkBodies` is: the plugin is the only `connect()`
|
|
114
|
-
* most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
115
|
-
*
|
|
116
108
|
* The presenter is hidden from every tool by design — an agent that can drive Reticle's own
|
|
117
109
|
* interface can fabricate its own impact report. The cost is that a HUD change is the only kind of
|
|
118
110
|
* change Reticle cannot be used to check. This is the hatch for that one case, and the app reports
|
|
@@ -127,9 +119,7 @@ export interface ReticleVitePluginOptions {
|
|
|
127
119
|
* Off by default: the SDK refuses outside localhost so a page on the open internet cannot be
|
|
128
120
|
* instrumented by a bridge it happened to reach. Turn it on for a dev server that CANNOT be served
|
|
129
121
|
* on localhost — a host-based multi-tenant frontend, a white-label app resolving the tenant from
|
|
130
|
-
* the `Host` header, anything with cookie-scoped auth on a custom dev hostname.
|
|
131
|
-
* apps cannot use Reticle at all, because the plugin is the only `connect()` they have and a
|
|
132
|
-
* second, hand-written one is a no-op.
|
|
122
|
+
* the `Host` header, anything with cookie-scoped auth on a custom dev hostname.
|
|
133
123
|
*
|
|
134
124
|
* NOT SUFFICIENT ON ITS OWN — a pairing token is also required. `connectionPolicy` in
|
|
135
125
|
* `@reticlehq/browser` refuses a non-localhost connect with "a pairing token is required outside
|
|
@@ -138,8 +128,7 @@ export interface ReticleVitePluginOptions {
|
|
|
138
128
|
* started daemon is normally all it takes; pass `token` yourself only when the daemon's file is
|
|
139
129
|
* unreachable. A non-loopback BRIDGE additionally has to be `wss://`.
|
|
140
130
|
*
|
|
141
|
-
* Also settable as `VITE_RETICLE_ALLOW_NON_LOCALHOST=1
|
|
142
|
-
* without editing vite.config.
|
|
131
|
+
* Also settable as `VITE_RETICLE_ALLOW_NON_LOCALHOST=1`.
|
|
143
132
|
*/
|
|
144
133
|
allowNonLocalhost?: boolean;
|
|
145
134
|
/**
|
|
@@ -230,6 +219,9 @@ export interface ReticleVitePlugin {
|
|
|
230
219
|
buildEnd?: () => void;
|
|
231
220
|
/** Runs the dev-mode injection check immediately. Test seam for the deferred timer. */
|
|
232
221
|
checkInjectedForTest?: () => void;
|
|
222
|
+
checkHtmlHookForTest?: () => void;
|
|
223
|
+
/** The post-condition watch itself, so a test drives the real predicate and not a copy of it. */
|
|
224
|
+
injectionWatchForTest?: InjectionWatch;
|
|
233
225
|
}
|
|
234
226
|
/**
|
|
235
227
|
* The slice of Vite's dev server this plugin touches, structurally — so `vite` stays a peer the
|
|
@@ -259,6 +251,9 @@ export interface ViteDevServerLike {
|
|
|
259
251
|
middlewares: {
|
|
260
252
|
use(handler: (req: {
|
|
261
253
|
url?: string | undefined;
|
|
254
|
+
headers?: {
|
|
255
|
+
accept?: string | undefined;
|
|
256
|
+
} | undefined;
|
|
262
257
|
}, res: unknown, next: () => void) => void): void;
|
|
263
258
|
};
|
|
264
259
|
moduleGraph: {
|
|
@@ -286,10 +281,9 @@ export declare function readPairingToken(): string | undefined;
|
|
|
286
281
|
/**
|
|
287
282
|
* The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
|
|
288
283
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* Convention costs one `existsSync` and leaves their entry untouched.
|
|
284
|
+
* Imported by CONVENTION rather than by patching the app's entry file: the connect is injected into
|
|
285
|
+
* a virtual module, so the alternative is `init` editing `src/main.tsx` — a file the user owns — for
|
|
286
|
+
* something that is opt-in enrichment. Convention costs one `existsSync`.
|
|
293
287
|
*/
|
|
294
288
|
export declare const RETICLE_DEV_MODULE_CANDIDATES: readonly ["src/reticle-dev.ts", "src/reticle-dev.js", "src/reticle-dev.tsx", "src/reticle-dev.jsx"];
|
|
295
289
|
/** The app's dev module, as an importable path — or null when the app has none. */
|
|
@@ -297,14 +291,11 @@ export declare function findDevModule(root: string, exists: (p: string) => boole
|
|
|
297
291
|
/**
|
|
298
292
|
* Which SDK package this app actually has, and whether `install()` applies.
|
|
299
293
|
*
|
|
300
|
-
* The injected connect
|
|
294
|
+
* The injected connect must NOT name `@reticlehq/react` unconditionally. That is right for a React
|
|
301
295
|
* app and fatal for any other: `reticle init` gives a Vue or Svelte codebase the framework-neutral
|
|
302
296
|
* `@reticlehq/browser` — deliberately, because a package named `@reticlehq/react` with `react` in
|
|
303
|
-
* its peers has no business in a Vue app — and the injected import then
|
|
304
|
-
* installed, so nothing connects and the page reports no session with no obvious cause.
|
|
305
|
-
*
|
|
306
|
-
* Measured end to end on a pristine `npm create vite --template vue` app: init wrote every file
|
|
307
|
-
* correctly and the tab never dialled the daemon, because of this one specifier.
|
|
297
|
+
* its peers has no business in a Vue app — and the injected import would then name a package that is
|
|
298
|
+
* not installed, so nothing connects and the page reports no session with no obvious cause.
|
|
308
299
|
*
|
|
309
300
|
* The React kit WINS when both resolve: it is a superset (it re-exports the sensor and adds the
|
|
310
301
|
* adapter), so an app that has it wants component identity. `install()` is the adapter's alone and
|
|
@@ -339,5 +330,4 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
|
|
|
339
330
|
*/
|
|
340
331
|
export declare const JOURNAL_IGNORE: RegExp;
|
|
341
332
|
export declare function reticle(options?: ReticleVitePluginOptions): ReticleVitePlugin;
|
|
342
|
-
export {};
|
|
343
333
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type InjectionWatch } from './injection-postcondition.js';
|
|
2
|
+
export { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
2
3
|
/**
|
|
3
4
|
* Compile-time global carrying the daemon's pairing token, for connects the plugin does not write
|
|
4
5
|
* itself. The bridge requires the token even on localhost, and nothing in a browser can read the
|
|
@@ -88,31 +89,22 @@ export interface ReticleVitePluginOptions {
|
|
|
88
89
|
* Off by default because a body is the one part of a request that routinely carries a card
|
|
89
90
|
* number, a token or a customer's address, and the daemon journals what it is told.
|
|
90
91
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* only `connect()` most apps ever have — had no way to pass it, and calling `connect()` a second
|
|
94
|
-
* time is a no-op. So for every app wired the recommended way, a payload was unreachable: on a
|
|
95
|
-
* real payments dashboard, a refund POSTing `amount: 1187.01` into a paise field (a 100x
|
|
96
|
-
* under-refund) was visible to Playwright's request inspector and invisible here.
|
|
92
|
+
* Exposed HERE because the plugin is the only `connect()` most apps have and a second `connect()`
|
|
93
|
+
* is a no-op, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
97
94
|
*
|
|
98
|
-
* Also settable as `VITE_RETICLE_CAPTURE_BODIES=1
|
|
99
|
-
* session without editing vite.config.
|
|
95
|
+
* Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`.
|
|
100
96
|
*/
|
|
101
97
|
captureNetworkBodies?: boolean;
|
|
102
98
|
/**
|
|
103
99
|
* Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
|
|
104
100
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* the inverse of the other env vars, because this is the one that defaults on (#800).
|
|
101
|
+
* Also settable as `VITE_RETICLE_NO_ERROR_BODIES=1`, which turns it OFF -- the inverse of the
|
|
102
|
+
* other env vars, because this is the one that defaults on (#800).
|
|
108
103
|
*/
|
|
109
104
|
captureErrorBodies?: boolean;
|
|
110
105
|
/**
|
|
111
106
|
* Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
|
|
112
107
|
*
|
|
113
|
-
* Reachable here for the same reason `captureNetworkBodies` is: the plugin is the only `connect()`
|
|
114
|
-
* most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
115
|
-
*
|
|
116
108
|
* The presenter is hidden from every tool by design — an agent that can drive Reticle's own
|
|
117
109
|
* interface can fabricate its own impact report. The cost is that a HUD change is the only kind of
|
|
118
110
|
* change Reticle cannot be used to check. This is the hatch for that one case, and the app reports
|
|
@@ -127,9 +119,7 @@ export interface ReticleVitePluginOptions {
|
|
|
127
119
|
* Off by default: the SDK refuses outside localhost so a page on the open internet cannot be
|
|
128
120
|
* instrumented by a bridge it happened to reach. Turn it on for a dev server that CANNOT be served
|
|
129
121
|
* on localhost — a host-based multi-tenant frontend, a white-label app resolving the tenant from
|
|
130
|
-
* the `Host` header, anything with cookie-scoped auth on a custom dev hostname.
|
|
131
|
-
* apps cannot use Reticle at all, because the plugin is the only `connect()` they have and a
|
|
132
|
-
* second, hand-written one is a no-op.
|
|
122
|
+
* the `Host` header, anything with cookie-scoped auth on a custom dev hostname.
|
|
133
123
|
*
|
|
134
124
|
* NOT SUFFICIENT ON ITS OWN — a pairing token is also required. `connectionPolicy` in
|
|
135
125
|
* `@reticlehq/browser` refuses a non-localhost connect with "a pairing token is required outside
|
|
@@ -138,8 +128,7 @@ export interface ReticleVitePluginOptions {
|
|
|
138
128
|
* started daemon is normally all it takes; pass `token` yourself only when the daemon's file is
|
|
139
129
|
* unreachable. A non-loopback BRIDGE additionally has to be `wss://`.
|
|
140
130
|
*
|
|
141
|
-
* Also settable as `VITE_RETICLE_ALLOW_NON_LOCALHOST=1
|
|
142
|
-
* without editing vite.config.
|
|
131
|
+
* Also settable as `VITE_RETICLE_ALLOW_NON_LOCALHOST=1`.
|
|
143
132
|
*/
|
|
144
133
|
allowNonLocalhost?: boolean;
|
|
145
134
|
/**
|
|
@@ -230,6 +219,9 @@ export interface ReticleVitePlugin {
|
|
|
230
219
|
buildEnd?: () => void;
|
|
231
220
|
/** Runs the dev-mode injection check immediately. Test seam for the deferred timer. */
|
|
232
221
|
checkInjectedForTest?: () => void;
|
|
222
|
+
checkHtmlHookForTest?: () => void;
|
|
223
|
+
/** The post-condition watch itself, so a test drives the real predicate and not a copy of it. */
|
|
224
|
+
injectionWatchForTest?: InjectionWatch;
|
|
233
225
|
}
|
|
234
226
|
/**
|
|
235
227
|
* The slice of Vite's dev server this plugin touches, structurally — so `vite` stays a peer the
|
|
@@ -259,6 +251,9 @@ export interface ViteDevServerLike {
|
|
|
259
251
|
middlewares: {
|
|
260
252
|
use(handler: (req: {
|
|
261
253
|
url?: string | undefined;
|
|
254
|
+
headers?: {
|
|
255
|
+
accept?: string | undefined;
|
|
256
|
+
} | undefined;
|
|
262
257
|
}, res: unknown, next: () => void) => void): void;
|
|
263
258
|
};
|
|
264
259
|
moduleGraph: {
|
|
@@ -286,10 +281,9 @@ export declare function readPairingToken(): string | undefined;
|
|
|
286
281
|
/**
|
|
287
282
|
* The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
|
|
288
283
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* Convention costs one `existsSync` and leaves their entry untouched.
|
|
284
|
+
* Imported by CONVENTION rather than by patching the app's entry file: the connect is injected into
|
|
285
|
+
* a virtual module, so the alternative is `init` editing `src/main.tsx` — a file the user owns — for
|
|
286
|
+
* something that is opt-in enrichment. Convention costs one `existsSync`.
|
|
293
287
|
*/
|
|
294
288
|
export declare const RETICLE_DEV_MODULE_CANDIDATES: readonly ["src/reticle-dev.ts", "src/reticle-dev.js", "src/reticle-dev.tsx", "src/reticle-dev.jsx"];
|
|
295
289
|
/** The app's dev module, as an importable path — or null when the app has none. */
|
|
@@ -297,14 +291,11 @@ export declare function findDevModule(root: string, exists: (p: string) => boole
|
|
|
297
291
|
/**
|
|
298
292
|
* Which SDK package this app actually has, and whether `install()` applies.
|
|
299
293
|
*
|
|
300
|
-
* The injected connect
|
|
294
|
+
* The injected connect must NOT name `@reticlehq/react` unconditionally. That is right for a React
|
|
301
295
|
* app and fatal for any other: `reticle init` gives a Vue or Svelte codebase the framework-neutral
|
|
302
296
|
* `@reticlehq/browser` — deliberately, because a package named `@reticlehq/react` with `react` in
|
|
303
|
-
* its peers has no business in a Vue app — and the injected import then
|
|
304
|
-
* installed, so nothing connects and the page reports no session with no obvious cause.
|
|
305
|
-
*
|
|
306
|
-
* Measured end to end on a pristine `npm create vite --template vue` app: init wrote every file
|
|
307
|
-
* correctly and the tab never dialled the daemon, because of this one specifier.
|
|
297
|
+
* its peers has no business in a Vue app — and the injected import would then name a package that is
|
|
298
|
+
* not installed, so nothing connects and the page reports no session with no obvious cause.
|
|
308
299
|
*
|
|
309
300
|
* The React kit WINS when both resolve: it is a superset (it re-exports the sensor and adds the
|
|
310
301
|
* adapter), so an app that has it wants component identity. `install()` is the adapter's alone and
|
|
@@ -339,4 +330,3 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
|
|
|
339
330
|
*/
|
|
340
331
|
export declare const JOURNAL_IGNORE: RegExp;
|
|
341
332
|
export declare function reticle(options?: ReticleVitePluginOptions): ReticleVitePlugin;
|
|
342
|
-
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { missingTokenWarning } from './missing-token.js';
|
|
3
|
-
import { ensurePairingToken } from './ensure-token.js';
|
|
2
|
+
import { missingTokenWarning } from './token/missing-token.js';
|
|
3
|
+
import { ensurePairingToken } from './token/ensure-token.js';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { transformSync } from '@babel/core';
|
|
@@ -11,7 +11,9 @@ import { discoverDaemonPort } from './discover-port.js';
|
|
|
11
11
|
import { announceDevServer } from './announce.js';
|
|
12
12
|
import { SVELTE_FILE, stampSvelte } from './svelte-source.js';
|
|
13
13
|
import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, optimizerOptions, } from './installed.js';
|
|
14
|
-
|
|
14
|
+
import { createInjectionWatch, notInjectedMessage, } from './injection-postcondition.js';
|
|
15
|
+
import { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
16
|
+
export { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
15
17
|
// The React kit the host app imports the SDK from. It re-exports the browser sensor, so a single
|
|
16
18
|
// specifier yields both `reticle` (connect) and `install` (the React adapter). NOT `@reticlehq/core`
|
|
17
19
|
// — that is the isomorphic foundation and exports neither.
|
|
@@ -84,22 +86,14 @@ onScheduleFiberRoot:function(){},onCommitFiberRoot:fire,onPostCommitFiberRoot:fu
|
|
|
84
86
|
}else{var prev=h.onCommitFiberRoot;h.onCommitFiberRoot=function(){try{fire.apply(null,arguments);}catch(e){}
|
|
85
87
|
if(typeof prev==='function')return prev.apply(this,arguments);};}
|
|
86
88
|
}catch(e){}})();`;
|
|
87
|
-
/**
|
|
88
|
-
* How long after serving the HTML to wait before concluding the entry was never injected.
|
|
89
|
-
*
|
|
90
|
-
* Generous on purpose: the browser has to request the entry, and a cold dev server transforming a
|
|
91
|
-
* large app can take a moment. A false warning would train people to ignore a real one.
|
|
92
|
-
*/
|
|
93
|
-
const DEV_INJECTION_GRACE_MS = 10_000;
|
|
94
89
|
/**
|
|
95
90
|
* How many times the connect module's source may legitimately change in one dev-server session
|
|
96
91
|
* before the plugin says so.
|
|
97
92
|
*
|
|
98
93
|
* The source is a function of the port, the projectId, the pairing token and whether the app has a
|
|
99
|
-
* `reticle-dev` module
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* every page load — the reload loop this counter exists to make audible instead of mysterious.
|
|
94
|
+
* `reticle-dev` module — in a healthy session, the daemon starting after Vite is one change and a
|
|
95
|
+
* dev module appearing is another. Past a handful, an input is oscillating, which makes Vite
|
|
96
|
+
* re-resolve the module on every page load: the reload loop this counter makes audible.
|
|
103
97
|
*/
|
|
104
98
|
const CONNECT_CHURN_LIMIT = 5;
|
|
105
99
|
/**
|
|
@@ -260,10 +254,9 @@ function connectArgs(options) {
|
|
|
260
254
|
/**
|
|
261
255
|
* The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
|
|
262
256
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* Convention costs one `existsSync` and leaves their entry untouched.
|
|
257
|
+
* Imported by CONVENTION rather than by patching the app's entry file: the connect is injected into
|
|
258
|
+
* a virtual module, so the alternative is `init` editing `src/main.tsx` — a file the user owns — for
|
|
259
|
+
* something that is opt-in enrichment. Convention costs one `existsSync`.
|
|
267
260
|
*/
|
|
268
261
|
export const RETICLE_DEV_MODULE_CANDIDATES = [
|
|
269
262
|
'src/reticle-dev.ts',
|
|
@@ -282,14 +275,11 @@ export function findDevModule(root, exists) {
|
|
|
282
275
|
/**
|
|
283
276
|
* Which SDK package this app actually has, and whether `install()` applies.
|
|
284
277
|
*
|
|
285
|
-
* The injected connect
|
|
278
|
+
* The injected connect must NOT name `@reticlehq/react` unconditionally. That is right for a React
|
|
286
279
|
* app and fatal for any other: `reticle init` gives a Vue or Svelte codebase the framework-neutral
|
|
287
280
|
* `@reticlehq/browser` — deliberately, because a package named `@reticlehq/react` with `react` in
|
|
288
|
-
* its peers has no business in a Vue app — and the injected import then
|
|
289
|
-
* installed, so nothing connects and the page reports no session with no obvious cause.
|
|
290
|
-
*
|
|
291
|
-
* Measured end to end on a pristine `npm create vite --template vue` app: init wrote every file
|
|
292
|
-
* correctly and the tab never dialled the daemon, because of this one specifier.
|
|
281
|
+
* its peers has no business in a Vue app — and the injected import would then name a package that is
|
|
282
|
+
* not installed, so nothing connects and the page reports no session with no obvious cause.
|
|
293
283
|
*
|
|
294
284
|
* The React kit WINS when both resolve: it is a superset (it re-exports the sensor and adds the
|
|
295
285
|
* adapter), so an app that has it wants component identity. `install()` is the adapter's alone and
|
|
@@ -388,6 +378,14 @@ export function reticle(options = {}) {
|
|
|
388
378
|
const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
|
|
389
379
|
/** Whether connect() actually reached a module — asserted at buildEnd, never assumed. */
|
|
390
380
|
let injected = false;
|
|
381
|
+
/**
|
|
382
|
+
* Whether Vite ever asked us to transform the app's HTML.
|
|
383
|
+
*
|
|
384
|
+
* On the web this is how the connect script gets in, so "this never happened" and "this app will
|
|
385
|
+
* not connect" are the same statement — which is what makes a warning safe here, unlike the
|
|
386
|
+
* desktop entry-module flag above.
|
|
387
|
+
*/
|
|
388
|
+
let htmlTransformed = false;
|
|
391
389
|
/**
|
|
392
390
|
* Resolve port + token at the moment of injection, not at plugin construction. By the time a
|
|
393
391
|
* module is served or built the daemon is up and has written its pairing token; resolving early
|
|
@@ -415,37 +413,17 @@ export function reticle(options = {}) {
|
|
|
415
413
|
/** How many times the served source has actually changed. See connectChurnWarning. */
|
|
416
414
|
let connectChanges = 0;
|
|
417
415
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
416
|
+
* When to conclude injection failed, and which of the three messages to say. Lives beside those
|
|
417
|
+
* messages rather than here: choosing between them is the subtlety, and it was a thousand lines
|
|
418
|
+
* from the wording it chose. The flags are read through getters because both flip mid-session.
|
|
420
419
|
*/
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
*
|
|
429
|
-
* In serve, `injected` records "my transform ran THIS session", which is not the same as "the app
|
|
430
|
-
* has no connect()". Vite serves an unchanged module straight from its transform cache, so on a
|
|
431
|
-
* warm cache the transform never runs, the flag stays false, and the old wording announced that
|
|
432
|
-
* the app "will never connect" while the served entry demonstrably contained the injection —
|
|
433
|
-
* verified by fetching it from the dev server. A false alarm, in the tool whose entire argument is
|
|
434
|
-
* that it does not raise them.
|
|
435
|
-
*
|
|
436
|
-
* So dev reports what it actually knows: unconfirmed, with the benign explanation first.
|
|
437
|
-
*/
|
|
438
|
-
const unconfirmedInjectionMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not confirm reticle.connect() was injected: the HTML entry ` +
|
|
439
|
-
'module was not transformed this session. That is expected when Vite served it from its ' +
|
|
440
|
-
'transform cache. If the app does not appear in `reticle status`, restart the dev server with ' +
|
|
441
|
-
'`--force` to bypass the cache, then check that index.html references your entry with a ' +
|
|
442
|
-
'<script type="module" src="...">.';
|
|
443
|
-
/** Warn (never throw) in dev — a running dev server should report the doubt, not die of it. */
|
|
444
|
-
const checkInjected = () => {
|
|
445
|
-
if (!desktop || !inject || injected)
|
|
446
|
-
return;
|
|
447
|
-
warn(unconfirmedInjectionMessage());
|
|
448
|
-
};
|
|
420
|
+
const watch = createInjectionWatch({
|
|
421
|
+
desktop,
|
|
422
|
+
inject,
|
|
423
|
+
injected: () => injected,
|
|
424
|
+
htmlTransformed: () => htmlTransformed,
|
|
425
|
+
warn,
|
|
426
|
+
});
|
|
449
427
|
return {
|
|
450
428
|
name: RETICLE_VITE_PLUGIN_NAME,
|
|
451
429
|
// Web: serve-only, so a production bundle can never carry the SDK — gating is the tool's job.
|
|
@@ -456,42 +434,31 @@ export function reticle(options = {}) {
|
|
|
456
434
|
/**
|
|
457
435
|
* Declare the SDK itself and the optimizer cache fingerprint.
|
|
458
436
|
*
|
|
459
|
-
* The
|
|
460
|
-
*
|
|
461
|
-
*
|
|
437
|
+
* The SDK itself only. It does not import a second accessibility engine, and naming CJS deps it
|
|
438
|
+
* does not use would make Vite pre-bundle packages the app may not have, then blame Reticle for a
|
|
439
|
+
* false `Failed to resolve dependency` warning.
|
|
462
440
|
*/
|
|
463
441
|
config(config) {
|
|
464
442
|
// Everything below asks what the APP has installed, so every lookup is rooted here and never
|
|
465
|
-
// at the plugin's own location. Vite defaults an omitted root to the cwd
|
|
443
|
+
// at the plugin's own location. Vite defaults an omitted root to the cwd, and so does this.
|
|
466
444
|
const appRoot = config.root ?? process.cwd();
|
|
467
445
|
const optimizerKey = optimizerOptionsKey(viteMajor(appRoot));
|
|
468
446
|
return {
|
|
469
447
|
// Keep the daemon's journal out of the dev server's watcher.
|
|
470
448
|
//
|
|
471
449
|
// The daemon writes `.reticle/` into the PROJECT root — session journals, and `ambient.json`
|
|
472
|
-
// rewritten atomically as `ambient.json.tmp` + rename on a live session.
|
|
473
|
-
//
|
|
474
|
-
//
|
|
450
|
+
// rewritten atomically as `ambient.json.tmp` + rename on a live session. Unignored, every
|
|
451
|
+
// journal write reads as a project file changing and Vite answers with a full page reload,
|
|
452
|
+
// which is a loop with no exit: page loads -> SDK connects and streams events -> daemon
|
|
453
|
+
// journals them -> Vite reloads the page -> SDK reconnects.
|
|
475
454
|
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
// misattributed: every ref went stale, every act_and_wait died mid-flight, and the log
|
|
480
|
-
// filled with connect/disconnect pairs that looked like a flapping SDK rather than a
|
|
481
|
-
// watcher chasing its own tail.
|
|
482
|
-
//
|
|
483
|
-
// A RegExp, not a glob, and that is the whole difference between this working and not.
|
|
484
|
-
// chokidar dropped glob support in v4 — Vite 7+ ships v4/v5, where a pattern like
|
|
485
|
-
// `**/.reticle/**` is silently accepted and matches nothing. MEASURED against the chokidar
|
|
486
|
-
// this repo resolves: with the glob, a write to `.reticle/ambient.json` still fires; with
|
|
487
|
-
// this RegExp it does not, while a normal file still does. Vite's own defaults are globs and
|
|
488
|
-
// have the same problem, which is why it is not safe to copy their shape here.
|
|
455
|
+
// A RegExp, NOT a glob: chokidar dropped glob support in v4, and Vite 7+ ships v4/v5, where
|
|
456
|
+
// `**/.reticle/**` is silently accepted and matches nothing. Vite's own defaults are globs
|
|
457
|
+
// and have the same problem, so their shape is not safe to copy here.
|
|
489
458
|
//
|
|
490
459
|
// Anchored on `^` or a separator so it matches the directory and not a file that merely ends
|
|
491
460
|
// in those characters, and both separators are accepted because chokidar reports the path in
|
|
492
|
-
// the platform's own form.
|
|
493
|
-
//
|
|
494
|
-
// Appends to the app's list rather than replacing it, so nothing it already excluded is lost.
|
|
461
|
+
// the platform's own form. Appends to the app's list rather than replacing it.
|
|
495
462
|
server: {
|
|
496
463
|
watch: {
|
|
497
464
|
ignored: mergeIgnored(config.server?.watch?.ignored, JOURNAL_IGNORE),
|
|
@@ -499,8 +466,8 @@ export function reticle(options = {}) {
|
|
|
499
466
|
},
|
|
500
467
|
// Expose the daemon's pairing token to hand-written connects in the same Vite app. The
|
|
501
468
|
// plugin's own injected connect gets the token directly, but a connect the USER writes —
|
|
502
|
-
// SvelteKit's client hook, a custom entry —
|
|
503
|
-
//
|
|
469
|
+
// SvelteKit's client hook, a custom entry — cannot reach a file only Node can read, so it
|
|
470
|
+
// would call connect() with no credential and the bridge would answer "authentication
|
|
504
471
|
// failed". Empty until the daemon has provisioned one; the page reloads once it has.
|
|
505
472
|
define: {
|
|
506
473
|
...(config.define ?? {}),
|
|
@@ -598,29 +565,32 @@ export function reticle(options = {}) {
|
|
|
598
565
|
* Serve the connect module fresh, every time.
|
|
599
566
|
*
|
|
600
567
|
* `load` reads the daemon's pairing token at serve time precisely because the daemon may start
|
|
601
|
-
* after the dev server — but Vite caches the module it produced
|
|
602
|
-
* from that cache, INCLUDING after a full page reload.
|
|
603
|
-
* tokenless connect module
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
568
|
+
* after the dev server — but Vite caches the module it produced and answers every later request
|
|
569
|
+
* from that cache, INCLUDING after a full page reload. A dev server started first therefore keeps
|
|
570
|
+
* serving a tokenless connect module: the SDK gets a 1008 `authentication failed` and stops
|
|
571
|
+
* retrying (correctly — a wrong token does not fix itself), so `reticle status` shows no session
|
|
572
|
+
* while the page demonstrably contains `/@reticle-connect`, and only a dev-server restart clears
|
|
573
|
+
* it.
|
|
607
574
|
*
|
|
608
575
|
* Dropping the cached module before it is served makes `load` re-read the token, so starting the
|
|
609
576
|
* daemon and reloading the page is enough.
|
|
610
577
|
*
|
|
611
|
-
* Only when the source would ACTUALLY differ, though.
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
* reloaded the whole page about once a second, `/@reticle-connect` was fetched in every cycle,
|
|
617
|
-
* and removing the plugin stopped it instantly. Comparing the source first costs one string
|
|
618
|
-
* compare, keeps the late-daemon fix intact (the token appearing IS a change), and makes the
|
|
619
|
-
* module inert once it has settled.
|
|
578
|
+
* Only when the source would ACTUALLY differ, though. A module force-invalidated on every
|
|
579
|
+
* request is re-resolved against Vite's dep optimizer on every page load, which is a
|
|
580
|
+
* self-sustaining reload loop: reload → request → invalidate → re-resolve → reload. Comparing
|
|
581
|
+
* the source first costs one string compare, keeps the late-daemon fix intact (the token
|
|
582
|
+
* appearing IS a change), and makes the module inert once it has settled.
|
|
620
583
|
*/
|
|
621
584
|
configureServer(server) {
|
|
622
585
|
if (!inject)
|
|
623
586
|
return;
|
|
587
|
+
// The web post-condition is armed by the first DOCUMENT REQUEST, in the middleware below.
|
|
588
|
+
//
|
|
589
|
+
// Not from `transformIndexHtml`, because a framework that renders its own HTML never calls it
|
|
590
|
+
// and the check would be unreachable in the one case it exists for. But not from boot either:
|
|
591
|
+
// that fires whether or not anybody has opened the app, and tells a healthy project it will
|
|
592
|
+
// never connect. The request is the earliest moment the plugin knows enough to have an
|
|
593
|
+
// opinion.
|
|
624
594
|
// Tell `~/.reticle` this dev server exists, the moment it is actually listening.
|
|
625
595
|
//
|
|
626
596
|
// This is the one fact nobody outside this process could observe: the plugin is loaded in the
|
|
@@ -666,6 +636,11 @@ export function reticle(options = {}) {
|
|
|
666
636
|
announce();
|
|
667
637
|
}
|
|
668
638
|
server.middlewares.use((req, _res, next) => {
|
|
639
|
+
// One middleware, two observations. A second `use()` would work equally well in Vite and
|
|
640
|
+
// is the obvious way to write this, but it makes the ORDER of registration load-bearing for
|
|
641
|
+
// anything that records a single handler — so both live here instead.
|
|
642
|
+
if (!desktop && watch.isDocumentRequest(req))
|
|
643
|
+
watch.noteHtmlRequest();
|
|
669
644
|
// Matched against BOTH forms: plugin middlewares run ahead of Vite's own base
|
|
670
645
|
// middleware, so the request still carries `base` here, while a middleware-mode host may
|
|
671
646
|
// have stripped it already.
|
|
@@ -693,15 +668,13 @@ export function reticle(options = {}) {
|
|
|
693
668
|
return;
|
|
694
669
|
throw new Error(notInjectedMessage());
|
|
695
670
|
},
|
|
696
|
-
checkInjectedForTest: checkInjected,
|
|
671
|
+
checkInjectedForTest: watch.checkInjected,
|
|
672
|
+
checkHtmlHookForTest: watch.checkHtmlHookRan,
|
|
673
|
+
injectionWatchForTest: watch,
|
|
697
674
|
transformIndexHtml() {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (desktop && inject && 'serve' === command) {
|
|
702
|
-
const timer = setTimeout(checkInjected, DEV_INJECTION_GRACE_MS);
|
|
703
|
-
timer.unref?.();
|
|
704
|
-
}
|
|
675
|
+
htmlTransformed = true;
|
|
676
|
+
if (desktop && inject && 'serve' === command)
|
|
677
|
+
watch.armDesktopCheck();
|
|
705
678
|
// Desktop injects via the entry module instead (see transform) — a tag here would be a dead
|
|
706
679
|
// URL in a packaged build. A Vitest run gets nothing unless `inject: true` says otherwise —
|
|
707
680
|
// see isVitestBrowserServer.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check that says whether `reticle.connect()` actually made it into the page, and what to say
|
|
3
|
+
* when it did not.
|
|
4
|
+
*
|
|
5
|
+
* These three messages live together because choosing between them is the whole subtlety. Each says
|
|
6
|
+
* exactly as much as the plugin actually knows and no more, and getting that wrong in either
|
|
7
|
+
* direction has cost real users:
|
|
8
|
+
*
|
|
9
|
+
* - Claim "this is broken" when it is not, and the tool whose entire pitch is that it does not
|
|
10
|
+
* raise false alarms has just raised one.
|
|
11
|
+
* - Say nothing when it IS broken, and someone waits twenty minutes for a session that can never
|
|
12
|
+
* arrive.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* How long to wait before concluding that the connect script never made it in.
|
|
16
|
+
*
|
|
17
|
+
* Generous on purpose: the browser has to request the entry, and a cold dev server transforming a
|
|
18
|
+
* large app can take a moment. A false warning would train people to ignore a real one.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEV_INJECTION_GRACE_MS = 10000;
|
|
21
|
+
/**
|
|
22
|
+
* The BUILD message. A build always runs every transform, so "my transform never ran" and "the
|
|
23
|
+
* bundle has no connect()" are the same statement there, and stating it as a certainty is correct.
|
|
24
|
+
*/
|
|
25
|
+
export declare const notInjectedMessage: () => string;
|
|
26
|
+
/**
|
|
27
|
+
* The DEV message, which must be weaker — and this is the whole reason the two are separate.
|
|
28
|
+
*
|
|
29
|
+
* In serve, `injected` records "my transform ran THIS session", which is not the same as "the app
|
|
30
|
+
* has no connect()". Vite serves an unchanged module straight from its transform cache, so on a
|
|
31
|
+
* warm cache the transform never runs and the flag stays false, so wording that announces the app
|
|
32
|
+
* "will never connect" is a false alarm raised over a served entry that does contain the injection.
|
|
33
|
+
*
|
|
34
|
+
* So dev reports what it actually knows: unconfirmed, with the benign explanation first.
|
|
35
|
+
*/
|
|
36
|
+
export declare const unconfirmedInjectionMessage: () => string;
|
|
37
|
+
/**
|
|
38
|
+
* The web message.
|
|
39
|
+
*
|
|
40
|
+
* On the web the connect script is added by `transformIndexHtml`. A framework that renders its own
|
|
41
|
+
* HTML never calls that hook, so the script is never added and the app never appears in
|
|
42
|
+
* `reticle status`. Known examples: SvelteKit, Nuxt, Astro, React Router in framework mode and
|
|
43
|
+
* TanStack Start. Each was found the same way — a user waiting on a session that could never
|
|
44
|
+
* arrive — so the plain-language cause comes first and the fix comes second.
|
|
45
|
+
*/
|
|
46
|
+
export declare const htmlHookNeverRanMessage: () => string;
|
|
47
|
+
/**
|
|
48
|
+
* The state behind those three messages, and the rules for reaching each one.
|
|
49
|
+
*
|
|
50
|
+
* Beside the wording rather than in the plugin factory: choosing BETWEEN these messages is the whole
|
|
51
|
+
* subtlety, the flags are only ever read by these four functions, and every defect in this area has
|
|
52
|
+
* been a rule about when to speak rather than a bug in the speaking.
|
|
53
|
+
*/
|
|
54
|
+
export interface InjectionWatchDeps {
|
|
55
|
+
/** Desktop builds keep the certainty; web dev does not. See the two messages above. */
|
|
56
|
+
readonly desktop: boolean;
|
|
57
|
+
/** `inject: false` means the user wires connect() themselves; the plugin has no opinion then. */
|
|
58
|
+
readonly inject: boolean;
|
|
59
|
+
/** Whether the desktop entry module was transformed. Read late: it flips during the session. */
|
|
60
|
+
readonly injected: () => boolean;
|
|
61
|
+
/** Whether Vite ever asked us to transform the app's HTML. Read late, for the same reason. */
|
|
62
|
+
readonly htmlTransformed: () => boolean;
|
|
63
|
+
readonly warn: (message: string) => void;
|
|
64
|
+
/** Injected so a test does not wait ten real seconds. Defaults to the module-level timer. */
|
|
65
|
+
readonly schedule?: (run: () => void, ms: number) => void;
|
|
66
|
+
}
|
|
67
|
+
export interface InjectionWatch {
|
|
68
|
+
/** Record a browser asking for a page, and start the grace period from there. */
|
|
69
|
+
noteHtmlRequest: () => void;
|
|
70
|
+
/** A navigation, as opposed to the module and asset fetches Vite serves constantly. */
|
|
71
|
+
isDocumentRequest: (req: {
|
|
72
|
+
headers?: {
|
|
73
|
+
accept?: string | undefined;
|
|
74
|
+
} | undefined;
|
|
75
|
+
}) => boolean;
|
|
76
|
+
checkHtmlHookRan: () => void;
|
|
77
|
+
checkInjected: () => void;
|
|
78
|
+
/** Defer the desktop check past the HTML response, which lands before the entry module. */
|
|
79
|
+
armDesktopCheck: () => void;
|
|
80
|
+
}
|
|
81
|
+
export declare function createInjectionWatch(deps: InjectionWatchDeps): InjectionWatch;
|