@reticlehq/vite-plugin 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/connect-args.d.ts +10 -0
- package/dist/connect-args.js +71 -0
- package/dist/disabled-browser-stub.d.ts +15 -0
- package/dist/disabled-browser-stub.js +200 -0
- package/dist/discover-port.d.ts +32 -1
- package/dist/discover-port.js +43 -4
- package/dist/index.cjs +491 -178
- package/dist/index.d.cts +27 -15
- package/dist/index.d.ts +27 -15
- package/dist/index.js +73 -117
- package/dist/injection-postcondition.d.ts +12 -3
- package/dist/injection-postcondition.js +13 -8
- package/dist/project-id.d.ts +8 -0
- package/dist/project-id.js +23 -7
- package/dist/stamping.d.ts +37 -0
- package/dist/stamping.js +99 -0
- package/package.json +5 -5
package/dist/index.d.cts
CHANGED
|
@@ -72,15 +72,15 @@ export interface ReticleVitePluginOptions {
|
|
|
72
72
|
* This build is an Electron/Tauri renderer. Changes two things a desktop shell needs and a web app
|
|
73
73
|
* must not get:
|
|
74
74
|
*
|
|
75
|
-
* -
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* - connect() is injected into the HTML's entry module rather than as a virtual <script src>,
|
|
76
|
+
* so a packaged renderer with no dev server still resolves it — but only for a build run in a
|
|
77
|
+
* NON-production mode (`vite build --mode development`). A production-mode build is stubbed
|
|
78
|
+
* exactly like a web one, so a release binary never carries the SDK. See
|
|
79
|
+
* isReticleDisabledBuild.
|
|
78
80
|
* - `connect()` is called with `allowInProduction`, because that same renderer reports
|
|
79
81
|
* NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
|
|
80
82
|
*
|
|
81
|
-
* Off by default and never inferred
|
|
82
|
-
* is exactly what a web app must never ship. Keep it behind your own dev-only build (a dev target,
|
|
83
|
-
* or `process.env.NODE_ENV !== 'production'` in vite.config) so it cannot reach a release binary.
|
|
83
|
+
* Off by default and never inferred.
|
|
84
84
|
*/
|
|
85
85
|
desktop?: boolean;
|
|
86
86
|
/**
|
|
@@ -95,6 +95,20 @@ export interface ReticleVitePluginOptions {
|
|
|
95
95
|
* Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`.
|
|
96
96
|
*/
|
|
97
97
|
captureNetworkBodies?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Per-body character cap for captured bodies. Default 8192; clamped to [256, 262144].
|
|
100
|
+
*
|
|
101
|
+
* Reachable here for the reason `captureNetworkBodies` is: the plugin is the only `connect()`
|
|
102
|
+
* most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
103
|
+
*
|
|
104
|
+
* Raise it to make a NEGATIVE `bodyContains` decidable -- a negation is checked over the whole
|
|
105
|
+
* payload, so on a list endpoint bigger than the cap it is permanently undecidable, and that is
|
|
106
|
+
* the class that proves "this dangerous field is absent from every row" (#799).
|
|
107
|
+
*
|
|
108
|
+
* Also settable as `VITE_RETICLE_BODY_MAX_CHARS=65536`, so one run can raise it without editing
|
|
109
|
+
* vite.config.
|
|
110
|
+
*/
|
|
111
|
+
networkBodyMaxChars?: number;
|
|
98
112
|
/**
|
|
99
113
|
* Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
|
|
100
114
|
*
|
|
@@ -195,8 +209,6 @@ export interface ReticleVitePlugin {
|
|
|
195
209
|
};
|
|
196
210
|
};
|
|
197
211
|
};
|
|
198
|
-
/** Absent in desktop mode, where the plugin must also run for `vite build`. */
|
|
199
|
-
apply?: 'serve';
|
|
200
212
|
enforce: 'pre';
|
|
201
213
|
transform: (code: string, id: string) => {
|
|
202
214
|
code: string;
|
|
@@ -209,6 +221,8 @@ export interface ReticleVitePlugin {
|
|
|
209
221
|
configResolved?: (config: {
|
|
210
222
|
root?: string;
|
|
211
223
|
command?: string;
|
|
224
|
+
/** 'production' unless `--mode` says otherwise. See isReticleDisabledBuild. */
|
|
225
|
+
mode?: string;
|
|
212
226
|
base?: string;
|
|
213
227
|
/** Vitest's block, read only to spot browser mode. See isVitestBrowserServer. */
|
|
214
228
|
test?: unknown;
|
|
@@ -313,14 +327,12 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
|
|
|
313
327
|
* import { reticle } from '@reticlehq/vite-plugin';
|
|
314
328
|
* export default defineConfig({ plugins: [react(), reticle()] });
|
|
315
329
|
*
|
|
316
|
-
*
|
|
317
|
-
* bundle
|
|
330
|
+
* The web plugin also builds, replacing the browser SDK with an inert stub so a default
|
|
331
|
+
* production bundle ships no Reticle runtime code at all.
|
|
318
332
|
*
|
|
319
|
-
* `desktop: true`
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
* caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
|
|
323
|
-
* a release binary.
|
|
333
|
+
* `desktop: true` keeps that guarantee for a production-mode build. It instruments `serve`, and a
|
|
334
|
+
* build only when it runs in another mode (`vite build --mode development`), which is how a packaged
|
|
335
|
+
* smoke renderer with no dev server still gets its connect(). See isReticleDisabledBuild.
|
|
324
336
|
*/
|
|
325
337
|
/**
|
|
326
338
|
* The daemon's journal directory, as a matcher every chokidar major honours.
|
package/dist/index.d.ts
CHANGED
|
@@ -72,15 +72,15 @@ export interface ReticleVitePluginOptions {
|
|
|
72
72
|
* This build is an Electron/Tauri renderer. Changes two things a desktop shell needs and a web app
|
|
73
73
|
* must not get:
|
|
74
74
|
*
|
|
75
|
-
* -
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* - connect() is injected into the HTML's entry module rather than as a virtual <script src>,
|
|
76
|
+
* so a packaged renderer with no dev server still resolves it — but only for a build run in a
|
|
77
|
+
* NON-production mode (`vite build --mode development`). A production-mode build is stubbed
|
|
78
|
+
* exactly like a web one, so a release binary never carries the SDK. See
|
|
79
|
+
* isReticleDisabledBuild.
|
|
78
80
|
* - `connect()` is called with `allowInProduction`, because that same renderer reports
|
|
79
81
|
* NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
|
|
80
82
|
*
|
|
81
|
-
* Off by default and never inferred
|
|
82
|
-
* is exactly what a web app must never ship. Keep it behind your own dev-only build (a dev target,
|
|
83
|
-
* or `process.env.NODE_ENV !== 'production'` in vite.config) so it cannot reach a release binary.
|
|
83
|
+
* Off by default and never inferred.
|
|
84
84
|
*/
|
|
85
85
|
desktop?: boolean;
|
|
86
86
|
/**
|
|
@@ -95,6 +95,20 @@ export interface ReticleVitePluginOptions {
|
|
|
95
95
|
* Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`.
|
|
96
96
|
*/
|
|
97
97
|
captureNetworkBodies?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Per-body character cap for captured bodies. Default 8192; clamped to [256, 262144].
|
|
100
|
+
*
|
|
101
|
+
* Reachable here for the reason `captureNetworkBodies` is: the plugin is the only `connect()`
|
|
102
|
+
* most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
103
|
+
*
|
|
104
|
+
* Raise it to make a NEGATIVE `bodyContains` decidable -- a negation is checked over the whole
|
|
105
|
+
* payload, so on a list endpoint bigger than the cap it is permanently undecidable, and that is
|
|
106
|
+
* the class that proves "this dangerous field is absent from every row" (#799).
|
|
107
|
+
*
|
|
108
|
+
* Also settable as `VITE_RETICLE_BODY_MAX_CHARS=65536`, so one run can raise it without editing
|
|
109
|
+
* vite.config.
|
|
110
|
+
*/
|
|
111
|
+
networkBodyMaxChars?: number;
|
|
98
112
|
/**
|
|
99
113
|
* Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
|
|
100
114
|
*
|
|
@@ -195,8 +209,6 @@ export interface ReticleVitePlugin {
|
|
|
195
209
|
};
|
|
196
210
|
};
|
|
197
211
|
};
|
|
198
|
-
/** Absent in desktop mode, where the plugin must also run for `vite build`. */
|
|
199
|
-
apply?: 'serve';
|
|
200
212
|
enforce: 'pre';
|
|
201
213
|
transform: (code: string, id: string) => {
|
|
202
214
|
code: string;
|
|
@@ -209,6 +221,8 @@ export interface ReticleVitePlugin {
|
|
|
209
221
|
configResolved?: (config: {
|
|
210
222
|
root?: string;
|
|
211
223
|
command?: string;
|
|
224
|
+
/** 'production' unless `--mode` says otherwise. See isReticleDisabledBuild. */
|
|
225
|
+
mode?: string;
|
|
212
226
|
base?: string;
|
|
213
227
|
/** Vitest's block, read only to spot browser mode. See isVitestBrowserServer. */
|
|
214
228
|
test?: unknown;
|
|
@@ -313,14 +327,12 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
|
|
|
313
327
|
* import { reticle } from '@reticlehq/vite-plugin';
|
|
314
328
|
* export default defineConfig({ plugins: [react(), reticle()] });
|
|
315
329
|
*
|
|
316
|
-
*
|
|
317
|
-
* bundle
|
|
330
|
+
* The web plugin also builds, replacing the browser SDK with an inert stub so a default
|
|
331
|
+
* production bundle ships no Reticle runtime code at all.
|
|
318
332
|
*
|
|
319
|
-
* `desktop: true`
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
* caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
|
|
323
|
-
* a release binary.
|
|
333
|
+
* `desktop: true` keeps that guarantee for a production-mode build. It instruments `serve`, and a
|
|
334
|
+
* build only when it runs in another mode (`vite build --mode development`), which is how a packaged
|
|
335
|
+
* smoke renderer with no dev server still gets its connect(). See isReticleDisabledBuild.
|
|
324
336
|
*/
|
|
325
337
|
/**
|
|
326
338
|
* The daemon's journal directory, as a matcher every chokidar major honours.
|
package/dist/index.js
CHANGED
|
@@ -3,14 +3,15 @@ import { missingTokenWarning } from './token/missing-token.js';
|
|
|
3
3
|
import { ensurePairingToken } from './token/ensure-token.js';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import {
|
|
7
|
-
import reticleSource from '@reticlehq/babel-plugin';
|
|
8
|
-
import { RETICLE_DEFAULT_PORT, RETICLE_RENDER_PREHOOK, bridgeWsUrl, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
|
|
6
|
+
import { RETICLE_RENDER_PREHOOK, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
|
|
9
7
|
import { resolveProjectId } from './project-id.js';
|
|
10
|
-
import {
|
|
8
|
+
import { resolveDaemonPort } from './discover-port.js';
|
|
11
9
|
import { announceDevServer } from './announce.js';
|
|
12
|
-
import {
|
|
10
|
+
import { stampSvelte } from './svelte-source.js';
|
|
11
|
+
import { NODE_MODULES, ignoredFileNotice, optsOutOfStamping, shouldStamp, shouldStampSvelte, stamp, } from './stamping.js';
|
|
13
12
|
import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, optimizerOptions, } from './installed.js';
|
|
13
|
+
import { RETICLE_DISABLED_STUB_CODE, RETICLE_DISABLED_STUB, isReticleDisabledBuild, } from './disabled-browser-stub.js';
|
|
14
|
+
import { connectArgs } from './connect-args.js';
|
|
14
15
|
import { createInjectionWatch, notInjectedMessage, } from './injection-postcondition.js';
|
|
15
16
|
import { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
16
17
|
export { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
@@ -26,11 +27,6 @@ const RETICLE_SENSOR = '@reticlehq/browser';
|
|
|
26
27
|
* file it lives in.
|
|
27
28
|
*/
|
|
28
29
|
export const RETICLE_TOKEN_GLOBAL = '__RETICLE_TOKEN__';
|
|
29
|
-
/** Files we stamp with source info — JSX/TSX only. */
|
|
30
|
-
const JSX_FILE = /\.[jt]sx$/;
|
|
31
|
-
/** Rollup virtual-module ids start with a NUL byte; never transform those. */
|
|
32
|
-
const VIRTUAL_PREFIX = '\0';
|
|
33
|
-
const NODE_MODULES = 'node_modules';
|
|
34
30
|
/**
|
|
35
31
|
* The connect code is served as a real module (not an inline <script>) so that Vite's import
|
|
36
32
|
* pipeline resolves the bare `@reticlehq/react` specifier. An inline injected script is NOT run through
|
|
@@ -132,40 +128,6 @@ function isHtmlEntry(id, specifier, root) {
|
|
|
132
128
|
// buildEnd post-condition means a wrong match cannot pass unnoticed as "nothing happened".
|
|
133
129
|
return candidate.endsWith(target.startsWith('/') ? target : `/${target}`);
|
|
134
130
|
}
|
|
135
|
-
/** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
|
|
136
|
-
function stampableId(id) {
|
|
137
|
-
if (id.startsWith(VIRTUAL_PREFIX))
|
|
138
|
-
return null;
|
|
139
|
-
if (id.includes(NODE_MODULES))
|
|
140
|
-
return null;
|
|
141
|
-
// Strip any query suffix (?worker, ?raw,...) before matching the extension.
|
|
142
|
-
return id.split('?')[0] ?? id;
|
|
143
|
-
}
|
|
144
|
-
function shouldStamp(id) {
|
|
145
|
-
const clean = stampableId(id);
|
|
146
|
-
return clean !== null && JSX_FILE.test(clean);
|
|
147
|
-
}
|
|
148
|
-
/** A `.svelte` single-file component, which needs the Svelte stamper rather than Babel. */
|
|
149
|
-
function shouldStampSvelte(id) {
|
|
150
|
-
const clean = stampableId(id);
|
|
151
|
-
return clean !== null && SVELTE_FILE.test(clean);
|
|
152
|
-
}
|
|
153
|
-
function stamp(code, id) {
|
|
154
|
-
const out = transformSync(code, {
|
|
155
|
-
filename: id,
|
|
156
|
-
plugins: [reticleSource],
|
|
157
|
-
parserOpts: { plugins: ['jsx', 'typescript'] },
|
|
158
|
-
sourceMaps: true,
|
|
159
|
-
configFile: false,
|
|
160
|
-
babelrc: false,
|
|
161
|
-
});
|
|
162
|
-
if (out?.code === undefined || null === out.code)
|
|
163
|
-
return null;
|
|
164
|
-
return {
|
|
165
|
-
code: out.code,
|
|
166
|
-
map: out.map === undefined || null === out.map ? null : JSON.stringify(out.map),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
131
|
/**
|
|
170
132
|
* Read the daemon's auto-provisioned pairing token (~/.reticle/pairing-token, or the
|
|
171
133
|
* RETICLE_PAIRING_TOKEN_DIR override) so the served app can present it. Node-side only — a browser
|
|
@@ -196,60 +158,6 @@ function warnIfTokenMissing(token) {
|
|
|
196
158
|
}
|
|
197
159
|
return token;
|
|
198
160
|
}
|
|
199
|
-
/** Build the `reticle.connect` argument literal — only includes keys the user set. */
|
|
200
|
-
function connectArgs(options) {
|
|
201
|
-
const args = {};
|
|
202
|
-
const port = options.port ?? RETICLE_DEFAULT_PORT;
|
|
203
|
-
if (port !== RETICLE_DEFAULT_PORT)
|
|
204
|
-
args['url'] = bridgeWsUrl(port);
|
|
205
|
-
if (options.session !== undefined)
|
|
206
|
-
args['session'] = options.session;
|
|
207
|
-
if (options.projectId !== undefined)
|
|
208
|
-
args['projectId'] = options.projectId;
|
|
209
|
-
if (options.token !== undefined)
|
|
210
|
-
args['token'] = options.token;
|
|
211
|
-
// Passed as connect ARGUMENTS, not as a `define`. A define substitutes a bare identifier in the
|
|
212
|
-
// source it transforms; the SDK reads these as `globalThis[NAME]`, a dynamic lookup no define can
|
|
213
|
-
// ever reach — so defining them looked right, shipped, and did nothing. Baking them into the
|
|
214
|
-
// generated connect call is a literal in generated source: no bundler subtleties, works the same
|
|
215
|
-
// in dev and in a desktop build.
|
|
216
|
-
if (options.root !== undefined && options.root.length > 0)
|
|
217
|
-
args['root'] = options.root;
|
|
218
|
-
if (options.sdkVersion !== undefined && options.sdkVersion.length > 0) {
|
|
219
|
-
args['sdkVersion'] = options.sdkVersion;
|
|
220
|
-
}
|
|
221
|
-
// A desktop renderer is a production build by construction; without this the SDK's prod backstop
|
|
222
|
-
// refuses to connect and the app is silently uninstrumented.
|
|
223
|
-
if (true === options.desktop)
|
|
224
|
-
args['allowInProduction'] = true;
|
|
225
|
-
// Env wins nothing — it only turns the flag ON, so a config that never set it can still be
|
|
226
|
-
// switched on for one debugging session without editing vite.config and restarting the mental
|
|
227
|
-
// model with it.
|
|
228
|
-
if (true === options.captureNetworkBodies || '1' === process.env['VITE_RETICLE_CAPTURE_BODIES']) {
|
|
229
|
-
args['captureNetworkBodies'] = true;
|
|
230
|
-
}
|
|
231
|
-
// Only the OPT-OUT is announced, never the default. The daemon reads absence as "unknown", so
|
|
232
|
-
// sending `true` here would say nothing it does not already assume — while sending `false` is the
|
|
233
|
-
// one fact a red verdict needs to stop prescribing a plugin this project has deliberately muted.
|
|
234
|
-
if (false === options.sourceMapping)
|
|
235
|
-
args['sourceMapping'] = false;
|
|
236
|
-
// The one option that defaults ON, so the env var and the config flag both DISABLE rather than
|
|
237
|
-
// enable. Emitted only when switched off; the default stays implicit in the SDK.
|
|
238
|
-
if (false === options.captureErrorBodies || '1' === process.env['VITE_RETICLE_NO_ERROR_BODIES']) {
|
|
239
|
-
args['captureErrorBodies'] = false;
|
|
240
|
-
}
|
|
241
|
-
// Same shape, same reason. Off unless asked for, in a config or for one session.
|
|
242
|
-
if (true === options.exposePresenter || '1' === process.env['VITE_RETICLE_EXPOSE_PRESENTER']) {
|
|
243
|
-
args['exposePresenter'] = true;
|
|
244
|
-
}
|
|
245
|
-
// Same shape, same reason: without it an app that cannot be served on localhost has no way to
|
|
246
|
-
// reach the SDK option at all. The pairing token still applies — see the option's docstring.
|
|
247
|
-
if (true === options.allowNonLocalhost ||
|
|
248
|
-
'1' === process.env['VITE_RETICLE_ALLOW_NON_LOCALHOST']) {
|
|
249
|
-
args['allowNonLocalhost'] = true;
|
|
250
|
-
}
|
|
251
|
-
return Object.keys(args).length > 0 ? JSON.stringify(args) : '';
|
|
252
|
-
}
|
|
253
161
|
/** The body of the connect module — real imports, resolved by Vite when the module is served. */
|
|
254
162
|
/**
|
|
255
163
|
* The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
|
|
@@ -333,14 +241,12 @@ export function connectModuleSource(options, devModule = null) {
|
|
|
333
241
|
* import { reticle } from '@reticlehq/vite-plugin';
|
|
334
242
|
* export default defineConfig({ plugins: [react(), reticle()] });
|
|
335
243
|
*
|
|
336
|
-
*
|
|
337
|
-
* bundle
|
|
244
|
+
* The web plugin also builds, replacing the browser SDK with an inert stub so a default
|
|
245
|
+
* production bundle ships no Reticle runtime code at all.
|
|
338
246
|
*
|
|
339
|
-
* `desktop: true`
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
|
|
343
|
-
* a release binary.
|
|
247
|
+
* `desktop: true` keeps that guarantee for a production-mode build. It instruments `serve`, and a
|
|
248
|
+
* build only when it runs in another mode (`vite build --mode development`), which is how a packaged
|
|
249
|
+
* smoke renderer with no dev server still gets its connect(). See isReticleDisabledBuild.
|
|
344
250
|
*/
|
|
345
251
|
/**
|
|
346
252
|
* The daemon's journal directory, as a matcher every chokidar major honours.
|
|
@@ -371,6 +277,10 @@ export function reticle(options = {}) {
|
|
|
371
277
|
let root;
|
|
372
278
|
/** 'serve' | 'build'. The dev check only applies to serve; buildEnd covers the other. */
|
|
373
279
|
let command;
|
|
280
|
+
/** Vite's resolved mode. Decides whether a desktop build may be instrumented at all. */
|
|
281
|
+
let mode;
|
|
282
|
+
/** A build that must carry no Reticle runtime. See isReticleDisabledBuild. */
|
|
283
|
+
const disabledBuild = () => isReticleDisabledBuild(desktop, command, mode);
|
|
374
284
|
/** Vite's resolved `base`. Undefined until configResolved, which is before any HTML is served. */
|
|
375
285
|
let base;
|
|
376
286
|
/** True only when THIS server is Vitest's browser-mode runner — see isVitestBrowserServer. */
|
|
@@ -378,21 +288,38 @@ export function reticle(options = {}) {
|
|
|
378
288
|
const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
|
|
379
289
|
/** Whether connect() actually reached a module — asserted at buildEnd, never assumed. */
|
|
380
290
|
let injected = false;
|
|
291
|
+
/** Files already announced as opted out of stamping: one line each, for the life of the server. */
|
|
292
|
+
const announcedIgnored = new Set();
|
|
381
293
|
/**
|
|
382
294
|
* Whether Vite ever asked us to transform the app's HTML.
|
|
383
295
|
*
|
|
384
|
-
* On the web this is
|
|
385
|
-
*
|
|
386
|
-
* desktop entry-module flag above.
|
|
296
|
+
* On the web this is the usual way the connect script gets in, but not the only one: see
|
|
297
|
+
* connectDelivered below, which the web warning reads together with this.
|
|
387
298
|
*/
|
|
388
299
|
let htmlTransformed = false;
|
|
300
|
+
/**
|
|
301
|
+
* Whether the connect reached the page by a route OTHER than the HTML hook.
|
|
302
|
+
*
|
|
303
|
+
* A framework that renders its own HTML never calls `transformIndexHtml`, and still connects when
|
|
304
|
+
* its client entry imports the module `load` serves (React Router) or calls connect() itself with
|
|
305
|
+
* the token this plugin inlines (SvelteKit's client hook). Counting only the hook told those apps,
|
|
306
|
+
* ten seconds after every page load and while they WERE connected, that they would never connect.
|
|
307
|
+
*/
|
|
308
|
+
let connectDelivered = false;
|
|
309
|
+
/** The last port warning printed, so a disagreement is said once and not on every request. */
|
|
310
|
+
let lastPortWarning;
|
|
389
311
|
/**
|
|
390
312
|
* Resolve port + token at the moment of injection, not at plugin construction. By the time a
|
|
391
313
|
* module is served or built the daemon is up and has written its pairing token; resolving early
|
|
392
314
|
* would bake in `undefined` and the app would fail auth on every connect.
|
|
393
315
|
*/
|
|
394
316
|
const resolveLazy = () => {
|
|
395
|
-
|
|
317
|
+
// Wherever the daemon for this project actually is; the order is argued in discover-port.ts.
|
|
318
|
+
const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, process.cwd());
|
|
319
|
+
if (warning !== undefined && warning !== lastPortWarning) {
|
|
320
|
+
lastPortWarning = warning;
|
|
321
|
+
warn(warning);
|
|
322
|
+
}
|
|
396
323
|
const withPort = port !== undefined ? { ...resolved, port } : resolved;
|
|
397
324
|
const token = withPort.token ?? readPairingToken();
|
|
398
325
|
const withToken = token !== undefined ? { ...withPort, token } : withPort;
|
|
@@ -421,15 +348,12 @@ export function reticle(options = {}) {
|
|
|
421
348
|
desktop,
|
|
422
349
|
inject,
|
|
423
350
|
injected: () => injected,
|
|
424
|
-
|
|
351
|
+
connectDelivered: () => htmlTransformed || connectDelivered,
|
|
352
|
+
connectModule: () => connectModuleUrl(base),
|
|
425
353
|
warn,
|
|
426
354
|
});
|
|
427
355
|
return {
|
|
428
356
|
name: RETICLE_VITE_PLUGIN_NAME,
|
|
429
|
-
// Web: serve-only, so a production bundle can never carry the SDK — gating is the tool's job.
|
|
430
|
-
// Desktop: a packaged renderer IS a production build with no dev server, so the plugin must also
|
|
431
|
-
// run for `vite build` or the shipped app has no connect() at all.
|
|
432
|
-
...(true === options.desktop ? {} : { apply: 'serve' }),
|
|
433
357
|
enforce: 'pre',
|
|
434
358
|
/**
|
|
435
359
|
* Declare the SDK itself and the optimizer cache fingerprint.
|
|
@@ -511,6 +435,13 @@ export function reticle(options = {}) {
|
|
|
511
435
|
};
|
|
512
436
|
},
|
|
513
437
|
transform(code, id) {
|
|
438
|
+
if (disabledBuild())
|
|
439
|
+
return null;
|
|
440
|
+
// A hand-written connect in app code reads the token this plugin inlines; that is the
|
|
441
|
+
// connect reaching the page without the HTML hook. See connectDelivered.
|
|
442
|
+
if (!connectDelivered && code.includes(RETICLE_TOKEN_GLOBAL) && !id.includes(NODE_MODULES)) {
|
|
443
|
+
connectDelivered = true;
|
|
444
|
+
}
|
|
514
445
|
// Desktop injection: prepend connect() to the HTML's own entry module. It is a REAL module, so
|
|
515
446
|
// its bare `@reticlehq/react` import resolves through the normal pipeline in both dev and
|
|
516
447
|
// build — which a virtual <script src> only ever did in dev.
|
|
@@ -522,6 +453,16 @@ export function reticle(options = {}) {
|
|
|
522
453
|
}
|
|
523
454
|
if (!sourceMapping)
|
|
524
455
|
return null;
|
|
456
|
+
// Decided HERE, ahead of both stampers, so the Svelte path honours the marker too and so the
|
|
457
|
+
// opt-out is announced once regardless of which stamper would have run. The Babel plugin reads
|
|
458
|
+
// the same marker on its own for the callers that reach it without Vite (#853).
|
|
459
|
+
if (optsOutOfStamping(code, id)) {
|
|
460
|
+
if (!announcedIgnored.has(id)) {
|
|
461
|
+
announcedIgnored.add(id);
|
|
462
|
+
warn(ignoredFileNotice(id));
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
525
466
|
// `.svelte` runs on the RAW component source, which is only still markup because this plugin
|
|
526
467
|
// declares `enforce: 'pre'` and therefore transforms before @sveltejs/vite-plugin-svelte. No
|
|
527
468
|
// map: the insertions are within a line and never move one, and a wrong map is worse than none.
|
|
@@ -534,6 +475,9 @@ export function reticle(options = {}) {
|
|
|
534
475
|
return stamp(code, id);
|
|
535
476
|
},
|
|
536
477
|
resolveId(id, importer) {
|
|
478
|
+
if (disabledBuild() && id === RETICLE_SENSOR) {
|
|
479
|
+
return RETICLE_DISABLED_STUB;
|
|
480
|
+
}
|
|
537
481
|
// Desktop: remember the module the HTML points at, so `transform` can prepend connect() into
|
|
538
482
|
// it. A packaged build has no dev server, so the serve-time trick below — a <script src> at a
|
|
539
483
|
// virtual URL — would emit a tag pointing at a file that does not exist. That shipped an app
|
|
@@ -549,15 +493,20 @@ export function reticle(options = {}) {
|
|
|
549
493
|
return inject && id === RETICLE_CONNECT_MODULE ? RETICLE_CONNECT_MODULE : null;
|
|
550
494
|
},
|
|
551
495
|
load(id) {
|
|
496
|
+
if (disabledBuild() && id === RETICLE_DISABLED_STUB) {
|
|
497
|
+
return RETICLE_DISABLED_STUB_CODE;
|
|
498
|
+
}
|
|
552
499
|
if (!inject || id !== RETICLE_CONNECT_MODULE)
|
|
553
500
|
return null;
|
|
554
501
|
const source = currentConnectSource();
|
|
555
502
|
lastServedConnectSource = source;
|
|
503
|
+
connectDelivered = true;
|
|
556
504
|
return source;
|
|
557
505
|
},
|
|
558
506
|
configResolved(config) {
|
|
559
507
|
root = config.root;
|
|
560
508
|
command = config.command;
|
|
509
|
+
mode = config.mode;
|
|
561
510
|
base = config.base;
|
|
562
511
|
vitestBrowser = isVitestBrowserServer(config);
|
|
563
512
|
},
|
|
@@ -582,8 +531,6 @@ export function reticle(options = {}) {
|
|
|
582
531
|
* appearing IS a change), and makes the module inert once it has settled.
|
|
583
532
|
*/
|
|
584
533
|
configureServer(server) {
|
|
585
|
-
if (!inject)
|
|
586
|
-
return;
|
|
587
534
|
// The web post-condition is armed by the first DOCUMENT REQUEST, in the middleware below.
|
|
588
535
|
//
|
|
589
536
|
// Not from `transformIndexHtml`, because a framework that renders its own HTML never calls it
|
|
@@ -635,6 +582,12 @@ export function reticle(options = {}) {
|
|
|
635
582
|
else {
|
|
636
583
|
announce();
|
|
637
584
|
}
|
|
585
|
+
// Everything below serves or watches the injected connect, so `inject: false` stops HERE and
|
|
586
|
+
// not at the top. SvelteKit and TanStack Start are written `inject: false` because they connect
|
|
587
|
+
// themselves, and returning before the announcement made the dev server `init` started for
|
|
588
|
+
// them invisible to everything that asks which dev servers are running.
|
|
589
|
+
if (!inject)
|
|
590
|
+
return;
|
|
638
591
|
server.middlewares.use((req, _res, next) => {
|
|
639
592
|
// One middleware, two observations. A second `use()` would work equally well in Vite and
|
|
640
593
|
// is the obvious way to write this, but it makes the ORDER of registration load-bearing for
|
|
@@ -664,7 +617,8 @@ export function reticle(options = {}) {
|
|
|
664
617
|
* build that could not instrument must fail loudly instead of shipping a binary that lies.
|
|
665
618
|
*/
|
|
666
619
|
buildEnd() {
|
|
667
|
-
|
|
620
|
+
// A production build injects nothing on purpose; see isReticleDisabledBuild.
|
|
621
|
+
if (!desktop || !inject || injected || disabledBuild())
|
|
668
622
|
return;
|
|
669
623
|
throw new Error(notInjectedMessage());
|
|
670
624
|
},
|
|
@@ -672,6 +626,8 @@ export function reticle(options = {}) {
|
|
|
672
626
|
checkHtmlHookForTest: watch.checkHtmlHookRan,
|
|
673
627
|
injectionWatchForTest: watch,
|
|
674
628
|
transformIndexHtml() {
|
|
629
|
+
if (disabledBuild())
|
|
630
|
+
return [];
|
|
675
631
|
htmlTransformed = true;
|
|
676
632
|
if (desktop && inject && 'serve' === command)
|
|
677
633
|
watch.armDesktopCheck();
|
|
@@ -42,8 +42,12 @@ export declare const unconfirmedInjectionMessage: () => string;
|
|
|
42
42
|
* `reticle status`. Known examples: SvelteKit, Nuxt, Astro, React Router in framework mode and
|
|
43
43
|
* TanStack Start. Each was found the same way — a user waiting on a session that could never
|
|
44
44
|
* arrive — so the plain-language cause comes first and the fix comes second.
|
|
45
|
+
*
|
|
46
|
+
* The fix it names keeps `inject` ON. It used to say "pass `inject: false`", and on a React Router
|
|
47
|
+
* app, whose client entry imports the connect module this plugin serves, following that advice
|
|
48
|
+
* switched the module off and broke a working connect.
|
|
45
49
|
*/
|
|
46
|
-
export declare const htmlHookNeverRanMessage: () => string;
|
|
50
|
+
export declare const htmlHookNeverRanMessage: (connectModule: string) => string;
|
|
47
51
|
/**
|
|
48
52
|
* The state behind those three messages, and the rules for reaching each one.
|
|
49
53
|
*
|
|
@@ -58,8 +62,13 @@ export interface InjectionWatchDeps {
|
|
|
58
62
|
readonly inject: boolean;
|
|
59
63
|
/** Whether the desktop entry module was transformed. Read late: it flips during the session. */
|
|
60
64
|
readonly injected: () => boolean;
|
|
61
|
-
/**
|
|
62
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Whether connect() reached the page by ANY route: the HTML hook, the served connect module, or a
|
|
67
|
+
* hand-written connect. Read late, for the same reason.
|
|
68
|
+
*/
|
|
69
|
+
readonly connectDelivered: () => boolean;
|
|
70
|
+
/** The URL the connect module is served at (base-aware, so read late), for the web message. */
|
|
71
|
+
readonly connectModule: () => string;
|
|
63
72
|
readonly warn: (message: string) => void;
|
|
64
73
|
/** Injected so a test does not wait ten real seconds. Defaults to the module-level timer. */
|
|
65
74
|
readonly schedule?: (run: () => void, ms: number) => void;
|
|
@@ -51,13 +51,18 @@ export const unconfirmedInjectionMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}]
|
|
|
51
51
|
* `reticle status`. Known examples: SvelteKit, Nuxt, Astro, React Router in framework mode and
|
|
52
52
|
* TanStack Start. Each was found the same way — a user waiting on a session that could never
|
|
53
53
|
* arrive — so the plain-language cause comes first and the fix comes second.
|
|
54
|
+
*
|
|
55
|
+
* The fix it names keeps `inject` ON. It used to say "pass `inject: false`", and on a React Router
|
|
56
|
+
* app, whose client entry imports the connect module this plugin serves, following that advice
|
|
57
|
+
* switched the module off and broke a working connect.
|
|
54
58
|
*/
|
|
55
|
-
export const htmlHookNeverRanMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] this app will never connect: the dev server never asked this ` +
|
|
56
|
-
'plugin to transform any HTML, so reticle.connect()
|
|
57
|
-
'means your framework renders its own HTML instead of serving
|
|
58
|
-
'Astro, React Router (framework mode) and TanStack Start all do.
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
export const htmlHookNeverRanMessage = (connectModule) => `[${RETICLE_VITE_PLUGIN_NAME}] this app will never connect: the dev server never asked this ` +
|
|
60
|
+
'plugin to transform any HTML and nothing loaded its connect module, so reticle.connect() never ' +
|
|
61
|
+
'reached the page. That usually means your framework renders its own HTML instead of serving ' +
|
|
62
|
+
'index.html — SvelteKit, Nuxt, Astro, React Router (framework mode) and TanStack Start all do. ' +
|
|
63
|
+
`Fix: in dev, dynamically import '${connectModule}' from your client entry file — this plugin ` +
|
|
64
|
+
"serves that module, so leave `inject` on — or import '@reticlehq/browser' there " +
|
|
65
|
+
'and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself.';
|
|
61
66
|
const defaultSchedule = (run, ms) => {
|
|
62
67
|
const timer = setTimeout(run, ms);
|
|
63
68
|
// Never hold a dev server open on account of a warning it may not even need to print.
|
|
@@ -85,12 +90,12 @@ export function createInjectionWatch(deps) {
|
|
|
85
90
|
* `warn(htmlHookNeverRanMessage())` needs to see it.
|
|
86
91
|
*/
|
|
87
92
|
const checkHtmlHookRan = () => {
|
|
88
|
-
if (deps.desktop || !deps.inject || deps.
|
|
93
|
+
if (deps.desktop || !deps.inject || deps.connectDelivered())
|
|
89
94
|
return;
|
|
90
95
|
// Nobody has opened the app. That says nothing about whether it can connect.
|
|
91
96
|
if (!htmlRequested)
|
|
92
97
|
return;
|
|
93
|
-
deps.warn(htmlHookNeverRanMessage());
|
|
98
|
+
deps.warn(htmlHookNeverRanMessage(deps.connectModule()));
|
|
94
99
|
};
|
|
95
100
|
/**
|
|
96
101
|
* Note a document request, and start the clock from THERE.
|
package/dist/project-id.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ type ReadFile = (path: string) => string;
|
|
|
26
26
|
* through `init` — it is not a second opinion, and where the two disagree the config wins.
|
|
27
27
|
*/
|
|
28
28
|
export declare function readConfiguredProjectId(startDir: string, readFile?: ReadFile): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Read the daemon port `reticle init --port` recorded in the nearest `.reticle.json`, or undefined.
|
|
31
|
+
*
|
|
32
|
+
* The CLI already obeys this field, so a plugin that ignored it put the two halves on different
|
|
33
|
+
* daemons: the page connected to the default port — on a machine running two projects, the other
|
|
34
|
+
* project's daemon — and the bridge refused it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readConfiguredPort(startDir: string, readFile?: ReadFile): number | undefined;
|
|
29
37
|
/**
|
|
30
38
|
* Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
|
|
31
39
|
* option, then the id `reticle init` recorded in `.reticle.json`, then derivation.
|
package/dist/project-id.js
CHANGED
|
@@ -45,18 +45,24 @@ const MAX_CONFIG_SEARCH_DEPTH = 6;
|
|
|
45
45
|
* costs a less specific name rather than the wrong app's identity. Left as it was.
|
|
46
46
|
*/
|
|
47
47
|
const MAX_PACKAGE_SEARCH_DEPTH = 50;
|
|
48
|
+
const nonEmptyString = (value) => 'string' === typeof value && value.length > 0 ? value : undefined;
|
|
49
|
+
const MAX_TCP_PORT = 65_535;
|
|
50
|
+
const tcpPort = (value) => Number.isInteger(value) && value > 0 && value <= MAX_TCP_PORT
|
|
51
|
+
? value
|
|
52
|
+
: undefined;
|
|
48
53
|
/**
|
|
49
|
-
* Walk up from `startDir` looking for `basename`, and return the first
|
|
50
|
-
*
|
|
54
|
+
* Walk up from `startDir` looking for `basename`, and return the first value of `field` that
|
|
55
|
+
* `accept` takes. Unreadable and unparseable files are skipped rather than fatal: a dev server must
|
|
56
|
+
* start.
|
|
51
57
|
*/
|
|
52
|
-
function readNearestField(startDir, basename, field, readFile, maxDepth) {
|
|
58
|
+
function readNearestField(startDir, basename, field, readFile, maxDepth, accept) {
|
|
53
59
|
let dir = startDir;
|
|
54
60
|
for (let depth = 0; depth <= maxDepth; depth++) {
|
|
55
61
|
try {
|
|
56
62
|
const parsed = JSON.parse(readFile(join(dir, basename)));
|
|
57
63
|
if ('object' === typeof parsed && parsed !== null) {
|
|
58
|
-
const value = parsed[field];
|
|
59
|
-
if (
|
|
64
|
+
const value = accept(parsed[field]);
|
|
65
|
+
if (value !== undefined)
|
|
60
66
|
return value;
|
|
61
67
|
}
|
|
62
68
|
}
|
|
@@ -72,7 +78,7 @@ function readNearestField(startDir, basename, field, readFile, maxDepth) {
|
|
|
72
78
|
}
|
|
73
79
|
/** Read the `name` from the nearest package.json at or above `startDir`, or undefined if none. */
|
|
74
80
|
function readNearestPackageName(startDir, readFile = readFileOrThrow) {
|
|
75
|
-
return readNearestField(startDir, 'package.json', 'name', readFile, MAX_PACKAGE_SEARCH_DEPTH);
|
|
81
|
+
return readNearestField(startDir, 'package.json', 'name', readFile, MAX_PACKAGE_SEARCH_DEPTH, nonEmptyString);
|
|
76
82
|
}
|
|
77
83
|
/**
|
|
78
84
|
* Read the `projectId` `reticle init` recorded in the nearest `.reticle.json`, or undefined.
|
|
@@ -81,7 +87,17 @@ function readNearestPackageName(startDir, readFile = readFileOrThrow) {
|
|
|
81
87
|
* through `init` — it is not a second opinion, and where the two disagree the config wins.
|
|
82
88
|
*/
|
|
83
89
|
export function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
|
|
84
|
-
return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'projectId', readFile, MAX_CONFIG_SEARCH_DEPTH);
|
|
90
|
+
return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'projectId', readFile, MAX_CONFIG_SEARCH_DEPTH, nonEmptyString);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read the daemon port `reticle init --port` recorded in the nearest `.reticle.json`, or undefined.
|
|
94
|
+
*
|
|
95
|
+
* The CLI already obeys this field, so a plugin that ignored it put the two halves on different
|
|
96
|
+
* daemons: the page connected to the default port — on a machine running two projects, the other
|
|
97
|
+
* project's daemon — and the bridge refused it.
|
|
98
|
+
*/
|
|
99
|
+
export function readConfiguredPort(startDir, readFile = readFileOrThrow) {
|
|
100
|
+
return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'port', readFile, MAX_CONFIG_SEARCH_DEPTH, tcpPort);
|
|
85
101
|
}
|
|
86
102
|
/**
|
|
87
103
|
* Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
|