@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/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,6 +56,21 @@ 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
|
+
|
|
61
|
+
### Opting one file out of source stamping
|
|
62
|
+
|
|
63
|
+
A file whose **first non-empty line** is a comment carrying `@reticle-ignore` is not stamped, and its elements will have no `file:line`:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// @reticle-ignore
|
|
67
|
+
export function Generated() { … }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`/* @reticle-ignore */` works the same way, and a `.svelte` component uses `<!-- @reticle-ignore -->` because it has no line for a `//` before its markup.
|
|
71
|
+
|
|
72
|
+
Whole file, first line, on purpose. The reasons to opt out are file-shaped — a generated component, a snapshot test the stamp would break — and a marker that worked from line 40 would be a marker nobody can find. `include`/`exclude` globs still exist for whole directories; this is for the one file whose reason sits with the file.
|
|
73
|
+
|
|
74
|
+
It is not silent. The plugin reports each opted-out file once, by name, in the dev-server output, so an opt-out is something a reader sees rather than something they discover months later from a pointer that never resolves.
|
|
60
75
|
|
|
61
76
|
Apache-2.0.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `reticle.connect` argument literal the plugin bakes into the generated connect call.
|
|
3
|
+
*
|
|
4
|
+
* Split out of index.ts, which sits six lines under its 1000-line cap on main -- this option could
|
|
5
|
+
* not be added inline. Cohesive on its own terms: one function, whose whole job is deciding which
|
|
6
|
+
* of the plugin's options the PAGE needs to be told about.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReticleVitePluginOptions } from './index.js';
|
|
9
|
+
/** Build the `reticle.connect` argument literal — only includes keys the user set. */
|
|
10
|
+
export declare function connectArgs(options: ReticleVitePluginOptions): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `reticle.connect` argument literal the plugin bakes into the generated connect call.
|
|
3
|
+
*
|
|
4
|
+
* Split out of index.ts, which sits six lines under its 1000-line cap on main -- this option could
|
|
5
|
+
* not be added inline. Cohesive on its own terms: one function, whose whole job is deciding which
|
|
6
|
+
* of the plugin's options the PAGE needs to be told about.
|
|
7
|
+
*/
|
|
8
|
+
import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
|
|
9
|
+
/** Build the `reticle.connect` argument literal — only includes keys the user set. */
|
|
10
|
+
export function connectArgs(options) {
|
|
11
|
+
const args = {};
|
|
12
|
+
const port = options.port ?? RETICLE_DEFAULT_PORT;
|
|
13
|
+
if (port !== RETICLE_DEFAULT_PORT)
|
|
14
|
+
args['url'] = bridgeWsUrl(port);
|
|
15
|
+
if (options.session !== undefined)
|
|
16
|
+
args['session'] = options.session;
|
|
17
|
+
if (options.projectId !== undefined)
|
|
18
|
+
args['projectId'] = options.projectId;
|
|
19
|
+
if (options.token !== undefined)
|
|
20
|
+
args['token'] = options.token;
|
|
21
|
+
// Passed as connect ARGUMENTS, not as a `define`. A define substitutes a bare identifier in the
|
|
22
|
+
// source it transforms; the SDK reads these as `globalThis[NAME]`, a dynamic lookup no define can
|
|
23
|
+
// ever reach — so defining them looked right, shipped, and did nothing. Baking them into the
|
|
24
|
+
// generated connect call is a literal in generated source: no bundler subtleties, works the same
|
|
25
|
+
// in dev and in a desktop build.
|
|
26
|
+
if (options.root !== undefined && options.root.length > 0)
|
|
27
|
+
args['root'] = options.root;
|
|
28
|
+
if (options.sdkVersion !== undefined && options.sdkVersion.length > 0) {
|
|
29
|
+
args['sdkVersion'] = options.sdkVersion;
|
|
30
|
+
}
|
|
31
|
+
// A desktop renderer is a production build by construction; without this the SDK's prod backstop
|
|
32
|
+
// refuses to connect and the app is silently uninstrumented.
|
|
33
|
+
if (true === options.desktop)
|
|
34
|
+
args['allowInProduction'] = true;
|
|
35
|
+
// Env wins nothing — it only turns the flag ON, so a config that never set it can still be
|
|
36
|
+
// switched on for one debugging session without editing vite.config and restarting the mental
|
|
37
|
+
// model with it.
|
|
38
|
+
if (true === options.captureNetworkBodies || '1' === process.env['VITE_RETICLE_CAPTURE_BODIES']) {
|
|
39
|
+
args['captureNetworkBodies'] = true;
|
|
40
|
+
}
|
|
41
|
+
// Only the OPT-OUT is announced, never the default. The daemon reads absence as "unknown", so
|
|
42
|
+
// sending `true` here would say nothing it does not already assume — while sending `false` is the
|
|
43
|
+
// one fact a red verdict needs to stop prescribing a plugin this project has deliberately muted.
|
|
44
|
+
if (false === options.sourceMapping)
|
|
45
|
+
args['sourceMapping'] = false;
|
|
46
|
+
// The one option that defaults ON, so the env var and the config flag both DISABLE rather than
|
|
47
|
+
// enable. Emitted only when switched off; the default stays implicit in the SDK.
|
|
48
|
+
if (false === options.captureErrorBodies || '1' === process.env['VITE_RETICLE_NO_ERROR_BODIES']) {
|
|
49
|
+
args['captureErrorBodies'] = false;
|
|
50
|
+
}
|
|
51
|
+
// Same shape, same reason -- except this one carries a VALUE, so the env var is parsed rather
|
|
52
|
+
// than tested for '1'. A non-numeric env var is ignored rather than fatal: it must not take down
|
|
53
|
+
// an app's dev server, and the SDK clamps whatever does arrive.
|
|
54
|
+
const envBodyChars = Number(process.env['VITE_RETICLE_BODY_MAX_CHARS']);
|
|
55
|
+
const bodyChars = options.networkBodyMaxChars ??
|
|
56
|
+
(Number.isFinite(envBodyChars) && envBodyChars > 0 ? envBodyChars : undefined);
|
|
57
|
+
if (bodyChars !== undefined) {
|
|
58
|
+
args['networkBodyMaxChars'] = bodyChars;
|
|
59
|
+
}
|
|
60
|
+
// Same shape, same reason. Off unless asked for, in a config or for one session.
|
|
61
|
+
if (true === options.exposePresenter || '1' === process.env['VITE_RETICLE_EXPOSE_PRESENTER']) {
|
|
62
|
+
args['exposePresenter'] = true;
|
|
63
|
+
}
|
|
64
|
+
// Same shape, same reason: without it an app that cannot be served on localhost has no way to
|
|
65
|
+
// reach the SDK option at all. The pairing token still applies — see the option's docstring.
|
|
66
|
+
if (true === options.allowNonLocalhost ||
|
|
67
|
+
'1' === process.env['VITE_RETICLE_ALLOW_NON_LOCALHOST']) {
|
|
68
|
+
args['allowNonLocalhost'] = true;
|
|
69
|
+
}
|
|
70
|
+
return Object.keys(args).length > 0 ? JSON.stringify(args) : '';
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const RETICLE_DISABLED_STUB = "\0reticle-disabled-stub";
|
|
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;
|
|
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";
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export const RETICLE_DISABLED_STUB = '\0reticle-disabled-stub';
|
|
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);
|
|
18
|
+
export const RETICLE_DISABLED_STUB_CODE = `
|
|
19
|
+
export class Reticle {
|
|
20
|
+
connect() {}
|
|
21
|
+
signal() {}
|
|
22
|
+
state() {}
|
|
23
|
+
renderCommit() {}
|
|
24
|
+
describe() {}
|
|
25
|
+
endSession() {}
|
|
26
|
+
observeHotUpdates() {}
|
|
27
|
+
disconnect() {}
|
|
28
|
+
|
|
29
|
+
get connected() {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const reticle = new Reticle();
|
|
35
|
+
|
|
36
|
+
export const SESSION_AUTO = 'auto';
|
|
37
|
+
|
|
38
|
+
export function setIgnoreSelectors() {}
|
|
39
|
+
|
|
40
|
+
export function registerAdapter() {}
|
|
41
|
+
export function identifyComponent() {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
export function readComponentState() {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
export function elementHasHoverHandlers() {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
export function adapterNames() {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerStore() {}
|
|
55
|
+
export function unregisterStore() {}
|
|
56
|
+
export function storeNames() {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
export function readStores() {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
export function readStoresWithTruncation() {
|
|
63
|
+
return { stores: {} };
|
|
64
|
+
}
|
|
65
|
+
export function sourceOwner() {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
export function markAdapterSource() {}
|
|
69
|
+
|
|
70
|
+
export function tanstackQueryStore() {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
export function jotaiStore() {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
export function xstateStore() {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
export function valtioStore() {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
export function mobxStore() {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
export function recoilStore() {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export function svelteStore() {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
export function piniaStore() {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
export function pushStore() {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function registerCapabilities() {}
|
|
99
|
+
export function getCapabilities() {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
export function hasCapabilities() {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
export function setCapabilitiesListener() {}
|
|
106
|
+
|
|
107
|
+
export function createReticleEmitter() {
|
|
108
|
+
return {
|
|
109
|
+
signal() {},
|
|
110
|
+
state() {},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function commitAndSignal() {}
|
|
115
|
+
export function registerReticleDomain() {}
|
|
116
|
+
|
|
117
|
+
export function buildSnapshot() {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function inspectChart() {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
export function canvasChartData() {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function matchQuery() {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
export function runQuery() {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function executeAction() {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
export function executeSequence() {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function describe() {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
export function getRole() {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
export function getAccessibleName() {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
export function getStates() {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
export function isVisible() {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class RefRegistry {
|
|
159
|
+
get size() {
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
markEdited() {}
|
|
164
|
+
|
|
165
|
+
mintedBeforeLastEdit() {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
refFor() {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
resolve() {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const refs = new RefRegistry();
|
|
179
|
+
|
|
180
|
+
export class Annotator {
|
|
181
|
+
constructor() {}
|
|
182
|
+
|
|
183
|
+
get active() {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
get markCount() {
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
mount() {}
|
|
192
|
+
unmount() {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function installAnnotator() {}
|
|
196
|
+
|
|
197
|
+
export function resolveMarkAnchor() {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
`;
|
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): DaemonPortChoice;
|
package/dist/discover-port.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
*/
|
|
@@ -8,6 +9,8 @@ import { readdirSync, readFileSync } from 'node:fs';
|
|
|
8
9
|
import { join } from 'node:path';
|
|
9
10
|
import { daemonRegistryPort, DaemonRegistryEntrySchema, pickDaemonPort, } 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';
|
|
11
14
|
/** process.kill(pid, 0) throws iff the process is gone — the same liveness probe the daemon uses. */
|
|
12
15
|
function isAlive(pid) {
|
|
13
16
|
try {
|
|
@@ -23,7 +26,9 @@ function isAlive(pid) {
|
|
|
23
26
|
* to the default port — never auto-connects to a mismatched daemon). `home` and `alive` are injectable
|
|
24
27
|
* so the selection is unit-tested without a real ~/.reticle or real processes.
|
|
25
28
|
*/
|
|
26
|
-
export function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive
|
|
29
|
+
export function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive,
|
|
30
|
+
/** `port` in `.reticle.json`: wins whenever a live daemon is registered on it — see pickDaemonPort. */
|
|
31
|
+
configured) {
|
|
27
32
|
const entries = [];
|
|
28
33
|
let files;
|
|
29
34
|
try {
|
|
@@ -44,5 +49,39 @@ export function discoverDaemonPort(projectId, home = stateHome(), alive = isAliv
|
|
|
44
49
|
// corrupt/unreadable entry — skip
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
|
-
return pickDaemonPort(entries, projectId, alive) ?? undefined;
|
|
52
|
+
return pickDaemonPort(entries, projectId, alive, configured) ?? undefined;
|
|
53
|
+
}
|
|
54
|
+
const portConflictWarning = (explicit, configured, used) => `[${RETICLE_VITE_PLUGIN_NAME}] reticle({ port: ${String(explicit)} }) and "port": ` +
|
|
55
|
+
`${String(configured)} in .reticle.json disagree; connecting to ${String(used)}. The daemon and ` +
|
|
56
|
+
'the CLI follow .reticle.json, so drop the port option from reticle() or make the two match.';
|
|
57
|
+
/**
|
|
58
|
+
* The one rule for which port the page dials: wherever the daemon for this project actually is.
|
|
59
|
+
*
|
|
60
|
+
* A live registered daemon first, because it is a fact rather than a statement of intent — and when
|
|
61
|
+
* one is live on the configured port, that one (see pickDaemonPort: a moved port left the old daemon
|
|
62
|
+
* alive, and discovery used to pick it). Then
|
|
63
|
+
* `.reticle.json`, because that is what the daemon and the CLI read. The option used to win, and a
|
|
64
|
+
* user who edited the file moved the daemon while the page kept dialling the literal `init` had
|
|
65
|
+
* written into vite.config: "never dialled the bridge", with nothing saying why. The option counts
|
|
66
|
+
* only when nothing else says anything, and undefined leaves the default to the connect.
|
|
67
|
+
*/
|
|
68
|
+
export function chooseDaemonPort(sources) {
|
|
69
|
+
const { discovered, configured, explicit } = sources;
|
|
70
|
+
const port = discovered ?? configured ?? explicit;
|
|
71
|
+
if (explicit === undefined || configured === undefined || port === undefined) {
|
|
72
|
+
return { port, warning: undefined };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
port,
|
|
76
|
+
warning: explicit === configured ? undefined : portConflictWarning(explicit, configured, port),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** {@link chooseDaemonPort} over the real registry and the real `.reticle.json` above `cwd`. */
|
|
80
|
+
export function resolveDaemonPort(explicit, projectId, cwd) {
|
|
81
|
+
const configured = readConfiguredPort(cwd);
|
|
82
|
+
return chooseDaemonPort({
|
|
83
|
+
discovered: discoverDaemonPort(projectId, stateHome(), isAlive, configured),
|
|
84
|
+
configured,
|
|
85
|
+
explicit,
|
|
86
|
+
});
|
|
48
87
|
}
|