@reticlehq/vite-plugin 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/announce.d.ts +16 -0
- package/dist/announce.js +60 -0
- package/dist/discover-port.js +3 -3
- package/dist/index.cjs +169 -54
- package/dist/index.d.cts +38 -21
- package/dist/index.d.ts +38 -21
- package/dist/index.js +79 -57
- package/dist/installed.d.ts +2 -4
- package/dist/installed.js +5 -6
- package/dist/state-home.d.ts +1 -0
- package/dist/state-home.js +19 -0
- package/package.json +5 -5
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type DevServerEntry } from '@reticlehq/core';
|
|
2
|
+
/** The filesystem this needs, injected so the writer is unit-testable without a real home. */
|
|
3
|
+
export interface AnnounceIo {
|
|
4
|
+
mkdir: (dir: string) => void;
|
|
5
|
+
writeFile: (path: string, data: string) => void;
|
|
6
|
+
removeFile: (path: string) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Publish the entry; returns the cleanup that withdraws it.
|
|
10
|
+
*
|
|
11
|
+
* The cleanup is idempotent because it is called from shutdown paths — `close`, `SIGINT`, process
|
|
12
|
+
* exit — which fire more than once and race each other. A stale entry is handled on the read side
|
|
13
|
+
* by a liveness check (`liveDevServers`), so a missed cleanup degrades rather than misleads; a
|
|
14
|
+
* throw inside a signal handler does not degrade, it takes the shutdown with it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function announceDevServer(entry: DevServerEntry, home?: string, io?: AnnounceIo): () => void;
|
package/dist/announce.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Announce this dev server to `~/.reticle`, so setup can be diagnosed from facts rather than guesses.
|
|
3
|
+
*
|
|
4
|
+
* The mirror of `discover-port.ts`: that reads the daemon's `daemon-<port>.json` to find a bridge,
|
|
5
|
+
* this writes `devserver-<port>.json` so the bridge — and `reticle init` — can see that an
|
|
6
|
+
* instrumented dev server exists at all.
|
|
7
|
+
*
|
|
8
|
+
* Why this is the right half to add, rather than teaching `init` to run the dev command: the script
|
|
9
|
+
* name, the package manager, the port and the framework are all things a user or their agent can
|
|
10
|
+
* change, and a setup step that hardcodes any of them breaks on exactly the project that needed it.
|
|
11
|
+
* This plugin is ALREADY inside the dev server when it boots. It knows the port because it is being
|
|
12
|
+
* served on it, and it knows the URL because Vite reports it. Nothing here is assumed.
|
|
13
|
+
*
|
|
14
|
+
* Writing this file proves one specific thing and no more: the plugin is loaded in the process that
|
|
15
|
+
* is actually running. That is the fact nobody could observe, and the commonest setup failure — a
|
|
16
|
+
* plugin added to a config the running dev server already read — is precisely its absence.
|
|
17
|
+
*
|
|
18
|
+
* Best-effort throughout. This is a diagnostic, and a diagnostic that can break the dev server it
|
|
19
|
+
* reports on is worse than no diagnostic at all.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { devServerRegistryFileName } from '@reticlehq/core';
|
|
24
|
+
import { stateHome } from './state-home.js';
|
|
25
|
+
const JSON_INDENT = 2;
|
|
26
|
+
const nodeIo = {
|
|
27
|
+
mkdir: (dir) => void mkdirSync(dir, { recursive: true }),
|
|
28
|
+
writeFile: (path, data) => void writeFileSync(path, data),
|
|
29
|
+
removeFile: (path) => void rmSync(path, { force: true }),
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Publish the entry; returns the cleanup that withdraws it.
|
|
33
|
+
*
|
|
34
|
+
* The cleanup is idempotent because it is called from shutdown paths — `close`, `SIGINT`, process
|
|
35
|
+
* exit — which fire more than once and race each other. A stale entry is handled on the read side
|
|
36
|
+
* by a liveness check (`liveDevServers`), so a missed cleanup degrades rather than misleads; a
|
|
37
|
+
* throw inside a signal handler does not degrade, it takes the shutdown with it.
|
|
38
|
+
*/
|
|
39
|
+
export function announceDevServer(entry, home = stateHome(), io = nodeIo) {
|
|
40
|
+
const path = join(home, devServerRegistryFileName(entry.port));
|
|
41
|
+
try {
|
|
42
|
+
io.mkdir(home);
|
|
43
|
+
io.writeFile(path, `${JSON.stringify(entry, null, JSON_INDENT)}\n`);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// No home directory, a read-only filesystem, a sandbox. The app still serves; we just cannot say so.
|
|
47
|
+
}
|
|
48
|
+
let withdrawn = false;
|
|
49
|
+
return () => {
|
|
50
|
+
if (withdrawn)
|
|
51
|
+
return;
|
|
52
|
+
withdrawn = true;
|
|
53
|
+
try {
|
|
54
|
+
io.removeFile(path);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Already gone, or never written. The reader's liveness check covers what we cannot remove.
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/discover-port.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* projectId, drop dead daemons — is the pure `pickDaemonPort` in core; this file is just the fs plumbing.
|
|
6
6
|
*/
|
|
7
7
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
8
|
-
import { homedir } from 'node:os';
|
|
9
8
|
import { join } from 'node:path';
|
|
10
|
-
import { daemonRegistryPort, DaemonRegistryEntrySchema, pickDaemonPort,
|
|
9
|
+
import { daemonRegistryPort, DaemonRegistryEntrySchema, pickDaemonPort, } from '@reticlehq/core';
|
|
10
|
+
import { stateHome } from './state-home.js';
|
|
11
11
|
/** process.kill(pid, 0) throws iff the process is gone — the same liveness probe the daemon uses. */
|
|
12
12
|
function isAlive(pid) {
|
|
13
13
|
try {
|
|
@@ -23,7 +23,7 @@ function isAlive(pid) {
|
|
|
23
23
|
* to the default port — never auto-connects to a mismatched daemon). `home` and `alive` are injectable
|
|
24
24
|
* so the selection is unit-tested without a real ~/.reticle or real processes.
|
|
25
25
|
*/
|
|
26
|
-
export function discoverDaemonPort(projectId, home =
|
|
26
|
+
export function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive) {
|
|
27
27
|
const entries = [];
|
|
28
28
|
let files;
|
|
29
29
|
try {
|
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,6 @@ __export(index_exports, {
|
|
|
36
36
|
RETICLE_DEV_MODULE_CANDIDATES: () => RETICLE_DEV_MODULE_CANDIDATES,
|
|
37
37
|
RETICLE_TOKEN_GLOBAL: () => RETICLE_TOKEN_GLOBAL,
|
|
38
38
|
RETICLE_VITE_PLUGIN_NAME: () => RETICLE_VITE_PLUGIN_NAME,
|
|
39
|
-
cjsDepIncludes: () => cjsDepIncludes,
|
|
40
39
|
connectChurnWarning: () => connectChurnWarning,
|
|
41
40
|
connectModuleSource: () => connectModuleSource,
|
|
42
41
|
findDevModule: () => findDevModule,
|
|
@@ -45,7 +44,7 @@ __export(index_exports, {
|
|
|
45
44
|
reticle: () => reticle
|
|
46
45
|
});
|
|
47
46
|
module.exports = __toCommonJS(index_exports);
|
|
48
|
-
var
|
|
47
|
+
var import_node_fs6 = require("node:fs");
|
|
49
48
|
|
|
50
49
|
// src/missing-token.ts
|
|
51
50
|
function missingTokenWarning(token) {
|
|
@@ -79,14 +78,15 @@ function ensurePairingToken(dir) {
|
|
|
79
78
|
|
|
80
79
|
// src/index.ts
|
|
81
80
|
var import_node_os2 = require("node:os");
|
|
82
|
-
var
|
|
83
|
-
var
|
|
81
|
+
var import_node_path8 = require("node:path");
|
|
82
|
+
var import_core6 = require("@babel/core");
|
|
84
83
|
var import_babel_plugin = __toESM(require("@reticlehq/babel-plugin"), 1);
|
|
85
84
|
|
|
86
85
|
// ../core/dist/constants.js
|
|
87
86
|
var RETICLE_DEFAULT_PORT = 4400;
|
|
88
87
|
var RETICLE_WS_PATH = "/reticle";
|
|
89
|
-
|
|
88
|
+
var RETICLE_CLIENT_HOST = "localhost";
|
|
89
|
+
function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
|
|
90
90
|
return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
|
|
91
91
|
}
|
|
92
92
|
var ReticleEnv = {
|
|
@@ -223,6 +223,8 @@ var ReticleDir = {
|
|
|
223
223
|
* questions about the product; this answers the user's question about their own work.
|
|
224
224
|
*/
|
|
225
225
|
IMPACT_FILE: "impact.json",
|
|
226
|
+
/** what changes were SUPPOSED to make true —.reticle/intent.json (git-checked, reviewed) */
|
|
227
|
+
INTENT_FILE: "intent.json",
|
|
226
228
|
/** opt-in pixel baselines —.reticle/visual/<name>.png + <name>.diff.png. */
|
|
227
229
|
VISUAL_SUBDIR: "visual",
|
|
228
230
|
/** verification-run artifacts —.reticle/runs/<runId>.json (the OEM/CI-consumable verdict). */
|
|
@@ -241,6 +243,24 @@ var ReticleDir = {
|
|
|
241
243
|
AMBIENT_FILE: "ambient.json",
|
|
242
244
|
/** per-flow flake ledger — replay outcomes that decide intermittent-failure quarantine. */
|
|
243
245
|
FLAKE_FILE: "flake.json",
|
|
246
|
+
/**
|
|
247
|
+
* the project's cloud binding — .reticle/cloud.json, written by `reticle link`. Git-checked and
|
|
248
|
+
* non-secret: the project id, the API origin, and where its dashboard lives. The KEY lives in
|
|
249
|
+
* ~/.reticle/credentials.json instead, because that one must never reach a repository.
|
|
250
|
+
*/
|
|
251
|
+
CLOUD_LINK_FILE: "cloud.json",
|
|
252
|
+
/**
|
|
253
|
+
* local sync bookkeeping — .reticle/cloud-state.json. The pull cursor, when each half last ran,
|
|
254
|
+
* and the last error. NOT git-checked: it describes this machine's conversation with the server,
|
|
255
|
+
* and committing one machine's cursor would make every other machine skip what it had not seen.
|
|
256
|
+
*/
|
|
257
|
+
CLOUD_STATE_FILE: "cloud-state.json",
|
|
258
|
+
/**
|
|
259
|
+
* triage decisions pulled BACK from the dashboard — .reticle/issues.json. What a human said about
|
|
260
|
+
* a defect ("resolved", "not a bug"), so the HUD stops showing it and the next run does not
|
|
261
|
+
* re-report it as though nobody had looked.
|
|
262
|
+
*/
|
|
263
|
+
ISSUES_FILE: "issues.json",
|
|
244
264
|
/** Per-flow assertion tiers recorded on each PASSING replay — the gate's anti-downgrade baseline. */
|
|
245
265
|
TIERS_FILE: "assertion-tiers.json",
|
|
246
266
|
/**
|
|
@@ -285,6 +305,12 @@ var EventType = {
|
|
|
285
305
|
STORAGE_CHANGE: "storage.change",
|
|
286
306
|
/** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
|
|
287
307
|
PAGE_HEALTH: "page.health",
|
|
308
|
+
/**
|
|
309
|
+
* synthetic: the page called window.open, so the consequence of what was just clicked may live in
|
|
310
|
+
* another browsing context this one cannot observe (an OAuth popup is the archetype).
|
|
311
|
+
* `data: { href }` — the URL the page asked to open, when it named one.
|
|
312
|
+
*/
|
|
313
|
+
CONTEXT_OPENED: "context.opened",
|
|
288
314
|
/** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
|
|
289
315
|
* wasted re-renders show up here without a per-render flood. */
|
|
290
316
|
RENDER_COMMIT: "render.commit",
|
|
@@ -4445,6 +4471,35 @@ function pickDaemonPort(entries, projectId, isAlive2) {
|
|
|
4445
4471
|
return matches[0] ?? null;
|
|
4446
4472
|
}
|
|
4447
4473
|
|
|
4474
|
+
// ../core/dist/dev-server-registry.js
|
|
4475
|
+
var DEV_SERVER_PREFIX = "devserver-";
|
|
4476
|
+
var DEV_SERVER_SUFFIX = ".json";
|
|
4477
|
+
function devServerRegistryFileName(port) {
|
|
4478
|
+
return `${DEV_SERVER_PREFIX}${String(port)}${DEV_SERVER_SUFFIX}`;
|
|
4479
|
+
}
|
|
4480
|
+
var DevServerEntrySchema = external_exports.object({
|
|
4481
|
+
port: external_exports.number(),
|
|
4482
|
+
pid: external_exports.number(),
|
|
4483
|
+
/** The project directory the dev server is serving — how a monorepo tells its apps apart. */
|
|
4484
|
+
root: external_exports.string(),
|
|
4485
|
+
/** Where the app is actually served, as the dev server itself reports it. Never assembled here. */
|
|
4486
|
+
url: external_exports.string(),
|
|
4487
|
+
/**
|
|
4488
|
+
* The SDK version in the bundle, so a skew can be named rather than guessed at.
|
|
4489
|
+
*
|
|
4490
|
+
* Optional, and ABSENT rather than empty when it cannot be resolved. `""` reads as "the version is
|
|
4491
|
+
* empty"; a missing field reads as "not known", which is the true statement — and the difference
|
|
4492
|
+
* matters to the one reader who exists for this field, a skew diagnosis.
|
|
4493
|
+
*/
|
|
4494
|
+
sdkVersion: external_exports.string().min(1).optional(),
|
|
4495
|
+
startedAt: external_exports.number(),
|
|
4496
|
+
/**
|
|
4497
|
+
* Optional because an app can be running before `init` has ever named it — which is exactly the
|
|
4498
|
+
* state this signal has to be able to describe.
|
|
4499
|
+
*/
|
|
4500
|
+
projectId: external_exports.string().optional()
|
|
4501
|
+
});
|
|
4502
|
+
|
|
4448
4503
|
// src/project-id.ts
|
|
4449
4504
|
var import_node_crypto2 = require("node:crypto");
|
|
4450
4505
|
var import_node_path2 = require("node:path");
|
|
@@ -4482,8 +4537,17 @@ function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName) {
|
|
|
4482
4537
|
|
|
4483
4538
|
// src/discover-port.ts
|
|
4484
4539
|
var import_node_fs3 = require("node:fs");
|
|
4540
|
+
var import_node_path4 = require("node:path");
|
|
4541
|
+
|
|
4542
|
+
// src/state-home.ts
|
|
4485
4543
|
var import_node_os = require("node:os");
|
|
4486
4544
|
var import_node_path3 = require("node:path");
|
|
4545
|
+
function stateHome(env = process.env) {
|
|
4546
|
+
const override = env[ReticleEnv.STATE_DIR];
|
|
4547
|
+
return override !== void 0 && override.length > 0 ? override : (0, import_node_path3.join)((0, import_node_os.homedir)(), ReticleDir.ROOT);
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
// src/discover-port.ts
|
|
4487
4551
|
function isAlive(pid) {
|
|
4488
4552
|
try {
|
|
4489
4553
|
process.kill(pid, 0);
|
|
@@ -4492,7 +4556,7 @@ function isAlive(pid) {
|
|
|
4492
4556
|
return false;
|
|
4493
4557
|
}
|
|
4494
4558
|
}
|
|
4495
|
-
function discoverDaemonPort(projectId, home = (
|
|
4559
|
+
function discoverDaemonPort(projectId, home = stateHome(), alive = isAlive) {
|
|
4496
4560
|
const entries = [];
|
|
4497
4561
|
let files;
|
|
4498
4562
|
try {
|
|
@@ -4504,7 +4568,7 @@ function discoverDaemonPort(projectId, home = (0, import_node_path3.join)((0, im
|
|
|
4504
4568
|
if (null === daemonRegistryPort(file)) continue;
|
|
4505
4569
|
try {
|
|
4506
4570
|
const parsed = DaemonRegistryEntrySchema.safeParse(
|
|
4507
|
-
JSON.parse((0, import_node_fs3.readFileSync)((0,
|
|
4571
|
+
JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path4.join)(home, file), "utf8"))
|
|
4508
4572
|
);
|
|
4509
4573
|
if (parsed.success) entries.push(parsed.data);
|
|
4510
4574
|
} catch {
|
|
@@ -4513,9 +4577,37 @@ function discoverDaemonPort(projectId, home = (0, import_node_path3.join)((0, im
|
|
|
4513
4577
|
return pickDaemonPort(entries, projectId, alive) ?? void 0;
|
|
4514
4578
|
}
|
|
4515
4579
|
|
|
4580
|
+
// src/announce.ts
|
|
4581
|
+
var import_node_fs4 = require("node:fs");
|
|
4582
|
+
var import_node_path5 = require("node:path");
|
|
4583
|
+
var JSON_INDENT = 2;
|
|
4584
|
+
var nodeIo = {
|
|
4585
|
+
mkdir: (dir) => void (0, import_node_fs4.mkdirSync)(dir, { recursive: true }),
|
|
4586
|
+
writeFile: (path, data) => void (0, import_node_fs4.writeFileSync)(path, data),
|
|
4587
|
+
removeFile: (path) => void (0, import_node_fs4.rmSync)(path, { force: true })
|
|
4588
|
+
};
|
|
4589
|
+
function announceDevServer(entry, home = stateHome(), io = nodeIo) {
|
|
4590
|
+
const path = (0, import_node_path5.join)(home, devServerRegistryFileName(entry.port));
|
|
4591
|
+
try {
|
|
4592
|
+
io.mkdir(home);
|
|
4593
|
+
io.writeFile(path, `${JSON.stringify(entry, null, JSON_INDENT)}
|
|
4594
|
+
`);
|
|
4595
|
+
} catch {
|
|
4596
|
+
}
|
|
4597
|
+
let withdrawn = false;
|
|
4598
|
+
return () => {
|
|
4599
|
+
if (withdrawn) return;
|
|
4600
|
+
withdrawn = true;
|
|
4601
|
+
try {
|
|
4602
|
+
io.removeFile(path);
|
|
4603
|
+
} catch {
|
|
4604
|
+
}
|
|
4605
|
+
};
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4516
4608
|
// src/svelte-source.ts
|
|
4517
4609
|
var import_node_module = require("node:module");
|
|
4518
|
-
var
|
|
4610
|
+
var import_node_path6 = require("node:path");
|
|
4519
4611
|
var SVELTE_FILE = /\.svelte$/;
|
|
4520
4612
|
var HOST_ELEMENT_TYPES = /* @__PURE__ */ new Set(["RegularElement", "Element"]);
|
|
4521
4613
|
var PARENT_KEY = "parent";
|
|
@@ -4544,7 +4636,7 @@ function offsetToLineColumn(source, offset) {
|
|
|
4544
4636
|
return { line, column: offset - lineStart };
|
|
4545
4637
|
}
|
|
4546
4638
|
function sourcePathFor(id, cwd = process.cwd()) {
|
|
4547
|
-
return (0,
|
|
4639
|
+
return (0, import_node_path6.relative)(cwd, id).replace(/\\/g, "/");
|
|
4548
4640
|
}
|
|
4549
4641
|
function isElementNode(value) {
|
|
4550
4642
|
if (null === value || typeof value !== "object") return false;
|
|
@@ -4598,24 +4690,25 @@ function stampSvelte(code, id, load = defaultLoadCompiler) {
|
|
|
4598
4690
|
}
|
|
4599
4691
|
|
|
4600
4692
|
// src/installed.ts
|
|
4601
|
-
var
|
|
4602
|
-
var
|
|
4693
|
+
var import_node_fs5 = require("node:fs");
|
|
4694
|
+
var import_node_path7 = require("node:path");
|
|
4603
4695
|
var import_node_module2 = require("node:module");
|
|
4604
4696
|
var RETICLE_PACKAGE = "@reticlehq/react";
|
|
4605
4697
|
var MANIFEST_SEARCH_DEPTH = 5;
|
|
4606
4698
|
function requireFromApp(from) {
|
|
4607
|
-
|
|
4699
|
+
const absoluteFrom = (0, import_node_path7.isAbsolute)(from) ? from : (0, import_node_path7.resolve)(process.cwd(), from);
|
|
4700
|
+
return (0, import_node_module2.createRequire)((0, import_node_path7.join)(absoluteFrom, "package.json"));
|
|
4608
4701
|
}
|
|
4609
4702
|
function packageDirOf(specifier, from) {
|
|
4610
4703
|
try {
|
|
4611
|
-
let dir = (0,
|
|
4704
|
+
let dir = (0, import_node_path7.dirname)(requireFromApp(from).resolve(specifier));
|
|
4612
4705
|
for (let up = 0; up < MANIFEST_SEARCH_DEPTH; up++) {
|
|
4613
|
-
const candidate = (0,
|
|
4614
|
-
if ((0,
|
|
4615
|
-
const parsed = JSON.parse((0,
|
|
4706
|
+
const candidate = (0, import_node_path7.join)(dir, "package.json");
|
|
4707
|
+
if ((0, import_node_fs5.existsSync)(candidate)) {
|
|
4708
|
+
const parsed = JSON.parse((0, import_node_fs5.readFileSync)(candidate, "utf8"));
|
|
4616
4709
|
if (parsed.name === specifier) return dir;
|
|
4617
4710
|
}
|
|
4618
|
-
const parent = (0,
|
|
4711
|
+
const parent = (0, import_node_path7.dirname)(dir);
|
|
4619
4712
|
if (parent === dir) break;
|
|
4620
4713
|
dir = parent;
|
|
4621
4714
|
}
|
|
@@ -4640,14 +4733,14 @@ function sdkPackageVersion(from = process.cwd()) {
|
|
|
4640
4733
|
} catch {
|
|
4641
4734
|
}
|
|
4642
4735
|
try {
|
|
4643
|
-
let dir = (0,
|
|
4736
|
+
let dir = (0, import_node_path7.dirname)(require_.resolve(RETICLE_PACKAGE));
|
|
4644
4737
|
for (let up = 0; up < MANIFEST_SEARCH_DEPTH; up++) {
|
|
4645
|
-
const candidate = (0,
|
|
4646
|
-
if ((0,
|
|
4647
|
-
const parsed = JSON.parse((0,
|
|
4738
|
+
const candidate = (0, import_node_path7.join)(dir, "package.json");
|
|
4739
|
+
if ((0, import_node_fs5.existsSync)(candidate)) {
|
|
4740
|
+
const parsed = JSON.parse((0, import_node_fs5.readFileSync)(candidate, "utf8"));
|
|
4648
4741
|
if ("string" === typeof parsed.version) return parsed.version;
|
|
4649
4742
|
}
|
|
4650
|
-
const parent = (0,
|
|
4743
|
+
const parent = (0, import_node_path7.dirname)(dir);
|
|
4651
4744
|
if (parent === dir) break;
|
|
4652
4745
|
dir = parent;
|
|
4653
4746
|
}
|
|
@@ -4658,7 +4751,7 @@ function sdkPackageVersion(from = process.cwd()) {
|
|
|
4658
4751
|
function sdkBuildFingerprint(from = process.cwd()) {
|
|
4659
4752
|
try {
|
|
4660
4753
|
const entry = requireFromApp(from).resolve(RETICLE_PACKAGE);
|
|
4661
|
-
const { size, mtimeMs } = (0,
|
|
4754
|
+
const { size, mtimeMs } = (0, import_node_fs5.statSync)(entry);
|
|
4662
4755
|
return `${String(size)}-${String(Math.trunc(mtimeMs))}`;
|
|
4663
4756
|
} catch {
|
|
4664
4757
|
return "unknown";
|
|
@@ -4744,7 +4837,7 @@ function shouldStampSvelte(id) {
|
|
|
4744
4837
|
return clean !== null && SVELTE_FILE.test(clean);
|
|
4745
4838
|
}
|
|
4746
4839
|
function stamp(code, id) {
|
|
4747
|
-
const out = (0,
|
|
4840
|
+
const out = (0, import_core6.transformSync)(code, {
|
|
4748
4841
|
filename: id,
|
|
4749
4842
|
plugins: [import_babel_plugin.default],
|
|
4750
4843
|
parserOpts: { plugins: ["jsx", "typescript"] },
|
|
@@ -4758,16 +4851,9 @@ function stamp(code, id) {
|
|
|
4758
4851
|
map: out.map === void 0 || null === out.map ? null : JSON.stringify(out.map)
|
|
4759
4852
|
};
|
|
4760
4853
|
}
|
|
4761
|
-
var SDK_CJS_DEPS = {
|
|
4762
|
-
TESTING_LIBRARY: "@testing-library/dom",
|
|
4763
|
-
ARIA_QUERY: "aria-query"
|
|
4764
|
-
};
|
|
4765
|
-
function cjsDepIncludes(appRoot, canResolve = (dep) => null !== resolvableChain([dep], appRoot)) {
|
|
4766
|
-
return [SDK_CJS_DEPS.TESTING_LIBRARY, SDK_CJS_DEPS.ARIA_QUERY].filter(canResolve);
|
|
4767
|
-
}
|
|
4768
4854
|
function readPairingToken() {
|
|
4769
4855
|
const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
|
|
4770
|
-
const dir = override !== void 0 && override.length > 0 ? override : (0,
|
|
4856
|
+
const dir = override !== void 0 && override.length > 0 ? override : (0, import_node_path8.join)((0, import_node_os2.homedir)(), ReticleDir.ROOT);
|
|
4771
4857
|
return ensurePairingToken(dir);
|
|
4772
4858
|
}
|
|
4773
4859
|
var tokenWarned = false;
|
|
@@ -4794,6 +4880,9 @@ function connectArgs(options) {
|
|
|
4794
4880
|
if (true === options.captureNetworkBodies || "1" === process.env["VITE_RETICLE_CAPTURE_BODIES"]) {
|
|
4795
4881
|
args["captureNetworkBodies"] = true;
|
|
4796
4882
|
}
|
|
4883
|
+
if (true === options.exposePresenter || "1" === process.env["VITE_RETICLE_EXPOSE_PRESENTER"]) {
|
|
4884
|
+
args["exposePresenter"] = true;
|
|
4885
|
+
}
|
|
4797
4886
|
if (true === options.allowNonLocalhost || "1" === process.env["VITE_RETICLE_ALLOW_NON_LOCALHOST"]) {
|
|
4798
4887
|
args["allowNonLocalhost"] = true;
|
|
4799
4888
|
}
|
|
@@ -4812,6 +4901,20 @@ function findDevModule(root, exists) {
|
|
|
4812
4901
|
return null;
|
|
4813
4902
|
}
|
|
4814
4903
|
function installedSdk(appRoot, canResolve = (dep) => null !== resolvableChain([dep], appRoot)) {
|
|
4904
|
+
try {
|
|
4905
|
+
const pkgPath = (0, import_node_path8.join)(appRoot, "package.json");
|
|
4906
|
+
if ((0, import_node_fs6.existsSync)(pkgPath)) {
|
|
4907
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)(pkgPath, "utf8"));
|
|
4908
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
4909
|
+
if (deps[RETICLE_SENSOR] !== void 0 && deps[RETICLE_PACKAGE2] === void 0) {
|
|
4910
|
+
return { specifier: RETICLE_SENSOR, usesInstall: false };
|
|
4911
|
+
}
|
|
4912
|
+
if (deps[RETICLE_PACKAGE2] !== void 0) {
|
|
4913
|
+
return { specifier: RETICLE_PACKAGE2, usesInstall: true };
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
} catch {
|
|
4917
|
+
}
|
|
4815
4918
|
if (canResolve(RETICLE_PACKAGE2)) return { specifier: RETICLE_PACKAGE2, usesInstall: true };
|
|
4816
4919
|
if (canResolve(RETICLE_SENSOR)) return { specifier: RETICLE_SENSOR, usesInstall: false };
|
|
4817
4920
|
return { specifier: RETICLE_PACKAGE2, usesInstall: true };
|
|
@@ -4821,9 +4924,11 @@ function connectModuleSource(options, devModule = null) {
|
|
|
4821
4924
|
const sdk = installedSdk(options.root ?? process.cwd());
|
|
4822
4925
|
const named = sdk.usesInstall ? "reticle, install" : "reticle";
|
|
4823
4926
|
const call = sdk.usesInstall ? "install();\n" : "";
|
|
4927
|
+
const hot = `if (import.meta.hot) reticle.observeHotUpdates(import.meta.hot);
|
|
4928
|
+
`;
|
|
4824
4929
|
const base = `import { ${named} } from '${sdk.specifier}';
|
|
4825
4930
|
${call}reticle.connect(${args});
|
|
4826
|
-
`;
|
|
4931
|
+
${hot}`;
|
|
4827
4932
|
return null === devModule ? base : `${base}import('${devModule}');
|
|
4828
4933
|
`;
|
|
4829
4934
|
}
|
|
@@ -4852,10 +4957,10 @@ function reticle(options = {}) {
|
|
|
4852
4957
|
const sdkVersion = withToken.sdkVersion ?? sdkPackageVersion(appRoot);
|
|
4853
4958
|
return { ...withToken, root: appRoot, sdkVersion };
|
|
4854
4959
|
};
|
|
4855
|
-
const currentConnectSource = () => connectModuleSource(resolveLazy(), root === void 0 ? null : findDevModule(root,
|
|
4960
|
+
const currentConnectSource = () => connectModuleSource(resolveLazy(), root === void 0 ? null : findDevModule(root, import_node_fs6.existsSync));
|
|
4856
4961
|
let lastServedConnectSource;
|
|
4857
4962
|
let connectChanges = 0;
|
|
4858
|
-
const 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() yourself.`;
|
|
4963
|
+
const 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.`;
|
|
4859
4964
|
const 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="...">.`;
|
|
4860
4965
|
const checkInjected = () => {
|
|
4861
4966
|
if (!desktop || !inject || injected) return;
|
|
@@ -4869,21 +4974,11 @@ function reticle(options = {}) {
|
|
|
4869
4974
|
...true === options.desktop ? {} : { apply: "serve" },
|
|
4870
4975
|
enforce: "pre",
|
|
4871
4976
|
/**
|
|
4872
|
-
* Declare the SDK
|
|
4977
|
+
* Declare the SDK itself and the optimizer cache fingerprint.
|
|
4873
4978
|
*
|
|
4874
|
-
*
|
|
4875
|
-
*
|
|
4876
|
-
*
|
|
4877
|
-
* export named 'elementRoles'". That takes the WHOLE SDK down before connect() runs, so there is
|
|
4878
|
-
* no session, no Reticle-side error, and only a console SyntaxError naming a package the
|
|
4879
|
-
* developer has never heard of. Measured on the react-admin demo with the SDK aliased to a local
|
|
4880
|
-
* checkout: zero sessions, and it looked like the app was failing to render.
|
|
4881
|
-
*
|
|
4882
|
-
* Declaring them is free when Vite would have found them anyway — but only when they are
|
|
4883
|
-
* actually installed. Naming a package that is not there makes Vite log `Failed to resolve
|
|
4884
|
-
* dependency: …, present in optimizeDeps.include` on every boot, which is a scary line pointing
|
|
4885
|
-
* at Reticle for a problem that does not exist. SvelteKit apps hit exactly that: nothing in that
|
|
4886
|
-
* tree depends on @testing-library/dom.
|
|
4979
|
+
* The browser SDK used to need extra CJS query-engine deps here. It no longer imports that
|
|
4980
|
+
* second accessibility engine, so keeping those names would make Vite pre-bundle packages the
|
|
4981
|
+
* app may not have and blame Reticle for a false `Failed to resolve dependency` warning.
|
|
4887
4982
|
*/
|
|
4888
4983
|
config(config) {
|
|
4889
4984
|
const appRoot = config.root ?? process.cwd();
|
|
@@ -4965,10 +5060,7 @@ function reticle(options = {}) {
|
|
|
4965
5060
|
// Whichever SDK this app actually has: naming `@reticlehq/react` in a Vue app that was
|
|
4966
5061
|
// given the sensor produces the exact boot warning the note below is about, for a
|
|
4967
5062
|
// package that is correctly absent.
|
|
4968
|
-
installedSdk(appRoot).specifier
|
|
4969
|
-
// Only in a form that resolves — see above; a name Vite cannot resolve produces a boot
|
|
4970
|
-
// warning that blames Reticle, and a forced re-optimization on every cold start.
|
|
4971
|
-
...cjsDepIncludes(appRoot)
|
|
5063
|
+
installedSdk(appRoot).specifier
|
|
4972
5064
|
]
|
|
4973
5065
|
}
|
|
4974
5066
|
};
|
|
@@ -5031,6 +5123,30 @@ ${code}`;
|
|
|
5031
5123
|
*/
|
|
5032
5124
|
configureServer(server) {
|
|
5033
5125
|
if (!inject) return;
|
|
5126
|
+
const announce = () => {
|
|
5127
|
+
const address = server.httpServer?.address();
|
|
5128
|
+
const port = "object" === typeof address && null !== address && void 0 !== address ? address.port : void 0;
|
|
5129
|
+
if (void 0 === port) return;
|
|
5130
|
+
const opts = resolveLazy();
|
|
5131
|
+
const withdraw = announceDevServer({
|
|
5132
|
+
port,
|
|
5133
|
+
pid: process.pid,
|
|
5134
|
+
root: opts.root ?? process.cwd(),
|
|
5135
|
+
url: server.resolvedUrls?.local[0] ?? `http://localhost:${String(port)}/`,
|
|
5136
|
+
...opts.sdkVersion === void 0 || 0 === opts.sdkVersion.length ? {} : { sdkVersion: opts.sdkVersion },
|
|
5137
|
+
startedAt: Date.now(),
|
|
5138
|
+
...opts.projectId === void 0 ? {} : { projectId: opts.projectId }
|
|
5139
|
+
});
|
|
5140
|
+
server.httpServer?.once("close", withdraw);
|
|
5141
|
+
process.once("exit", withdraw);
|
|
5142
|
+
process.once("SIGINT", withdraw);
|
|
5143
|
+
process.once("SIGTERM", withdraw);
|
|
5144
|
+
};
|
|
5145
|
+
if (null === server.httpServer?.address() || void 0 === server.httpServer?.address()) {
|
|
5146
|
+
server.httpServer?.once("listening", announce);
|
|
5147
|
+
} else {
|
|
5148
|
+
announce();
|
|
5149
|
+
}
|
|
5034
5150
|
server.middlewares.use((req, _res, next) => {
|
|
5035
5151
|
if ((req.url ?? "").split("?")[0] === RETICLE_CONNECT_MODULE) {
|
|
5036
5152
|
if (currentConnectSource() !== lastServedConnectSource) {
|
|
@@ -5082,7 +5198,6 @@ ${code}`;
|
|
|
5082
5198
|
RETICLE_DEV_MODULE_CANDIDATES,
|
|
5083
5199
|
RETICLE_TOKEN_GLOBAL,
|
|
5084
5200
|
RETICLE_VITE_PLUGIN_NAME,
|
|
5085
|
-
cjsDepIncludes,
|
|
5086
5201
|
connectChurnWarning,
|
|
5087
5202
|
connectModuleSource,
|
|
5088
5203
|
findDevModule,
|
package/dist/index.d.cts
CHANGED
|
@@ -84,6 +84,20 @@ export interface ReticleVitePluginOptions {
|
|
|
84
84
|
* session without editing vite.config.
|
|
85
85
|
*/
|
|
86
86
|
captureNetworkBodies?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
|
|
89
|
+
*
|
|
90
|
+
* Reachable here for the same reason `captureNetworkBodies` is: the plugin is the only `connect()`
|
|
91
|
+
* most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
92
|
+
*
|
|
93
|
+
* The presenter is hidden from every tool by design — an agent that can drive Reticle's own
|
|
94
|
+
* interface can fabricate its own impact report. The cost is that a HUD change is the only kind of
|
|
95
|
+
* change Reticle cannot be used to check. This is the hatch for that one case, and the app reports
|
|
96
|
+
* it in its capabilities so a verdict drawn with it open is never mistaken for an ordinary one.
|
|
97
|
+
*
|
|
98
|
+
* Also settable as `VITE_RETICLE_EXPOSE_PRESENTER=1`.
|
|
99
|
+
*/
|
|
100
|
+
exposePresenter?: boolean;
|
|
87
101
|
/**
|
|
88
102
|
* Let Reticle run when the page or the bridge is not on localhost.
|
|
89
103
|
*
|
|
@@ -171,6 +185,26 @@ export interface ReticleVitePlugin {
|
|
|
171
185
|
* plugin never imports, the same way the Svelte compiler and Playwright are handled elsewhere.
|
|
172
186
|
*/
|
|
173
187
|
export interface ViteDevServerLike {
|
|
188
|
+
/**
|
|
189
|
+
* Vite's own HTTP server, for the port it ACTUALLY bound and the moment it bound it. Optional and
|
|
190
|
+
* nullable because middleware mode has none — and because a structural stand-in that demands more
|
|
191
|
+
* than Vite guarantees stops being assignable, which red-builds every typechecked config.
|
|
192
|
+
*/
|
|
193
|
+
httpServer?: {
|
|
194
|
+
once(event: string, listener: () => void): unknown;
|
|
195
|
+
address(): string | {
|
|
196
|
+
port: number;
|
|
197
|
+
} | null;
|
|
198
|
+
} | null;
|
|
199
|
+
/**
|
|
200
|
+
* The URLs Vite prints on boot. Read rather than assembled: host, protocol and base are all
|
|
201
|
+
* configurable, so composing a URL here would be a guess about the one thing the dev server can
|
|
202
|
+
* simply be asked.
|
|
203
|
+
*/
|
|
204
|
+
resolvedUrls?: {
|
|
205
|
+
local: string[];
|
|
206
|
+
network: string[];
|
|
207
|
+
} | null;
|
|
174
208
|
middlewares: {
|
|
175
209
|
use(handler: (req: {
|
|
176
210
|
url?: string | undefined;
|
|
@@ -191,28 +225,11 @@ interface HtmlTag {
|
|
|
191
225
|
injectTo: 'body' | 'head-prepend';
|
|
192
226
|
}
|
|
193
227
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* segment by segment, and under pnpm that succeeds exactly where Vite fails. Measured on the
|
|
199
|
-
* sveltekit fixture:
|
|
200
|
-
*
|
|
201
|
-
* ['@testing-library/dom'] -> null
|
|
202
|
-
* ['@reticlehq/browser', '@testing-library/dom'] -> null
|
|
203
|
-
* ['@reticlehq/react', '@reticlehq/browser', '@testing-library/dom'] -> emitted
|
|
204
|
-
*
|
|
205
|
-
* So we emitted the three-segment chain, Vite could not follow it, and the boot warning this
|
|
206
|
-
* function exists to prevent appeared anyway — `Failed to resolve dependency: …, present in
|
|
207
|
-
* optimizeDeps.include`, pointing at Reticle, naming a package the developer has never heard of,
|
|
208
|
-
* and forcing a full re-optimization on every cold start. The comment stated the rule correctly and
|
|
209
|
-
* the code broke it.
|
|
210
|
-
*
|
|
211
|
-
* Dropping the nested form loses nothing that matters: the SDK itself is still pre-bundled, and Vite
|
|
212
|
-
* follows its imports when it does that, so these deps are handled as part of it. Naming them
|
|
213
|
-
* separately was belt-and-braces for a locally-aliased SDK, where the bare form resolves anyway.
|
|
228
|
+
* Read the daemon's auto-provisioned pairing token (~/.reticle/pairing-token, or the
|
|
229
|
+
* RETICLE_PAIRING_TOKEN_DIR override) so the served app can present it. Node-side only — a browser
|
|
230
|
+
* sandbox can't read the file, which is exactly why a rogue localhost app can't forge it. Best-effort:
|
|
231
|
+
* undefined if the daemon hasn't started yet (the page reloads once it has). Exported for testing.
|
|
214
232
|
*/
|
|
215
|
-
export declare function cjsDepIncludes(appRoot: string, canResolve?: (dep: string) => boolean): string[];
|
|
216
233
|
export declare function readPairingToken(): string | undefined;
|
|
217
234
|
/** The body of the connect module — real imports, resolved by Vite when the module is served. */
|
|
218
235
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -84,6 +84,20 @@ export interface ReticleVitePluginOptions {
|
|
|
84
84
|
* session without editing vite.config.
|
|
85
85
|
*/
|
|
86
86
|
captureNetworkBodies?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
|
|
89
|
+
*
|
|
90
|
+
* Reachable here for the same reason `captureNetworkBodies` is: the plugin is the only `connect()`
|
|
91
|
+
* most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
|
|
92
|
+
*
|
|
93
|
+
* The presenter is hidden from every tool by design — an agent that can drive Reticle's own
|
|
94
|
+
* interface can fabricate its own impact report. The cost is that a HUD change is the only kind of
|
|
95
|
+
* change Reticle cannot be used to check. This is the hatch for that one case, and the app reports
|
|
96
|
+
* it in its capabilities so a verdict drawn with it open is never mistaken for an ordinary one.
|
|
97
|
+
*
|
|
98
|
+
* Also settable as `VITE_RETICLE_EXPOSE_PRESENTER=1`.
|
|
99
|
+
*/
|
|
100
|
+
exposePresenter?: boolean;
|
|
87
101
|
/**
|
|
88
102
|
* Let Reticle run when the page or the bridge is not on localhost.
|
|
89
103
|
*
|
|
@@ -171,6 +185,26 @@ export interface ReticleVitePlugin {
|
|
|
171
185
|
* plugin never imports, the same way the Svelte compiler and Playwright are handled elsewhere.
|
|
172
186
|
*/
|
|
173
187
|
export interface ViteDevServerLike {
|
|
188
|
+
/**
|
|
189
|
+
* Vite's own HTTP server, for the port it ACTUALLY bound and the moment it bound it. Optional and
|
|
190
|
+
* nullable because middleware mode has none — and because a structural stand-in that demands more
|
|
191
|
+
* than Vite guarantees stops being assignable, which red-builds every typechecked config.
|
|
192
|
+
*/
|
|
193
|
+
httpServer?: {
|
|
194
|
+
once(event: string, listener: () => void): unknown;
|
|
195
|
+
address(): string | {
|
|
196
|
+
port: number;
|
|
197
|
+
} | null;
|
|
198
|
+
} | null;
|
|
199
|
+
/**
|
|
200
|
+
* The URLs Vite prints on boot. Read rather than assembled: host, protocol and base are all
|
|
201
|
+
* configurable, so composing a URL here would be a guess about the one thing the dev server can
|
|
202
|
+
* simply be asked.
|
|
203
|
+
*/
|
|
204
|
+
resolvedUrls?: {
|
|
205
|
+
local: string[];
|
|
206
|
+
network: string[];
|
|
207
|
+
} | null;
|
|
174
208
|
middlewares: {
|
|
175
209
|
use(handler: (req: {
|
|
176
210
|
url?: string | undefined;
|
|
@@ -191,28 +225,11 @@ interface HtmlTag {
|
|
|
191
225
|
injectTo: 'body' | 'head-prepend';
|
|
192
226
|
}
|
|
193
227
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* segment by segment, and under pnpm that succeeds exactly where Vite fails. Measured on the
|
|
199
|
-
* sveltekit fixture:
|
|
200
|
-
*
|
|
201
|
-
* ['@testing-library/dom'] -> null
|
|
202
|
-
* ['@reticlehq/browser', '@testing-library/dom'] -> null
|
|
203
|
-
* ['@reticlehq/react', '@reticlehq/browser', '@testing-library/dom'] -> emitted
|
|
204
|
-
*
|
|
205
|
-
* So we emitted the three-segment chain, Vite could not follow it, and the boot warning this
|
|
206
|
-
* function exists to prevent appeared anyway — `Failed to resolve dependency: …, present in
|
|
207
|
-
* optimizeDeps.include`, pointing at Reticle, naming a package the developer has never heard of,
|
|
208
|
-
* and forcing a full re-optimization on every cold start. The comment stated the rule correctly and
|
|
209
|
-
* the code broke it.
|
|
210
|
-
*
|
|
211
|
-
* Dropping the nested form loses nothing that matters: the SDK itself is still pre-bundled, and Vite
|
|
212
|
-
* follows its imports when it does that, so these deps are handled as part of it. Naming them
|
|
213
|
-
* separately was belt-and-braces for a locally-aliased SDK, where the bare form resolves anyway.
|
|
228
|
+
* Read the daemon's auto-provisioned pairing token (~/.reticle/pairing-token, or the
|
|
229
|
+
* RETICLE_PAIRING_TOKEN_DIR override) so the served app can present it. Node-side only — a browser
|
|
230
|
+
* sandbox can't read the file, which is exactly why a rogue localhost app can't forge it. Best-effort:
|
|
231
|
+
* undefined if the daemon hasn't started yet (the page reloads once it has). Exported for testing.
|
|
214
232
|
*/
|
|
215
|
-
export declare function cjsDepIncludes(appRoot: string, canResolve?: (dep: string) => boolean): string[];
|
|
216
233
|
export declare function readPairingToken(): string | undefined;
|
|
217
234
|
/** The body of the connect module — real imports, resolved by Vite when the module is served. */
|
|
218
235
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { missingTokenWarning } from './missing-token.js';
|
|
3
3
|
import { ensurePairingToken } from './ensure-token.js';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
@@ -8,6 +8,7 @@ import reticleSource from '@reticlehq/babel-plugin';
|
|
|
8
8
|
import { RETICLE_DEFAULT_PORT, RETICLE_RENDER_PREHOOK, bridgeWsUrl, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
|
|
9
9
|
import { resolveProjectId } from './project-id.js';
|
|
10
10
|
import { discoverDaemonPort } from './discover-port.js';
|
|
11
|
+
import { announceDevServer } from './announce.js';
|
|
11
12
|
import { SVELTE_FILE, stampSvelte } from './svelte-source.js';
|
|
12
13
|
import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, optimizerOptions, } from './installed.js';
|
|
13
14
|
export const RETICLE_VITE_PLUGIN_NAME = 'reticle';
|
|
@@ -150,43 +151,6 @@ function stamp(code, id) {
|
|
|
150
151
|
* sandbox can't read the file, which is exactly why a rogue localhost app can't forge it. Best-effort:
|
|
151
152
|
* undefined if the daemon hasn't started yet (the page reloads once it has). Exported for testing.
|
|
152
153
|
*/
|
|
153
|
-
/**
|
|
154
|
-
* CJS packages the browser SDK needs at runtime. Named, because a bare string here is exactly the
|
|
155
|
-
* kind of thing that silently rots when a dependency is renamed.
|
|
156
|
-
*/
|
|
157
|
-
const SDK_CJS_DEPS = {
|
|
158
|
-
TESTING_LIBRARY: '@testing-library/dom',
|
|
159
|
-
ARIA_QUERY: 'aria-query',
|
|
160
|
-
};
|
|
161
|
-
/**
|
|
162
|
-
* Name each CJS dep ONLY in the bare form, and only when the app root can resolve it.
|
|
163
|
-
*
|
|
164
|
-
* This used to try three layouts and emit Vite's nested `a > b > c` form when a hoisted lookup
|
|
165
|
-
* failed. The guard asked the wrong question: it tested NODE resolvability, walking the chain
|
|
166
|
-
* segment by segment, and under pnpm that succeeds exactly where Vite fails. Measured on the
|
|
167
|
-
* sveltekit fixture:
|
|
168
|
-
*
|
|
169
|
-
* ['@testing-library/dom'] -> null
|
|
170
|
-
* ['@reticlehq/browser', '@testing-library/dom'] -> null
|
|
171
|
-
* ['@reticlehq/react', '@reticlehq/browser', '@testing-library/dom'] -> emitted
|
|
172
|
-
*
|
|
173
|
-
* So we emitted the three-segment chain, Vite could not follow it, and the boot warning this
|
|
174
|
-
* function exists to prevent appeared anyway — `Failed to resolve dependency: …, present in
|
|
175
|
-
* optimizeDeps.include`, pointing at Reticle, naming a package the developer has never heard of,
|
|
176
|
-
* and forcing a full re-optimization on every cold start. The comment stated the rule correctly and
|
|
177
|
-
* the code broke it.
|
|
178
|
-
*
|
|
179
|
-
* Dropping the nested form loses nothing that matters: the SDK itself is still pre-bundled, and Vite
|
|
180
|
-
* follows its imports when it does that, so these deps are handled as part of it. Naming them
|
|
181
|
-
* separately was belt-and-braces for a locally-aliased SDK, where the bare form resolves anyway.
|
|
182
|
-
*/
|
|
183
|
-
export function cjsDepIncludes(appRoot,
|
|
184
|
-
// Injected so the rule can be tested hermetically. Real module resolution is not: under vitest,
|
|
185
|
-
// `createRequire` from a directory that does not exist still resolves packages out of the runner's
|
|
186
|
-
// own graph, so a filesystem-based test of "nothing is reachable here" silently asserts nothing.
|
|
187
|
-
canResolve = (dep) => null !== resolvableChain([dep], appRoot)) {
|
|
188
|
-
return [SDK_CJS_DEPS.TESTING_LIBRARY, SDK_CJS_DEPS.ARIA_QUERY].filter(canResolve);
|
|
189
|
-
}
|
|
190
154
|
export function readPairingToken() {
|
|
191
155
|
const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
|
|
192
156
|
const dir = override !== undefined && override.length > 0 ? override : join(homedir(), ReticleDir.ROOT);
|
|
@@ -243,6 +207,10 @@ function connectArgs(options) {
|
|
|
243
207
|
if (true === options.captureNetworkBodies || '1' === process.env['VITE_RETICLE_CAPTURE_BODIES']) {
|
|
244
208
|
args['captureNetworkBodies'] = true;
|
|
245
209
|
}
|
|
210
|
+
// Same shape, same reason. Off unless asked for, in a config or for one session.
|
|
211
|
+
if (true === options.exposePresenter || '1' === process.env['VITE_RETICLE_EXPOSE_PRESENTER']) {
|
|
212
|
+
args['exposePresenter'] = true;
|
|
213
|
+
}
|
|
246
214
|
// Same shape, same reason: without it an app that cannot be served on localhost has no way to
|
|
247
215
|
// reach the SDK option at all. The pairing token still applies — see the option's docstring.
|
|
248
216
|
if (true === options.allowNonLocalhost ||
|
|
@@ -292,6 +260,22 @@ export function findDevModule(root, exists) {
|
|
|
292
260
|
* missing export.
|
|
293
261
|
*/
|
|
294
262
|
export function installedSdk(appRoot, canResolve = (dep) => null !== resolvableChain([dep], appRoot)) {
|
|
263
|
+
try {
|
|
264
|
+
const pkgPath = join(appRoot, 'package.json');
|
|
265
|
+
if (existsSync(pkgPath)) {
|
|
266
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
267
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
268
|
+
if (deps[RETICLE_SENSOR] !== undefined && deps[RETICLE_PACKAGE] === undefined) {
|
|
269
|
+
return { specifier: RETICLE_SENSOR, usesInstall: false };
|
|
270
|
+
}
|
|
271
|
+
if (deps[RETICLE_PACKAGE] !== undefined) {
|
|
272
|
+
return { specifier: RETICLE_PACKAGE, usesInstall: true };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Ignore read or parse failures when checking dependencies
|
|
278
|
+
}
|
|
295
279
|
if (canResolve(RETICLE_PACKAGE))
|
|
296
280
|
return { specifier: RETICLE_PACKAGE, usesInstall: true };
|
|
297
281
|
if (canResolve(RETICLE_SENSOR))
|
|
@@ -305,7 +289,13 @@ export function connectModuleSource(options, devModule = null) {
|
|
|
305
289
|
const sdk = installedSdk(options.root ?? process.cwd());
|
|
306
290
|
const named = sdk.usesInstall ? 'reticle, install' : 'reticle';
|
|
307
291
|
const call = sdk.usesInstall ? 'install();\n' : '';
|
|
308
|
-
|
|
292
|
+
// The only place in a Vite app that can see hot updates. `import.meta.hot` exists per module and
|
|
293
|
+
// only for modules Vite serves through its own transform; the SDK is a dependency, pre-bundled by
|
|
294
|
+
// the optimizer, and gets no hot context however it is written — so the channel is handed to it
|
|
295
|
+
// from here and the SDK owns everything after that (including which events it cares about).
|
|
296
|
+
// Guarded, because a desktop build has no dev server and therefore no hot context at all.
|
|
297
|
+
const hot = `if (import.meta.hot) reticle.observeHotUpdates(import.meta.hot);\n`;
|
|
298
|
+
const base = `import { ${named} } from '${sdk.specifier}';\n${call}reticle.connect(${args});\n${hot}`;
|
|
309
299
|
// AFTER connect: registerStore subscribes through the live SDK, and registering before there is a
|
|
310
300
|
// session to report into drops the first diffs.
|
|
311
301
|
return null === devModule ? base : `${base}import('${devModule}');\n`;
|
|
@@ -390,7 +380,8 @@ export function reticle(options = {}) {
|
|
|
390
380
|
const notInjectedMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not inject reticle.connect(): the HTML entry module was ` +
|
|
391
381
|
'never matched, so this app carries no instrumentation and will never connect. Check that ' +
|
|
392
382
|
'index.html references your entry with a <script type="module" src="...">, or pass ' +
|
|
393
|
-
'`inject: false` and call reticle.connect() yourself.'
|
|
383
|
+
'`inject: false` and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself. The plugin ' +
|
|
384
|
+
'still inlines that define; a connect without it is refused.';
|
|
394
385
|
/**
|
|
395
386
|
* The DEV message, which must be weaker — and this is the whole reason the two are separate.
|
|
396
387
|
*
|
|
@@ -422,21 +413,11 @@ export function reticle(options = {}) {
|
|
|
422
413
|
...(true === options.desktop ? {} : { apply: 'serve' }),
|
|
423
414
|
enforce: 'pre',
|
|
424
415
|
/**
|
|
425
|
-
* Declare the SDK
|
|
416
|
+
* Declare the SDK itself and the optimizer cache fingerprint.
|
|
426
417
|
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* export named 'elementRoles'". That takes the WHOLE SDK down before connect() runs, so there is
|
|
431
|
-
* no session, no Reticle-side error, and only a console SyntaxError naming a package the
|
|
432
|
-
* developer has never heard of. Measured on the react-admin demo with the SDK aliased to a local
|
|
433
|
-
* checkout: zero sessions, and it looked like the app was failing to render.
|
|
434
|
-
*
|
|
435
|
-
* Declaring them is free when Vite would have found them anyway — but only when they are
|
|
436
|
-
* actually installed. Naming a package that is not there makes Vite log `Failed to resolve
|
|
437
|
-
* dependency: …, present in optimizeDeps.include` on every boot, which is a scary line pointing
|
|
438
|
-
* at Reticle for a problem that does not exist. SvelteKit apps hit exactly that: nothing in that
|
|
439
|
-
* tree depends on @testing-library/dom.
|
|
418
|
+
* The browser SDK used to need extra CJS query-engine deps here. It no longer imports that
|
|
419
|
+
* second accessibility engine, so keeping those names would make Vite pre-bundle packages the
|
|
420
|
+
* app may not have and blame Reticle for a false `Failed to resolve dependency` warning.
|
|
440
421
|
*/
|
|
441
422
|
config(config) {
|
|
442
423
|
// Everything below asks what the APP has installed, so every lookup is rooted here and never
|
|
@@ -517,9 +498,6 @@ export function reticle(options = {}) {
|
|
|
517
498
|
// given the sensor produces the exact boot warning the note below is about, for a
|
|
518
499
|
// package that is correctly absent.
|
|
519
500
|
installedSdk(appRoot).specifier,
|
|
520
|
-
// Only in a form that resolves — see above; a name Vite cannot resolve produces a boot
|
|
521
|
-
// warning that blames Reticle, and a forced re-optimization on every cold start.
|
|
522
|
-
...cjsDepIncludes(appRoot),
|
|
523
501
|
],
|
|
524
502
|
},
|
|
525
503
|
};
|
|
@@ -600,6 +578,50 @@ export function reticle(options = {}) {
|
|
|
600
578
|
configureServer(server) {
|
|
601
579
|
if (!inject)
|
|
602
580
|
return;
|
|
581
|
+
// Tell `~/.reticle` this dev server exists, the moment it is actually listening.
|
|
582
|
+
//
|
|
583
|
+
// This is the one fact nobody outside this process could observe: the plugin is loaded in the
|
|
584
|
+
// dev server that is RUNNING, not merely present in a config file on disk. Its absence is the
|
|
585
|
+
// commonest setup failure there is — a plugin added to a config the running server already
|
|
586
|
+
// read — and until now that failure was indistinguishable from every other one.
|
|
587
|
+
//
|
|
588
|
+
// Deliberately reads the port and URL off the server rather than composing them. The port may
|
|
589
|
+
// be `strictPort: false` and have moved, the host and base are configurable, and every one of
|
|
590
|
+
// those is something the user can change under us.
|
|
591
|
+
const announce = () => {
|
|
592
|
+
const address = server.httpServer?.address();
|
|
593
|
+
const port = 'object' === typeof address && null !== address && undefined !== address
|
|
594
|
+
? address.port
|
|
595
|
+
: undefined;
|
|
596
|
+
if (undefined === port)
|
|
597
|
+
return;
|
|
598
|
+
const opts = resolveLazy();
|
|
599
|
+
const withdraw = announceDevServer({
|
|
600
|
+
port,
|
|
601
|
+
pid: process.pid,
|
|
602
|
+
root: opts.root ?? process.cwd(),
|
|
603
|
+
url: server.resolvedUrls?.local[0] ?? `http://localhost:${String(port)}/`,
|
|
604
|
+
...(opts.sdkVersion === undefined || 0 === opts.sdkVersion.length
|
|
605
|
+
? {}
|
|
606
|
+
: { sdkVersion: opts.sdkVersion }),
|
|
607
|
+
startedAt: Date.now(),
|
|
608
|
+
...(opts.projectId === undefined ? {} : { projectId: opts.projectId }),
|
|
609
|
+
});
|
|
610
|
+
server.httpServer?.once('close', withdraw);
|
|
611
|
+
// `close` does not fire on Ctrl-C, which is how a dev server usually dies. The read side
|
|
612
|
+
// checks liveness anyway, so a missed withdrawal degrades rather than lies — these just
|
|
613
|
+
// keep the directory tidy in the cases we can catch.
|
|
614
|
+
process.once('exit', withdraw);
|
|
615
|
+
process.once('SIGINT', withdraw);
|
|
616
|
+
process.once('SIGTERM', withdraw);
|
|
617
|
+
};
|
|
618
|
+
// Already bound in some setups (middleware mode, a restart), not yet in the common one.
|
|
619
|
+
if (null === server.httpServer?.address() || undefined === server.httpServer?.address()) {
|
|
620
|
+
server.httpServer?.once('listening', announce);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
announce();
|
|
624
|
+
}
|
|
603
625
|
server.middlewares.use((req, _res, next) => {
|
|
604
626
|
if ((req.url ?? '').split('?')[0] === RETICLE_CONNECT_MODULE) {
|
|
605
627
|
if (currentConnectSource() !== lastServedConnectSource) {
|
package/dist/installed.d.ts
CHANGED
|
@@ -24,10 +24,8 @@ export declare function isResolvable(specifier: string, from?: string): boolean;
|
|
|
24
24
|
*
|
|
25
25
|
* A single-segment chain is just the bare specifier, so this also answers "can the app see it
|
|
26
26
|
* itself". Each further segment is resolved from the PREVIOUS package's directory, which is what
|
|
27
|
-
* Vite does with the `>` form and the only way to name a dependency the app root cannot see
|
|
28
|
-
* pnpm
|
|
29
|
-
* user's app. Naming it bare made Vite fail to resolve it and force a re-optimization on every cold
|
|
30
|
-
* boot. SvelteKit ships `svelte > clsx` for the same reason.
|
|
27
|
+
* Vite does with the `>` form and the only way to name a dependency the app root cannot see under
|
|
28
|
+
* pnpm or npm's nested layout. SvelteKit ships `svelte > clsx` for the same reason.
|
|
31
29
|
*/
|
|
32
30
|
export declare function resolvableChain(chain: readonly string[], from: string): string | null;
|
|
33
31
|
/** The installed SDK's package version, for the HELLO's `sdkVersion`. Node-side only. */
|
package/dist/installed.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Silently, for every pnpm user, with no error anywhere.
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
18
|
-
import { dirname, join } from 'node:path';
|
|
18
|
+
import { dirname, join, isAbsolute, resolve } from 'node:path';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
/** The React kit the host app imports the SDK from. Mirrors the constant in index.ts. */
|
|
21
21
|
const RETICLE_PACKAGE = '@reticlehq/react';
|
|
@@ -26,7 +26,8 @@ const MANIFEST_SEARCH_DEPTH = 5;
|
|
|
26
26
|
* `process.cwd()` default matches how Vite itself defaults the root when the config omits it.
|
|
27
27
|
*/
|
|
28
28
|
function requireFromApp(from) {
|
|
29
|
-
|
|
29
|
+
const absoluteFrom = isAbsolute(from) ? from : resolve(process.cwd(), from);
|
|
30
|
+
return createRequire(join(absoluteFrom, 'package.json'));
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* Whether a package can be resolved from the app. Used to avoid declaring an optimizeDeps entry for
|
|
@@ -74,10 +75,8 @@ function packageDirOf(specifier, from) {
|
|
|
74
75
|
*
|
|
75
76
|
* A single-segment chain is just the bare specifier, so this also answers "can the app see it
|
|
76
77
|
* itself". Each further segment is resolved from the PREVIOUS package's directory, which is what
|
|
77
|
-
* Vite does with the `>` form and the only way to name a dependency the app root cannot see
|
|
78
|
-
* pnpm
|
|
79
|
-
* user's app. Naming it bare made Vite fail to resolve it and force a re-optimization on every cold
|
|
80
|
-
* boot. SvelteKit ships `svelte > clsx` for the same reason.
|
|
78
|
+
* Vite does with the `>` form and the only way to name a dependency the app root cannot see under
|
|
79
|
+
* pnpm or npm's nested layout. SvelteKit ships `svelte > clsx` for the same reason.
|
|
81
80
|
*/
|
|
82
81
|
export function resolvableChain(chain, from) {
|
|
83
82
|
let base = from;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stateHome(env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Reticle keeps machine state — the one answer, for both halves of discovery.
|
|
3
|
+
*
|
|
4
|
+
* `RETICLE_STATE_DIR` already exists and already relocates the daemon's pidfiles, logs and discovery
|
|
5
|
+
* registry (a read-only `$HOME` — sandboxed agent, locked-down Windows profile, container — makes
|
|
6
|
+
* the default unwritable). The plugin's reader did not honour it: with the variable set, the daemon
|
|
7
|
+
* wrote its registry to one directory while `discoverDaemonPort` looked in another, so discovery
|
|
8
|
+
* silently found nothing and every app fell back to the default port. Exactly the class of split
|
|
9
|
+
* this file exists to prevent — two halves of one mechanism with two ideas of where it lives.
|
|
10
|
+
*/
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { ReticleDir, ReticleEnv } from '@reticlehq/core';
|
|
14
|
+
export function stateHome(env = process.env) {
|
|
15
|
+
const override = env[ReticleEnv.STATE_DIR];
|
|
16
|
+
return override !== undefined && override.length > 0
|
|
17
|
+
? override
|
|
18
|
+
: join(homedir(), ReticleDir.ROOT);
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/vite-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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,14 +41,14 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@babel/core": "^7.26.0",
|
|
44
|
-
"@reticlehq/
|
|
45
|
-
"@reticlehq/
|
|
44
|
+
"@reticlehq/core": "2.13.0",
|
|
45
|
+
"@reticlehq/babel-plugin": "2.13.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/babel__core": "^7.20.5",
|
|
49
|
-
"svelte": "^5.56.
|
|
49
|
+
"svelte": "^5.56.10",
|
|
50
50
|
"vite": "^8",
|
|
51
|
-
"esbuild": "^0.28.
|
|
51
|
+
"esbuild": "^0.28.2"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"vite": ">=4"
|