@reticlehq/vite-plugin 3.3.0 → 3.5.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 +4 -4
- package/dist/disabled-browser-stub.d.ts +13 -1
- package/dist/disabled-browser-stub.js +16 -1
- package/dist/discover-port.d.ts +32 -1
- package/dist/discover-port.js +53 -5
- package/dist/index.cjs +99 -24
- package/dist/index.d.cts +11 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.js +50 -20
- 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 +1 -0
- package/dist/stamping.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
One-line Vite integration for [Reticle](https://github.com/reticlehq/reticle). The plugin does the whole dev-time wiring for you:
|
|
4
4
|
|
|
5
|
-
- **Source mapping**: stamps `data-reticle-source="file:line:col"` on JSX host elements (via [`@reticlehq/babel-plugin`](https://www.npmjs.com/package/@reticlehq/babel-plugin)) so `
|
|
5
|
+
- **Source mapping**: stamps `data-reticle-source="file:line:col"` on JSX host elements (via [`@reticlehq/babel-plugin`](https://www.npmjs.com/package/@reticlehq/babel-plugin)) so `reticle_look { action: "element" }` can report the component's source file. Needed on React 19.
|
|
6
6
|
- **Auto-connect**: injects a dev-only `install(); reticle.connect()` into your entry module, so you never touch the entry file yourself.
|
|
7
7
|
- **Svelte source mapping**: the same stamp on Svelte markup, applied before `@sveltejs/vite-plugin-svelte` compiles it.
|
|
8
8
|
- **Dependency pre-bundling**: declares the SDK's CJS runtime deps in `optimizeDeps` so the SDK loads on linked and monorepo setups.
|
|
@@ -45,9 +45,9 @@ reticle({
|
|
|
45
45
|
sdkVersion, // installed SDK version, so a skewed pair can name itself
|
|
46
46
|
sourceMapping, // default true; stamp data-reticle-source (harmless on React <=18)
|
|
47
47
|
inject, // default true; auto-inject reticle.connect()
|
|
48
|
-
captureNetworkBodies, // default false; record request/response bodies on
|
|
48
|
+
captureNetworkBodies, // default false; record request/response bodies on reticle_observe { action: "network" }
|
|
49
49
|
allowNonLocalhost, // default false; allow a page/bridge that is not on localhost (needs a token)
|
|
50
|
-
desktop, // default false;
|
|
50
|
+
desktop, // default false; for an Electron/Tauri renderer (see below)
|
|
51
51
|
onWarn, // where a diagnostic goes; defaults to the console
|
|
52
52
|
});
|
|
53
53
|
```
|
|
@@ -56,7 +56,7 @@ reticle({
|
|
|
56
56
|
|
|
57
57
|
`allowNonLocalhost` is for a dev server that cannot be served on localhost — a host-based multi-tenant frontend, or an app with cookie-scoped auth on a custom dev hostname. It is **not sufficient on its own**: the SDK also requires a pairing token outside localhost, and refuses with "a pairing token is required outside localhost" when it is missing. The plugin supplies one automatically from the daemon's `~/.reticle/pairing-token`, so a running daemon is normally all it takes — pass `token` yourself only when that file is unreachable. A bridge that is itself non-local must also use `wss://`. Settable as `VITE_RETICLE_ALLOW_NON_LOCALHOST=1` for a single session.
|
|
58
58
|
|
|
59
|
-
`desktop: true`
|
|
59
|
+
`desktop: true` injects `connect()` into the renderer's entry module and calls it with `allowInProduction`, because a desktop renderer reports `NODE_ENV=production`. It instruments the dev server, and a `vite build` only when that build runs in a non-production mode (`vite build --mode development`), for a packaged renderer you drive with no dev server. A production-mode build, which is what `vite build` and `electron-vite build` do by default, ships no Reticle code at all, exactly like a web build.
|
|
60
60
|
|
|
61
61
|
### Opting one file out of source stamping
|
|
62
62
|
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
export declare const RETICLE_DISABLED_STUB = "\0reticle-disabled-stub";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Whether this is a build that must ship no Reticle runtime at all: the SDK swapped for the stub
|
|
4
|
+
* below, nothing injected.
|
|
5
|
+
*
|
|
6
|
+
* Every production build, desktop included. `desktop: true` used to exempt the whole build, and it
|
|
7
|
+
* is what `reticle init` writes for electron-vite, so a RELEASE renderer carried the full SDK and
|
|
8
|
+
* dialled `ws://localhost:<port>/reticle`: `electron-vite preview` of a production build connected
|
|
9
|
+
* to the daemon. A release binary gets the same guarantee a web bundle does. A packaged desktop
|
|
10
|
+
* build that SHOULD be instrumented (a smoke app driven headless) says so the way Vite already
|
|
11
|
+
* provides, by building in another mode: `vite build --mode development`. An unknown mode counts as
|
|
12
|
+
* production, because the cost of guessing wrong the other way is an instrumented release.
|
|
13
|
+
*/
|
|
14
|
+
export declare const isReticleDisabledBuild: (desktop: boolean, command: string | undefined, mode: string | undefined) => boolean;
|
|
3
15
|
export declare const RETICLE_DISABLED_STUB_CODE = "\nexport class Reticle {\n connect() {}\n signal() {}\n state() {}\n renderCommit() {}\n describe() {}\n endSession() {}\n observeHotUpdates() {}\n disconnect() {}\n\n get connected() {\n return false;\n }\n}\n\nexport const reticle = new Reticle();\n\nexport const SESSION_AUTO = 'auto';\n\nexport function setIgnoreSelectors() {}\n\nexport function registerAdapter() {}\nexport function identifyComponent() {\n return null;\n}\nexport function readComponentState() {\n return undefined;\n}\nexport function elementHasHoverHandlers() {\n return false;\n}\nexport function adapterNames() {\n return [];\n}\n\nexport function registerStore() {}\nexport function unregisterStore() {}\nexport function storeNames() {\n return [];\n}\nexport function readStores() {\n return {};\n}\nexport function readStoresWithTruncation() {\n return { stores: {} };\n}\nexport function sourceOwner() {\n return undefined;\n}\nexport function markAdapterSource() {}\n\nexport function tanstackQueryStore() {\n return undefined;\n}\nexport function jotaiStore() {\n return undefined;\n}\nexport function xstateStore() {\n return undefined;\n}\nexport function valtioStore() {\n return undefined;\n}\nexport function mobxStore() {\n return undefined;\n}\nexport function recoilStore() {\n return undefined;\n}\nexport function svelteStore() {\n return undefined;\n}\nexport function piniaStore() {\n return undefined;\n}\nexport function pushStore() {\n return undefined;\n}\n\nexport function registerCapabilities() {}\nexport function getCapabilities() {\n return {};\n}\nexport function hasCapabilities() {\n return false;\n}\nexport function setCapabilitiesListener() {}\n\nexport function createReticleEmitter() {\n return {\n signal() {},\n state() {},\n };\n}\n\nexport function commitAndSignal() {}\nexport function registerReticleDomain() {}\n\nexport function buildSnapshot() {\n return {};\n}\n\nexport function inspectChart() {\n return {};\n}\nexport function canvasChartData() {\n return {};\n}\n\nexport function matchQuery() {\n return false;\n}\nexport function runQuery() {\n return undefined;\n}\n\nexport function executeAction() {\n return undefined;\n}\nexport function executeSequence() {\n return undefined;\n}\n\nexport function describe() {\n return undefined;\n}\nexport function getRole() {\n return undefined;\n}\nexport function getAccessibleName() {\n return undefined;\n}\nexport function getStates() {\n return {};\n}\nexport function isVisible() {\n return false;\n}\n\nexport class RefRegistry {\n get size() {\n return 0;\n }\n\n markEdited() {}\n\n mintedBeforeLastEdit() {\n return false;\n }\n\n refFor() {\n return undefined;\n }\n\n resolve() {\n return null;\n }\n}\n\nexport const refs = new RefRegistry();\n\nexport class Annotator {\n constructor() {}\n\n get active() {\n return false;\n }\n\n get markCount() {\n return 0;\n }\n\n mount() {}\n unmount() {}\n}\n\nexport function installAnnotator() {}\n\nexport function resolveMarkAnchor() {\n return undefined;\n}\n";
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
export const RETICLE_DISABLED_STUB = '\0reticle-disabled-stub';
|
|
2
|
-
|
|
2
|
+
const BUILD_COMMAND = 'build';
|
|
3
|
+
/** The mode `vite build` and `electron-vite build` use unless the command line says otherwise. */
|
|
4
|
+
const PRODUCTION_MODE = 'production';
|
|
5
|
+
/**
|
|
6
|
+
* Whether this is a build that must ship no Reticle runtime at all: the SDK swapped for the stub
|
|
7
|
+
* below, nothing injected.
|
|
8
|
+
*
|
|
9
|
+
* Every production build, desktop included. `desktop: true` used to exempt the whole build, and it
|
|
10
|
+
* is what `reticle init` writes for electron-vite, so a RELEASE renderer carried the full SDK and
|
|
11
|
+
* dialled `ws://localhost:<port>/reticle`: `electron-vite preview` of a production build connected
|
|
12
|
+
* to the daemon. A release binary gets the same guarantee a web bundle does. A packaged desktop
|
|
13
|
+
* build that SHOULD be instrumented (a smoke app driven headless) says so the way Vite already
|
|
14
|
+
* provides, by building in another mode: `vite build --mode development`. An unknown mode counts as
|
|
15
|
+
* production, because the cost of guessing wrong the other way is an instrumented release.
|
|
16
|
+
*/
|
|
17
|
+
export const isReticleDisabledBuild = (desktop, command, mode) => BUILD_COMMAND === command && (!desktop || mode === undefined || PRODUCTION_MODE === mode);
|
|
3
18
|
export const RETICLE_DISABLED_STUB_CODE = `
|
|
4
19
|
export class Reticle {
|
|
5
20
|
connect() {}
|
package/dist/discover-port.d.ts
CHANGED
|
@@ -3,4 +3,35 @@
|
|
|
3
3
|
* to the default port — never auto-connects to a mismatched daemon). `home` and `alive` are injectable
|
|
4
4
|
* so the selection is unit-tested without a real ~/.reticle or real processes.
|
|
5
5
|
*/
|
|
6
|
-
export declare function discoverDaemonPort(projectId: string | undefined, home?: string, alive?: (pid: number) => boolean
|
|
6
|
+
export declare function discoverDaemonPort(projectId: string | undefined, home?: string, alive?: (pid: number) => boolean,
|
|
7
|
+
/** `port` in `.reticle.json`: wins whenever a live daemon is registered on it — see pickDaemonPort. */
|
|
8
|
+
configured?: number): number | undefined;
|
|
9
|
+
/** The three places a daemon port can come from, as the plugin sees them at connect time. */
|
|
10
|
+
export interface DaemonPortSources {
|
|
11
|
+
/** A live daemon registered in ~/.reticle for THIS project. */
|
|
12
|
+
readonly discovered: number | undefined;
|
|
13
|
+
/** `port` in the nearest `.reticle.json`: the value the daemon and the CLI follow. */
|
|
14
|
+
readonly configured: number | undefined;
|
|
15
|
+
/** The literal `reticle({ port })`, usually written once by `reticle init`. */
|
|
16
|
+
readonly explicit: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface DaemonPortChoice {
|
|
19
|
+
/** Where to dial, or undefined for the default port. */
|
|
20
|
+
readonly port: number | undefined;
|
|
21
|
+
/** Set only when the option and `.reticle.json` disagree: one line naming both and the winner. */
|
|
22
|
+
readonly warning: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The one rule for which port the page dials: wherever the daemon for this project actually is.
|
|
26
|
+
*
|
|
27
|
+
* A live registered daemon first, because it is a fact rather than a statement of intent — and when
|
|
28
|
+
* one is live on the configured port, that one (see pickDaemonPort: a moved port left the old daemon
|
|
29
|
+
* alive, and discovery used to pick it). Then
|
|
30
|
+
* `.reticle.json`, because that is what the daemon and the CLI read. The option used to win, and a
|
|
31
|
+
* user who edited the file moved the daemon while the page kept dialling the literal `init` had
|
|
32
|
+
* written into vite.config: "never dialled the bridge", with nothing saying why. The option counts
|
|
33
|
+
* only when nothing else says anything, and undefined leaves the default to the connect.
|
|
34
|
+
*/
|
|
35
|
+
export declare function chooseDaemonPort(sources: DaemonPortSources): DaemonPortChoice;
|
|
36
|
+
/** {@link chooseDaemonPort} over the real registry and the real `.reticle.json` above `cwd`. */
|
|
37
|
+
export declare function resolveDaemonPort(explicit: number | undefined, projectId: string | undefined, cwd: string, env?: NodeJS.ProcessEnv): DaemonPortChoice;
|
package/dist/discover-port.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build-time daemon discovery
|
|
3
|
-
* by reading the registry entries the daemon drops in ~/.reticle
|
|
2
|
+
* Build-time daemon discovery, and the ONE place the plugin decides which port the page dials. Finds
|
|
3
|
+
* the daemon serving THIS project by reading the registry entries the daemon drops in ~/.reticle
|
|
4
|
+
* (daemon-<port>.json). Node-only and
|
|
4
5
|
* runs at dev-server request time (the daemon is up by then). The tricky selection rule — match by
|
|
5
6
|
* projectId, drop dead daemons — is the pure `pickDaemonPort` in core; this file is just the fs plumbing.
|
|
6
7
|
*/
|
|
7
8
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
8
9
|
import { join } from 'node:path';
|
|
9
|
-
import { daemonRegistryPort, DaemonRegistryEntrySchema, pickDaemonPort, } from '@reticlehq/core';
|
|
10
|
+
import { daemonRegistryPort, DaemonRegistryEntrySchema, pickDaemonPort, ReticleEnv, } from '@reticlehq/core';
|
|
10
11
|
import { stateHome } from './state-home.js';
|
|
12
|
+
import { readConfiguredPort } from './project-id.js';
|
|
13
|
+
import { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
14
|
+
const MAX_TCP_PORT = 65_535;
|
|
11
15
|
/** process.kill(pid, 0) throws iff the process is gone — the same liveness probe the daemon uses. */
|
|
12
16
|
function isAlive(pid) {
|
|
13
17
|
try {
|
|
@@ -23,7 +27,9 @@ function isAlive(pid) {
|
|
|
23
27
|
* to the default port — never auto-connects to a mismatched daemon). `home` and `alive` are injectable
|
|
24
28
|
* so the selection is unit-tested without a real ~/.reticle or real processes.
|
|
25
29
|
*/
|
|
26
|
-
export function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive
|
|
30
|
+
export function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive,
|
|
31
|
+
/** `port` in `.reticle.json`: wins whenever a live daemon is registered on it — see pickDaemonPort. */
|
|
32
|
+
configured) {
|
|
27
33
|
const entries = [];
|
|
28
34
|
let files;
|
|
29
35
|
try {
|
|
@@ -44,5 +50,47 @@ export function discoverDaemonPort(projectId, home = stateHome(), alive = isAliv
|
|
|
44
50
|
// corrupt/unreadable entry — skip
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
|
-
return pickDaemonPort(entries, projectId, alive) ?? undefined;
|
|
53
|
+
return pickDaemonPort(entries, projectId, alive, configured) ?? undefined;
|
|
54
|
+
}
|
|
55
|
+
const portConflictWarning = (explicit, configured, used) => `[${RETICLE_VITE_PLUGIN_NAME}] reticle({ port: ${String(explicit)} }) and "port": ` +
|
|
56
|
+
`${String(configured)} in .reticle.json disagree; connecting to ${String(used)}. The daemon and ` +
|
|
57
|
+
'the CLI follow .reticle.json, so drop the port option from reticle() or make the two match.';
|
|
58
|
+
/**
|
|
59
|
+
* The one rule for which port the page dials: wherever the daemon for this project actually is.
|
|
60
|
+
*
|
|
61
|
+
* A live registered daemon first, because it is a fact rather than a statement of intent — and when
|
|
62
|
+
* one is live on the configured port, that one (see pickDaemonPort: a moved port left the old daemon
|
|
63
|
+
* alive, and discovery used to pick it). Then
|
|
64
|
+
* `.reticle.json`, because that is what the daemon and the CLI read. The option used to win, and a
|
|
65
|
+
* user who edited the file moved the daemon while the page kept dialling the literal `init` had
|
|
66
|
+
* written into vite.config: "never dialled the bridge", with nothing saying why. The option counts
|
|
67
|
+
* only when nothing else says anything, and undefined leaves the default to the connect.
|
|
68
|
+
*/
|
|
69
|
+
export function chooseDaemonPort(sources) {
|
|
70
|
+
const { discovered, configured, explicit } = sources;
|
|
71
|
+
const port = discovered ?? configured ?? explicit;
|
|
72
|
+
if (explicit === undefined || configured === undefined || port === undefined) {
|
|
73
|
+
return { port, warning: undefined };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
port,
|
|
77
|
+
warning: explicit === configured ? undefined : portConflictWarning(explicit, configured, port),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** {@link chooseDaemonPort} over the real registry and the real `.reticle.json` above `cwd`. */
|
|
81
|
+
export function resolveDaemonPort(explicit, projectId, cwd, env = process.env) {
|
|
82
|
+
// The CLI treats RETICLE_PORT as an override across projects. A page must follow the same
|
|
83
|
+
// daemon even when an older instance is still registered or init wrote a different port.
|
|
84
|
+
const raw = env[ReticleEnv.PORT];
|
|
85
|
+
if (raw !== undefined && /^\d+$/.test(raw)) {
|
|
86
|
+
const port = Number(raw);
|
|
87
|
+
if (0 < port && port <= MAX_TCP_PORT)
|
|
88
|
+
return { port, warning: undefined };
|
|
89
|
+
}
|
|
90
|
+
const configured = readConfiguredPort(cwd);
|
|
91
|
+
return chooseDaemonPort({
|
|
92
|
+
discovered: discoverDaemonPort(projectId, stateHome(env), isAlive, configured),
|
|
93
|
+
configured,
|
|
94
|
+
explicit,
|
|
95
|
+
});
|
|
48
96
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -4359,7 +4359,14 @@ var ReticleEnv = {
|
|
|
4359
4359
|
API_KEY: "RETICLE_API_KEY",
|
|
4360
4360
|
/** The name this key had until 2026-09. Still honoured; see `apiKeyFrom()`. */
|
|
4361
4361
|
CLOUD_KEY: "RETICLE_CLOUD_KEY",
|
|
4362
|
-
|
|
4362
|
+
/** The platform host. Read through `cloudUrlFrom()`, which also accepts `URL`. */
|
|
4363
|
+
CLOUD_URL: "RETICLE_CLOUD_URL",
|
|
4364
|
+
/** The short name for `CLOUD_URL`, which people type first. `CLOUD_URL` wins when both are set. */
|
|
4365
|
+
URL: "RETICLE_URL"
|
|
4366
|
+
};
|
|
4367
|
+
var SYNC_BATCH_LIMITS = {
|
|
4368
|
+
MAX_RUNS: 50,
|
|
4369
|
+
MAX_BYTES: 4 * 1024 * 1024
|
|
4363
4370
|
};
|
|
4364
4371
|
var TRANSPORT_LIMITS = {
|
|
4365
4372
|
MAX_MESSAGE_BYTES: 1024 * 1024,
|
|
@@ -4489,6 +4496,8 @@ var ReticleDir = {
|
|
|
4489
4496
|
AMBIENT_FILE: "ambient.json",
|
|
4490
4497
|
/** per-flow flake ledger — replay outcomes that decide intermittent-failure quarantine. */
|
|
4491
4498
|
FLAKE_FILE: "flake.json",
|
|
4499
|
+
/** The app-wide coverage ledger — see server features/exhaust/ledger.ts. */
|
|
4500
|
+
COVERAGE_FILE: "coverage.json",
|
|
4492
4501
|
/**
|
|
4493
4502
|
* the project's cloud binding — .reticle/cloud.json, written by `reticle link`. Git-checked and
|
|
4494
4503
|
* non-secret: the project id, the API origin, and where its dashboard lives. The KEY lives in
|
|
@@ -4595,7 +4604,10 @@ var DaemonRegistryEntrySchema = external_exports.object({
|
|
|
4595
4604
|
projectId: external_exports.string().optional(),
|
|
4596
4605
|
startedAt: external_exports.number()
|
|
4597
4606
|
});
|
|
4598
|
-
function pickDaemonPort(entries, projectId, isAlive2) {
|
|
4607
|
+
function pickDaemonPort(entries, projectId, isAlive2, configuredPort) {
|
|
4608
|
+
if (void 0 !== configuredPort && entries.some((e) => e.port === configuredPort && isAlive2(e.pid) && (e.projectId === void 0 || projectId === void 0 || e.projectId === projectId))) {
|
|
4609
|
+
return configuredPort;
|
|
4610
|
+
}
|
|
4599
4611
|
if (projectId === void 0 || 0 === projectId.length)
|
|
4600
4612
|
return null;
|
|
4601
4613
|
const matches = entries.filter((e) => e.projectId === projectId && isAlive2(e.pid)).map((e) => e.port).sort((a, b) => a - b);
|
|
@@ -4645,14 +4657,17 @@ function deriveProjectId(pkgName, rootPath) {
|
|
|
4645
4657
|
var readFileOrThrow = (path) => (0, import_node_fs2.readFileSync)(path, "utf8");
|
|
4646
4658
|
var MAX_CONFIG_SEARCH_DEPTH = 6;
|
|
4647
4659
|
var MAX_PACKAGE_SEARCH_DEPTH = 50;
|
|
4648
|
-
|
|
4660
|
+
var nonEmptyString = (value) => "string" === typeof value && value.length > 0 ? value : void 0;
|
|
4661
|
+
var MAX_TCP_PORT = 65535;
|
|
4662
|
+
var tcpPort = (value) => Number.isInteger(value) && value > 0 && value <= MAX_TCP_PORT ? value : void 0;
|
|
4663
|
+
function readNearestField(startDir, basename, field, readFile, maxDepth, accept) {
|
|
4649
4664
|
let dir = startDir;
|
|
4650
4665
|
for (let depth = 0; depth <= maxDepth; depth++) {
|
|
4651
4666
|
try {
|
|
4652
4667
|
const parsed = JSON.parse(readFile((0, import_node_path2.join)(dir, basename)));
|
|
4653
4668
|
if ("object" === typeof parsed && parsed !== null) {
|
|
4654
|
-
const value = parsed[field];
|
|
4655
|
-
if (
|
|
4669
|
+
const value = accept(parsed[field]);
|
|
4670
|
+
if (value !== void 0) return value;
|
|
4656
4671
|
}
|
|
4657
4672
|
} catch {
|
|
4658
4673
|
}
|
|
@@ -4663,7 +4678,14 @@ function readNearestField(startDir, basename, field, readFile, maxDepth) {
|
|
|
4663
4678
|
return void 0;
|
|
4664
4679
|
}
|
|
4665
4680
|
function readNearestPackageName(startDir, readFile = readFileOrThrow) {
|
|
4666
|
-
return readNearestField(
|
|
4681
|
+
return readNearestField(
|
|
4682
|
+
startDir,
|
|
4683
|
+
"package.json",
|
|
4684
|
+
"name",
|
|
4685
|
+
readFile,
|
|
4686
|
+
MAX_PACKAGE_SEARCH_DEPTH,
|
|
4687
|
+
nonEmptyString
|
|
4688
|
+
);
|
|
4667
4689
|
}
|
|
4668
4690
|
function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
|
|
4669
4691
|
return readNearestField(
|
|
@@ -4671,7 +4693,18 @@ function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
|
|
|
4671
4693
|
RETICLE_CONFIG_BASENAME,
|
|
4672
4694
|
"projectId",
|
|
4673
4695
|
readFile,
|
|
4674
|
-
MAX_CONFIG_SEARCH_DEPTH
|
|
4696
|
+
MAX_CONFIG_SEARCH_DEPTH,
|
|
4697
|
+
nonEmptyString
|
|
4698
|
+
);
|
|
4699
|
+
}
|
|
4700
|
+
function readConfiguredPort(startDir, readFile = readFileOrThrow) {
|
|
4701
|
+
return readNearestField(
|
|
4702
|
+
startDir,
|
|
4703
|
+
RETICLE_CONFIG_BASENAME,
|
|
4704
|
+
"port",
|
|
4705
|
+
readFile,
|
|
4706
|
+
MAX_CONFIG_SEARCH_DEPTH,
|
|
4707
|
+
tcpPort
|
|
4675
4708
|
);
|
|
4676
4709
|
}
|
|
4677
4710
|
function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName, readConfiguredId = readConfiguredProjectId) {
|
|
@@ -4693,7 +4726,11 @@ function stateHome(env = process.env) {
|
|
|
4693
4726
|
return override !== void 0 && override.length > 0 ? override : (0, import_node_path3.join)((0, import_node_os.homedir)(), ReticleDir.ROOT);
|
|
4694
4727
|
}
|
|
4695
4728
|
|
|
4729
|
+
// src/plugin-name.ts
|
|
4730
|
+
var RETICLE_VITE_PLUGIN_NAME = "reticle";
|
|
4731
|
+
|
|
4696
4732
|
// src/discover-port.ts
|
|
4733
|
+
var MAX_TCP_PORT2 = 65535;
|
|
4697
4734
|
function isAlive(pid) {
|
|
4698
4735
|
try {
|
|
4699
4736
|
process.kill(pid, 0);
|
|
@@ -4702,7 +4739,7 @@ function isAlive(pid) {
|
|
|
4702
4739
|
return false;
|
|
4703
4740
|
}
|
|
4704
4741
|
}
|
|
4705
|
-
function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive) {
|
|
4742
|
+
function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive, configured) {
|
|
4706
4743
|
const entries = [];
|
|
4707
4744
|
let files;
|
|
4708
4745
|
try {
|
|
@@ -4720,7 +4757,32 @@ function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive) {
|
|
|
4720
4757
|
} catch {
|
|
4721
4758
|
}
|
|
4722
4759
|
}
|
|
4723
|
-
return pickDaemonPort(entries, projectId, alive) ?? void 0;
|
|
4760
|
+
return pickDaemonPort(entries, projectId, alive, configured) ?? void 0;
|
|
4761
|
+
}
|
|
4762
|
+
var portConflictWarning = (explicit, configured, used) => `[${RETICLE_VITE_PLUGIN_NAME}] reticle({ port: ${String(explicit)} }) and "port": ${String(configured)} in .reticle.json disagree; connecting to ${String(used)}. The daemon and the CLI follow .reticle.json, so drop the port option from reticle() or make the two match.`;
|
|
4763
|
+
function chooseDaemonPort(sources) {
|
|
4764
|
+
const { discovered, configured, explicit } = sources;
|
|
4765
|
+
const port = discovered ?? configured ?? explicit;
|
|
4766
|
+
if (explicit === void 0 || configured === void 0 || port === void 0) {
|
|
4767
|
+
return { port, warning: void 0 };
|
|
4768
|
+
}
|
|
4769
|
+
return {
|
|
4770
|
+
port,
|
|
4771
|
+
warning: explicit === configured ? void 0 : portConflictWarning(explicit, configured, port)
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
function resolveDaemonPort(explicit, projectId, cwd, env = process.env) {
|
|
4775
|
+
const raw = env[ReticleEnv.PORT];
|
|
4776
|
+
if (raw !== void 0 && /^\d+$/.test(raw)) {
|
|
4777
|
+
const port = Number(raw);
|
|
4778
|
+
if (0 < port && port <= MAX_TCP_PORT2) return { port, warning: void 0 };
|
|
4779
|
+
}
|
|
4780
|
+
const configured = readConfiguredPort(cwd);
|
|
4781
|
+
return chooseDaemonPort({
|
|
4782
|
+
discovered: discoverDaemonPort(projectId, stateHome(env), isAlive, configured),
|
|
4783
|
+
configured,
|
|
4784
|
+
explicit
|
|
4785
|
+
});
|
|
4724
4786
|
}
|
|
4725
4787
|
|
|
4726
4788
|
// src/announce.ts
|
|
@@ -4980,7 +5042,9 @@ function optimizerOptions(key, inherited, add) {
|
|
|
4980
5042
|
|
|
4981
5043
|
// src/disabled-browser-stub.ts
|
|
4982
5044
|
var RETICLE_DISABLED_STUB = "\0reticle-disabled-stub";
|
|
4983
|
-
var
|
|
5045
|
+
var BUILD_COMMAND = "build";
|
|
5046
|
+
var PRODUCTION_MODE = "production";
|
|
5047
|
+
var isReticleDisabledBuild = (desktop, command, mode) => BUILD_COMMAND === command && (!desktop || mode === void 0 || PRODUCTION_MODE === mode);
|
|
4984
5048
|
var RETICLE_DISABLED_STUB_CODE = `
|
|
4985
5049
|
export class Reticle {
|
|
4986
5050
|
connect() {}
|
|
@@ -5199,14 +5263,11 @@ function connectArgs(options) {
|
|
|
5199
5263
|
return Object.keys(args).length > 0 ? JSON.stringify(args) : "";
|
|
5200
5264
|
}
|
|
5201
5265
|
|
|
5202
|
-
// src/plugin-name.ts
|
|
5203
|
-
var RETICLE_VITE_PLUGIN_NAME = "reticle";
|
|
5204
|
-
|
|
5205
5266
|
// src/injection-postcondition.ts
|
|
5206
5267
|
var DEV_INJECTION_GRACE_MS = 1e4;
|
|
5207
5268
|
var notInjectedMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not inject reticle.connect(): the HTML entry module was never matched, so this app carries no instrumentation and will never connect. Check that index.html references your entry with a <script type="module" src="...">, or pass \`inject: false\` and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself. The plugin still inlines that define; a connect without it is refused.`;
|
|
5208
5269
|
var unconfirmedInjectionMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not confirm reticle.connect() was injected: the HTML entry module was not transformed this session. That is expected when Vite served it from its transform cache. If the app does not appear in \`reticle status\`, restart the dev server with \`--force\` to bypass the cache, then check that index.html references your entry with a <script type="module" src="...">.`;
|
|
5209
|
-
var htmlHookNeverRanMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] this app will never connect: the dev server never asked this plugin to transform any HTML, so reticle.connect()
|
|
5270
|
+
var htmlHookNeverRanMessage = (connectModule) => `[${RETICLE_VITE_PLUGIN_NAME}] this app will never connect: the dev server never asked this plugin to transform any HTML and nothing loaded its connect module, so reticle.connect() never reached the page. That usually means your framework renders its own HTML instead of serving index.html \u2014 SvelteKit, Nuxt, Astro, React Router (framework mode) and TanStack Start all do. Fix: in dev, dynamically import '${connectModule}' from your client entry file \u2014 this plugin serves that module, so leave \`inject\` on \u2014 or import '@reticlehq/browser' there and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself.`;
|
|
5210
5271
|
var defaultSchedule = (run, ms) => {
|
|
5211
5272
|
const timer = setTimeout(run, ms);
|
|
5212
5273
|
timer.unref?.();
|
|
@@ -5215,9 +5276,9 @@ function createInjectionWatch(deps) {
|
|
|
5215
5276
|
let htmlRequested = false;
|
|
5216
5277
|
const isDocumentRequest = (req) => true === req.headers?.accept?.includes("text/html");
|
|
5217
5278
|
const checkHtmlHookRan = () => {
|
|
5218
|
-
if (deps.desktop || !deps.inject || deps.
|
|
5279
|
+
if (deps.desktop || !deps.inject || deps.connectDelivered()) return;
|
|
5219
5280
|
if (!htmlRequested) return;
|
|
5220
|
-
deps.warn(htmlHookNeverRanMessage());
|
|
5281
|
+
deps.warn(htmlHookNeverRanMessage(deps.connectModule()));
|
|
5221
5282
|
};
|
|
5222
5283
|
const noteHtmlRequest = () => {
|
|
5223
5284
|
if (htmlRequested) return;
|
|
@@ -5363,14 +5424,22 @@ function reticle(options = {}) {
|
|
|
5363
5424
|
let htmlEntrySpecifier;
|
|
5364
5425
|
let root;
|
|
5365
5426
|
let command;
|
|
5427
|
+
let mode;
|
|
5428
|
+
const disabledBuild = () => isReticleDisabledBuild(desktop, command, mode);
|
|
5366
5429
|
let base;
|
|
5367
5430
|
let vitestBrowser = false;
|
|
5368
5431
|
const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
|
|
5369
5432
|
let injected = false;
|
|
5370
5433
|
const announcedIgnored = /* @__PURE__ */ new Set();
|
|
5371
5434
|
let htmlTransformed = false;
|
|
5435
|
+
let connectDelivered = false;
|
|
5436
|
+
let lastPortWarning;
|
|
5372
5437
|
const resolveLazy = () => {
|
|
5373
|
-
const port = resolved.port
|
|
5438
|
+
const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, process.cwd());
|
|
5439
|
+
if (warning !== void 0 && warning !== lastPortWarning) {
|
|
5440
|
+
lastPortWarning = warning;
|
|
5441
|
+
warn(warning);
|
|
5442
|
+
}
|
|
5374
5443
|
const withPort = port !== void 0 ? { ...resolved, port } : resolved;
|
|
5375
5444
|
const token = withPort.token ?? readPairingToken();
|
|
5376
5445
|
const withToken = token !== void 0 ? { ...withPort, token } : withPort;
|
|
@@ -5385,7 +5454,8 @@ function reticle(options = {}) {
|
|
|
5385
5454
|
desktop,
|
|
5386
5455
|
inject,
|
|
5387
5456
|
injected: () => injected,
|
|
5388
|
-
|
|
5457
|
+
connectDelivered: () => htmlTransformed || connectDelivered,
|
|
5458
|
+
connectModule: () => connectModuleUrl(base),
|
|
5389
5459
|
warn
|
|
5390
5460
|
});
|
|
5391
5461
|
return {
|
|
@@ -5473,7 +5543,10 @@ function reticle(options = {}) {
|
|
|
5473
5543
|
};
|
|
5474
5544
|
},
|
|
5475
5545
|
transform(code, id) {
|
|
5476
|
-
if (
|
|
5546
|
+
if (disabledBuild()) return null;
|
|
5547
|
+
if (!connectDelivered && code.includes(RETICLE_TOKEN_GLOBAL) && !id.includes(NODE_MODULES)) {
|
|
5548
|
+
connectDelivered = true;
|
|
5549
|
+
}
|
|
5477
5550
|
if (desktop && inject && isHtmlEntry(id, htmlEntrySpecifier, root)) {
|
|
5478
5551
|
injected = true;
|
|
5479
5552
|
const withConnect = `${connectModuleSource(resolveLazy())}
|
|
@@ -5497,7 +5570,7 @@ ${code}`;
|
|
|
5497
5570
|
return stamp(code, id);
|
|
5498
5571
|
},
|
|
5499
5572
|
resolveId(id, importer) {
|
|
5500
|
-
if (
|
|
5573
|
+
if (disabledBuild() && id === RETICLE_SENSOR) {
|
|
5501
5574
|
return RETICLE_DISABLED_STUB;
|
|
5502
5575
|
}
|
|
5503
5576
|
if (desktop && inject && importer !== void 0 && importer.includes(".html")) {
|
|
@@ -5506,17 +5579,19 @@ ${code}`;
|
|
|
5506
5579
|
return inject && id === RETICLE_CONNECT_MODULE ? RETICLE_CONNECT_MODULE : null;
|
|
5507
5580
|
},
|
|
5508
5581
|
load(id) {
|
|
5509
|
-
if (
|
|
5582
|
+
if (disabledBuild() && id === RETICLE_DISABLED_STUB) {
|
|
5510
5583
|
return RETICLE_DISABLED_STUB_CODE;
|
|
5511
5584
|
}
|
|
5512
5585
|
if (!inject || id !== RETICLE_CONNECT_MODULE) return null;
|
|
5513
5586
|
const source = currentConnectSource();
|
|
5514
5587
|
lastServedConnectSource = source;
|
|
5588
|
+
connectDelivered = true;
|
|
5515
5589
|
return source;
|
|
5516
5590
|
},
|
|
5517
5591
|
configResolved(config) {
|
|
5518
5592
|
root = config.root;
|
|
5519
5593
|
command = config.command;
|
|
5594
|
+
mode = config.mode;
|
|
5520
5595
|
base = config.base;
|
|
5521
5596
|
vitestBrowser = isVitestBrowserServer(config);
|
|
5522
5597
|
},
|
|
@@ -5541,7 +5616,6 @@ ${code}`;
|
|
|
5541
5616
|
* appearing IS a change), and makes the module inert once it has settled.
|
|
5542
5617
|
*/
|
|
5543
5618
|
configureServer(server) {
|
|
5544
|
-
if (!inject) return;
|
|
5545
5619
|
const announce = () => {
|
|
5546
5620
|
const address = server.httpServer?.address();
|
|
5547
5621
|
const port = "object" === typeof address && null !== address && void 0 !== address ? address.port : void 0;
|
|
@@ -5566,6 +5640,7 @@ ${code}`;
|
|
|
5566
5640
|
} else {
|
|
5567
5641
|
announce();
|
|
5568
5642
|
}
|
|
5643
|
+
if (!inject) return;
|
|
5569
5644
|
server.middlewares.use((req, _res, next) => {
|
|
5570
5645
|
if (!desktop && watch.isDocumentRequest(req)) watch.noteHtmlRequest();
|
|
5571
5646
|
const requestPath = (req.url ?? "").split("?")[0];
|
|
@@ -5586,14 +5661,14 @@ ${code}`;
|
|
|
5586
5661
|
* build that could not instrument must fail loudly instead of shipping a binary that lies.
|
|
5587
5662
|
*/
|
|
5588
5663
|
buildEnd() {
|
|
5589
|
-
if (!desktop || !inject || injected) return;
|
|
5664
|
+
if (!desktop || !inject || injected || disabledBuild()) return;
|
|
5590
5665
|
throw new Error(notInjectedMessage());
|
|
5591
5666
|
},
|
|
5592
5667
|
checkInjectedForTest: watch.checkInjected,
|
|
5593
5668
|
checkHtmlHookForTest: watch.checkHtmlHookRan,
|
|
5594
5669
|
injectionWatchForTest: watch,
|
|
5595
5670
|
transformIndexHtml() {
|
|
5596
|
-
if (
|
|
5671
|
+
if (disabledBuild()) return [];
|
|
5597
5672
|
htmlTransformed = true;
|
|
5598
5673
|
if (desktop && inject && "serve" === command) watch.armDesktopCheck();
|
|
5599
5674
|
if (!inject || desktop) return [];
|
package/dist/index.d.cts
CHANGED
|
@@ -72,14 +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
|
-
*
|
|
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.
|
|
77
80
|
* - `connect()` is called with `allowInProduction`, because that same renderer reports
|
|
78
81
|
* NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
|
|
79
82
|
*
|
|
80
|
-
* Off by default and never inferred
|
|
81
|
-
* is exactly what a web app must never ship. Keep it behind your own dev-only build (a dev target,
|
|
82
|
-
* or `process.env.NODE_ENV !== 'production'` in vite.config) so it cannot reach a release binary.
|
|
83
|
+
* Off by default and never inferred.
|
|
83
84
|
*/
|
|
84
85
|
desktop?: boolean;
|
|
85
86
|
/**
|
|
@@ -220,6 +221,8 @@ export interface ReticleVitePlugin {
|
|
|
220
221
|
configResolved?: (config: {
|
|
221
222
|
root?: string;
|
|
222
223
|
command?: string;
|
|
224
|
+
/** 'production' unless `--mode` says otherwise. See isReticleDisabledBuild. */
|
|
225
|
+
mode?: string;
|
|
223
226
|
base?: string;
|
|
224
227
|
/** Vitest's block, read only to spot browser mode. See isVitestBrowserServer. */
|
|
225
228
|
test?: unknown;
|
|
@@ -327,11 +330,9 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
|
|
|
327
330
|
* The web plugin also builds, replacing the browser SDK with an inert stub so a default
|
|
328
331
|
* production bundle ships no Reticle runtime code at all.
|
|
329
332
|
*
|
|
330
|
-
* `desktop: true`
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* gating back to the caller — keep it behind your own dev-only build target so an instrumented
|
|
334
|
-
* bundle can never reach 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.
|
|
335
336
|
*/
|
|
336
337
|
/**
|
|
337
338
|
* The daemon's journal directory, as a matcher every chokidar major honours.
|
package/dist/index.d.ts
CHANGED
|
@@ -72,14 +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
|
-
*
|
|
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.
|
|
77
80
|
* - `connect()` is called with `allowInProduction`, because that same renderer reports
|
|
78
81
|
* NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
|
|
79
82
|
*
|
|
80
|
-
* Off by default and never inferred
|
|
81
|
-
* is exactly what a web app must never ship. Keep it behind your own dev-only build (a dev target,
|
|
82
|
-
* or `process.env.NODE_ENV !== 'production'` in vite.config) so it cannot reach a release binary.
|
|
83
|
+
* Off by default and never inferred.
|
|
83
84
|
*/
|
|
84
85
|
desktop?: boolean;
|
|
85
86
|
/**
|
|
@@ -220,6 +221,8 @@ export interface ReticleVitePlugin {
|
|
|
220
221
|
configResolved?: (config: {
|
|
221
222
|
root?: string;
|
|
222
223
|
command?: string;
|
|
224
|
+
/** 'production' unless `--mode` says otherwise. See isReticleDisabledBuild. */
|
|
225
|
+
mode?: string;
|
|
223
226
|
base?: string;
|
|
224
227
|
/** Vitest's block, read only to spot browser mode. See isVitestBrowserServer. */
|
|
225
228
|
test?: unknown;
|
|
@@ -327,11 +330,9 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
|
|
|
327
330
|
* The web plugin also builds, replacing the browser SDK with an inert stub so a default
|
|
328
331
|
* production bundle ships no Reticle runtime code at all.
|
|
329
332
|
*
|
|
330
|
-
* `desktop: true`
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* gating back to the caller — keep it behind your own dev-only build target so an instrumented
|
|
334
|
-
* bundle can never reach 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.
|
|
335
336
|
*/
|
|
336
337
|
/**
|
|
337
338
|
* The daemon's journal directory, as a matcher every chokidar major honours.
|
package/dist/index.js
CHANGED
|
@@ -5,12 +5,12 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { RETICLE_RENDER_PREHOOK, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
|
|
7
7
|
import { resolveProjectId } from './project-id.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveDaemonPort } from './discover-port.js';
|
|
9
9
|
import { announceDevServer } from './announce.js';
|
|
10
10
|
import { stampSvelte } from './svelte-source.js';
|
|
11
|
-
import { ignoredFileNotice, optsOutOfStamping, shouldStamp, shouldStampSvelte, stamp, } from './stamping.js';
|
|
11
|
+
import { NODE_MODULES, ignoredFileNotice, optsOutOfStamping, shouldStamp, shouldStampSvelte, stamp, } from './stamping.js';
|
|
12
12
|
import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, optimizerOptions, } from './installed.js';
|
|
13
|
-
import { RETICLE_DISABLED_STUB_CODE, RETICLE_DISABLED_STUB,
|
|
13
|
+
import { RETICLE_DISABLED_STUB_CODE, RETICLE_DISABLED_STUB, isReticleDisabledBuild, } from './disabled-browser-stub.js';
|
|
14
14
|
import { connectArgs } from './connect-args.js';
|
|
15
15
|
import { createInjectionWatch, notInjectedMessage, } from './injection-postcondition.js';
|
|
16
16
|
import { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
|
|
@@ -244,11 +244,9 @@ export function connectModuleSource(options, devModule = null) {
|
|
|
244
244
|
* The web plugin also builds, replacing the browser SDK with an inert stub so a default
|
|
245
245
|
* production bundle ships no Reticle runtime code at all.
|
|
246
246
|
*
|
|
247
|
-
* `desktop: true`
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* gating back to the caller — keep it behind your own dev-only build target so an instrumented
|
|
251
|
-
* bundle can never reach 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.
|
|
252
250
|
*/
|
|
253
251
|
/**
|
|
254
252
|
* The daemon's journal directory, as a matcher every chokidar major honours.
|
|
@@ -279,6 +277,10 @@ export function reticle(options = {}) {
|
|
|
279
277
|
let root;
|
|
280
278
|
/** 'serve' | 'build'. The dev check only applies to serve; buildEnd covers the other. */
|
|
281
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);
|
|
282
284
|
/** Vite's resolved `base`. Undefined until configResolved, which is before any HTML is served. */
|
|
283
285
|
let base;
|
|
284
286
|
/** True only when THIS server is Vitest's browser-mode runner — see isVitestBrowserServer. */
|
|
@@ -291,18 +293,33 @@ export function reticle(options = {}) {
|
|
|
291
293
|
/**
|
|
292
294
|
* Whether Vite ever asked us to transform the app's HTML.
|
|
293
295
|
*
|
|
294
|
-
* On the web this is
|
|
295
|
-
*
|
|
296
|
-
* 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.
|
|
297
298
|
*/
|
|
298
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;
|
|
299
311
|
/**
|
|
300
312
|
* Resolve port + token at the moment of injection, not at plugin construction. By the time a
|
|
301
313
|
* module is served or built the daemon is up and has written its pairing token; resolving early
|
|
302
314
|
* would bake in `undefined` and the app would fail auth on every connect.
|
|
303
315
|
*/
|
|
304
316
|
const resolveLazy = () => {
|
|
305
|
-
|
|
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
|
+
}
|
|
306
323
|
const withPort = port !== undefined ? { ...resolved, port } : resolved;
|
|
307
324
|
const token = withPort.token ?? readPairingToken();
|
|
308
325
|
const withToken = token !== undefined ? { ...withPort, token } : withPort;
|
|
@@ -331,7 +348,8 @@ export function reticle(options = {}) {
|
|
|
331
348
|
desktop,
|
|
332
349
|
inject,
|
|
333
350
|
injected: () => injected,
|
|
334
|
-
|
|
351
|
+
connectDelivered: () => htmlTransformed || connectDelivered,
|
|
352
|
+
connectModule: () => connectModuleUrl(base),
|
|
335
353
|
warn,
|
|
336
354
|
});
|
|
337
355
|
return {
|
|
@@ -417,8 +435,13 @@ export function reticle(options = {}) {
|
|
|
417
435
|
};
|
|
418
436
|
},
|
|
419
437
|
transform(code, id) {
|
|
420
|
-
if (
|
|
438
|
+
if (disabledBuild())
|
|
421
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
|
+
}
|
|
422
445
|
// Desktop injection: prepend connect() to the HTML's own entry module. It is a REAL module, so
|
|
423
446
|
// its bare `@reticlehq/react` import resolves through the normal pipeline in both dev and
|
|
424
447
|
// build — which a virtual <script src> only ever did in dev.
|
|
@@ -452,7 +475,7 @@ export function reticle(options = {}) {
|
|
|
452
475
|
return stamp(code, id);
|
|
453
476
|
},
|
|
454
477
|
resolveId(id, importer) {
|
|
455
|
-
if (
|
|
478
|
+
if (disabledBuild() && id === RETICLE_SENSOR) {
|
|
456
479
|
return RETICLE_DISABLED_STUB;
|
|
457
480
|
}
|
|
458
481
|
// Desktop: remember the module the HTML points at, so `transform` can prepend connect() into
|
|
@@ -470,18 +493,20 @@ export function reticle(options = {}) {
|
|
|
470
493
|
return inject && id === RETICLE_CONNECT_MODULE ? RETICLE_CONNECT_MODULE : null;
|
|
471
494
|
},
|
|
472
495
|
load(id) {
|
|
473
|
-
if (
|
|
496
|
+
if (disabledBuild() && id === RETICLE_DISABLED_STUB) {
|
|
474
497
|
return RETICLE_DISABLED_STUB_CODE;
|
|
475
498
|
}
|
|
476
499
|
if (!inject || id !== RETICLE_CONNECT_MODULE)
|
|
477
500
|
return null;
|
|
478
501
|
const source = currentConnectSource();
|
|
479
502
|
lastServedConnectSource = source;
|
|
503
|
+
connectDelivered = true;
|
|
480
504
|
return source;
|
|
481
505
|
},
|
|
482
506
|
configResolved(config) {
|
|
483
507
|
root = config.root;
|
|
484
508
|
command = config.command;
|
|
509
|
+
mode = config.mode;
|
|
485
510
|
base = config.base;
|
|
486
511
|
vitestBrowser = isVitestBrowserServer(config);
|
|
487
512
|
},
|
|
@@ -506,8 +531,6 @@ export function reticle(options = {}) {
|
|
|
506
531
|
* appearing IS a change), and makes the module inert once it has settled.
|
|
507
532
|
*/
|
|
508
533
|
configureServer(server) {
|
|
509
|
-
if (!inject)
|
|
510
|
-
return;
|
|
511
534
|
// The web post-condition is armed by the first DOCUMENT REQUEST, in the middleware below.
|
|
512
535
|
//
|
|
513
536
|
// Not from `transformIndexHtml`, because a framework that renders its own HTML never calls it
|
|
@@ -559,6 +582,12 @@ export function reticle(options = {}) {
|
|
|
559
582
|
else {
|
|
560
583
|
announce();
|
|
561
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;
|
|
562
591
|
server.middlewares.use((req, _res, next) => {
|
|
563
592
|
// One middleware, two observations. A second `use()` would work equally well in Vite and
|
|
564
593
|
// is the obvious way to write this, but it makes the ORDER of registration load-bearing for
|
|
@@ -588,7 +617,8 @@ export function reticle(options = {}) {
|
|
|
588
617
|
* build that could not instrument must fail loudly instead of shipping a binary that lies.
|
|
589
618
|
*/
|
|
590
619
|
buildEnd() {
|
|
591
|
-
|
|
620
|
+
// A production build injects nothing on purpose; see isReticleDisabledBuild.
|
|
621
|
+
if (!desktop || !inject || injected || disabledBuild())
|
|
592
622
|
return;
|
|
593
623
|
throw new Error(notInjectedMessage());
|
|
594
624
|
},
|
|
@@ -596,7 +626,7 @@ export function reticle(options = {}) {
|
|
|
596
626
|
checkHtmlHookForTest: watch.checkHtmlHookRan,
|
|
597
627
|
injectionWatchForTest: watch,
|
|
598
628
|
transformIndexHtml() {
|
|
599
|
-
if (
|
|
629
|
+
if (disabledBuild())
|
|
600
630
|
return [];
|
|
601
631
|
htmlTransformed = true;
|
|
602
632
|
if (desktop && inject && 'serve' === command)
|
|
@@ -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
|
package/dist/stamping.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* the JSX one is `@reticlehq/babel-plugin`, which also reads the opt-out on its own for the callers
|
|
9
9
|
* that reach it without Vite.
|
|
10
10
|
*/
|
|
11
|
+
export declare const NODE_MODULES = "node_modules";
|
|
11
12
|
/** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
|
|
12
13
|
export declare function stampableId(id: string): string | null;
|
|
13
14
|
export declare function shouldStamp(id: string): boolean;
|
package/dist/stamping.js
CHANGED
|
@@ -17,7 +17,7 @@ import { SVELTE_FILE } from './svelte-source.js';
|
|
|
17
17
|
const JSX_FILE = /\.[jt]sx$/;
|
|
18
18
|
/** Rollup virtual-module ids start with a NUL byte; never transform those. */
|
|
19
19
|
const VIRTUAL_PREFIX = '\0';
|
|
20
|
-
const NODE_MODULES = 'node_modules';
|
|
20
|
+
export const NODE_MODULES = 'node_modules';
|
|
21
21
|
/** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
|
|
22
22
|
export function stampableId(id) {
|
|
23
23
|
if (id.startsWith(VIRTUAL_PREFIX))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/vite-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Vite plugin for Reticle: dev-only source-map stamping plus auto-injected reticle.connect(). apply:'serve' guarantees it never ships to production.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@babel/core": "^7.26.0",
|
|
44
|
-
"@reticlehq/babel-plugin": "3.
|
|
45
|
-
"@reticlehq/core": "3.
|
|
44
|
+
"@reticlehq/babel-plugin": "3.5.0",
|
|
45
|
+
"@reticlehq/core": "3.5.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/babel__core": "^7.20.5",
|
|
49
49
|
"esbuild": "^0.28.2",
|
|
50
|
-
"svelte": "^5.
|
|
50
|
+
"svelte": "^5.57.1",
|
|
51
51
|
"vite": "^8",
|
|
52
|
-
"@reticlehq/react": "3.
|
|
52
|
+
"@reticlehq/react": "3.5.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"vite": ">=4"
|