@reticlehq/vite-plugin 3.5.0 → 3.7.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.
@@ -6,6 +6,7 @@
6
6
  * of the plugin's options the PAGE needs to be told about.
7
7
  */
8
8
  import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
9
+ const PRESENT_OPTION = 'present';
9
10
  /** Build the `reticle.connect` argument literal — only includes keys the user set. */
10
11
  export function connectArgs(options) {
11
12
  const args = {};
@@ -18,6 +19,9 @@ export function connectArgs(options) {
18
19
  args['projectId'] = options.projectId;
19
20
  if (options.token !== undefined)
20
21
  args['token'] = options.token;
22
+ // Only the opt-out is needed; the SDK defaults to showing its presenter.
23
+ if (false === options.present)
24
+ args[PRESENT_OPTION] = false;
21
25
  // Passed as connect ARGUMENTS, not as a `define`. A define substitutes a bare identifier in the
22
26
  // source it transforms; the SDK reads these as `globalThis[NAME]`, a dynamic lookup no define can
23
27
  // ever reach — so defining them looked right, shipped, and did nothing. Baking them into the
@@ -67,5 +71,8 @@ export function connectArgs(options) {
67
71
  '1' === process.env['VITE_RETICLE_ALLOW_NON_LOCALHOST']) {
68
72
  args['allowNonLocalhost'] = true;
69
73
  }
74
+ if (options.testIdAttribute !== undefined && options.testIdAttribute.length > 0) {
75
+ args['testIdAttribute'] = options.testIdAttribute;
76
+ }
70
77
  return Object.keys(args).length > 0 ? JSON.stringify(args) : '';
71
78
  }
@@ -12,4 +12,4 @@ export declare const RETICLE_DISABLED_STUB = "\0reticle-disabled-stub";
12
12
  * production, because the cost of guessing wrong the other way is an instrumented release.
13
13
  */
14
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";
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\n// It wraps the APP's write, so the write still runs: Reticle does nothing in production, the app does.\nexport function commitAndSignal(_emitter, mutate) {\n return mutate();\n}\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";
@@ -111,7 +111,10 @@ export function createReticleEmitter() {
111
111
  };
112
112
  }
113
113
 
114
- export function commitAndSignal() {}
114
+ // It wraps the APP's write, so the write still runs: Reticle does nothing in production, the app does.
115
+ export function commitAndSignal(_emitter, mutate) {
116
+ return mutate();
117
+ }
115
118
  export function registerReticleDomain() {}
116
119
 
117
120
  export function buildSnapshot() {
package/dist/index.cjs CHANGED
@@ -4243,6 +4243,9 @@ function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
4243
4243
  return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
4244
4244
  }
4245
4245
  var ReticleEnv = {
4246
+ /** "0"/"false"/"off" turns off telemetry and every optional outbound call; DO_NOT_TRACK too. */
4247
+ TELEMETRY: "RETICLE_TELEMETRY",
4248
+ DO_NOT_TRACK: "DO_NOT_TRACK",
4246
4249
  /** Shared-secret the browser SDK must present in HELLO; absent ⇒ loopback-trust only. */
4247
4250
  TOKEN: "RETICLE_TOKEN",
4248
4251
  /** Bridge bind host. Defaults to loopback; setting anything else is opt-in remote exposure. */
@@ -4257,6 +4260,8 @@ var ReticleEnv = {
4257
4260
  STATE_DIR: "RETICLE_STATE_DIR",
4258
4261
  /** Attach to an already-running browser over CDP instead of launching one. */
4259
4262
  CDP_URL: "RETICLE_CDP_URL",
4263
+ /** The Chromium executable to launch, in place of Playwright's own build or an installed Chrome. */
4264
+ CHROMIUM_PATH: "RETICLE_CHROMIUM_PATH",
4260
4265
  /** Max simultaneous leased headless contexts in the browser pool (resource cap). */
4261
4266
  MAX_CONTEXTS: "RETICLE_MAX_CONTEXTS",
4262
4267
  /**
@@ -4325,6 +4330,8 @@ var ReticleEnv = {
4325
4330
  HARNESS_BASE_URL: "RETICLE_HARNESS_BASE_URL",
4326
4331
  /** Hard ceiling on harness model turns in one drive. Bounds cost, not value. */
4327
4332
  HARNESS_MAX_STEPS: "RETICLE_HARNESS_MAX_STEPS",
4333
+ /** The free drive `reticle try` was granted, sent on every Harness call it bills. */
4334
+ DRIVE_ID: "RETICLE_DRIVE_ID",
4328
4335
  /**
4329
4336
  * Which model backs the harness driver: `anthropic` (the default) or `jev`.
4330
4337
  *
@@ -4437,8 +4444,9 @@ var ReticleDir = {
4437
4444
  /**
4438
4445
  * the user's own record of what Reticle has done for them — .reticle/impact.json.
4439
4446
  *
4440
- * Local only, never uploaded, and deliberately NOT part of telemetry: telemetry answers questions
4441
- * about the product; this answers the user's question about their own work.
4447
+ * NOT telemetry: telemetry answers questions about the product; this answers the user's question
4448
+ * about their own work. A linked project's copy syncs to the user's own dashboard (the sync cycle's
4449
+ * `impact` kind); the machine-wide copy in `~/.reticle` never leaves the machine.
4442
4450
  */
4443
4451
  IMPACT_FILE: "impact.json",
4444
4452
  /** what changes were SUPPOSED to make true —.reticle/intent.json (git-checked, reviewed) */
@@ -4453,14 +4461,6 @@ var ReticleDir = {
4453
4461
  * classified nor ignored.
4454
4462
  */
4455
4463
  INTENT_SUBDIR: "intent",
4456
- /**
4457
- * what a drive types into a field, keyed by the field's label —.reticle/fill-values.json.
4458
- *
4459
- * Git-checked on purpose. A generated value is paid for once and then belongs to the project: the
4460
- * next drive reuses it for free, a replay sends exactly what the recording sent, and a human who
4461
- * dislikes one can edit the file rather than argue with a model.
4462
- */
4463
- FILL_VALUES_FILE: "fill-values.json",
4464
4464
  /** opt-in pixel baselines —.reticle/visual/<name>.png + <name>.diff.png. */
4465
4465
  VISUAL_SUBDIR: "visual",
4466
4466
  /** verification-run artifacts —.reticle/runs/<runId>.json (the OEM/CI-consumable verdict). */
@@ -4475,6 +4475,8 @@ var ReticleDir = {
4475
4475
  * in three separate files, so the guard could not see it and nobody was ever asked.
4476
4476
  */
4477
4477
  FEEDBACK_SUBDIR: "feedback",
4478
+ /** Every Harness drive plan, and how each journey went — .reticle/plans/<when>-<planId>.json. */
4479
+ PLANS_SUBDIR: "plans",
4478
4480
  /** durable causal journal —.reticle/sessions/<id>/{events,actions}.jsonl (the substrate). */
4479
4481
  SESSIONS_SUBDIR: "sessions",
4480
4482
  /** append-only event ledger inside a session dir (one ReticleEvent per line). */
@@ -4492,14 +4494,22 @@ var ReticleDir = {
4492
4494
  JOURNAL_EVENTS_CLOSED_FILE: "events.closed.json",
4493
4495
  /** learned expected-envelopes per route, accumulated across runs (the deviation-report baseline). */
4494
4496
  ENVELOPES_FILE: "envelopes.json",
4495
- /** learned ambient (action-less churn) region map — excluded from settle/summaries/envelopes. */
4496
4497
  AMBIENT_FILE: "ambient.json",
4498
+ // learned action-less churn regions, excluded from settle/summaries
4497
4499
  /** per-flow flake ledger — replay outcomes that decide intermittent-failure quarantine. */
4498
4500
  FLAKE_FILE: "flake.json",
4499
4501
  /** The app-wide coverage ledger — see server features/exhaust/ledger.ts. */
4500
4502
  COVERAGE_FILE: "coverage.json",
4503
+ /** Human review marks (HUD notes), pending and resolved — see server portal/session/human. */
4504
+ NOTES_FILE: "notes.json",
4505
+ /** The user's latest request, as the agent relayed it. Local: it is the user's own words. */
4506
+ REQUEST_FILE: "request.json",
4507
+ APP_NOTES_FILE: "app-notes.json",
4508
+ // confirmed targeting hints for the next session; never evidence
4509
+ PLATFORM_MOMENTS_FILE: "platform-moments.json",
4510
+ // moments already said, so each is said once
4501
4511
  /**
4502
- * the project's cloud binding — .reticle/cloud.json, written by `reticle link`. Git-checked and
4512
+ * the project's cloud binding — .reticle/cloud.json, written by `reticle connect`. Git-checked and
4503
4513
  * non-secret: the project id, the API origin, and where its dashboard lives. The KEY lives in
4504
4514
  * ~/.reticle/credentials.json instead, because that one must never reach a repository.
4505
4515
  */
@@ -4557,6 +4567,9 @@ var RETICLE_ROOT_GLOBAL = "__RETICLE_ROOT__";
4557
4567
  var RETICLE_SDK_VERSION_GLOBAL = "__RETICLE_SDK_VERSION__";
4558
4568
  var RETICLE_IGNORE_MARKER = "@reticle-ignore";
4559
4569
 
4570
+ // ../../../core/dist/identity/testid-attribute.js
4571
+ var TESTID_ATTR_PATTERN = /^[a-zA-Z_][\w-]*$/;
4572
+
4560
4573
  // ../../../core/dist/identity/project-id.js
4561
4574
  var PROJECT_ID_HASH_LENGTH = 8;
4562
4575
  var FALLBACK_PROJECT_BASE = "app";
@@ -4707,6 +4720,16 @@ function readConfiguredPort(startDir, readFile = readFileOrThrow) {
4707
4720
  tcpPort
4708
4721
  );
4709
4722
  }
4723
+ function readConfiguredTestIdAttribute(startDir, readFile = readFileOrThrow) {
4724
+ return readNearestField(
4725
+ startDir,
4726
+ RETICLE_CONFIG_BASENAME,
4727
+ "testIdAttribute",
4728
+ readFile,
4729
+ MAX_CONFIG_SEARCH_DEPTH,
4730
+ (value) => "string" === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : void 0
4731
+ );
4732
+ }
4710
4733
  function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName, readConfiguredId = readConfiguredProjectId) {
4711
4734
  if (explicit !== void 0 && explicit.length > 0) return explicit;
4712
4735
  const configured = readConfiguredId(cwd);
@@ -5141,7 +5164,10 @@ export function createReticleEmitter() {
5141
5164
  };
5142
5165
  }
5143
5166
 
5144
- export function commitAndSignal() {}
5167
+ // It wraps the APP's write, so the write still runs: Reticle does nothing in production, the app does.
5168
+ export function commitAndSignal(_emitter, mutate) {
5169
+ return mutate();
5170
+ }
5145
5171
  export function registerReticleDomain() {}
5146
5172
 
5147
5173
  export function buildSnapshot() {
@@ -5230,6 +5256,7 @@ export function resolveMarkAnchor() {
5230
5256
  `;
5231
5257
 
5232
5258
  // src/connect-args.ts
5259
+ var PRESENT_OPTION = "present";
5233
5260
  function connectArgs(options) {
5234
5261
  const args = {};
5235
5262
  const port = options.port ?? RETICLE_DEFAULT_PORT;
@@ -5237,6 +5264,7 @@ function connectArgs(options) {
5237
5264
  if (options.session !== void 0) args["session"] = options.session;
5238
5265
  if (options.projectId !== void 0) args["projectId"] = options.projectId;
5239
5266
  if (options.token !== void 0) args["token"] = options.token;
5267
+ if (false === options.present) args[PRESENT_OPTION] = false;
5240
5268
  if (options.root !== void 0 && options.root.length > 0) args["root"] = options.root;
5241
5269
  if (options.sdkVersion !== void 0 && options.sdkVersion.length > 0) {
5242
5270
  args["sdkVersion"] = options.sdkVersion;
@@ -5260,6 +5288,9 @@ function connectArgs(options) {
5260
5288
  if (true === options.allowNonLocalhost || "1" === process.env["VITE_RETICLE_ALLOW_NON_LOCALHOST"]) {
5261
5289
  args["allowNonLocalhost"] = true;
5262
5290
  }
5291
+ if (options.testIdAttribute !== void 0 && options.testIdAttribute.length > 0) {
5292
+ args["testIdAttribute"] = options.testIdAttribute;
5293
+ }
5263
5294
  return Object.keys(args).length > 0 ? JSON.stringify(args) : "";
5264
5295
  }
5265
5296
 
@@ -5417,10 +5448,7 @@ function reticle(options = {}) {
5417
5448
  const sourceMapping = options.sourceMapping !== false;
5418
5449
  const inject = options.inject !== false;
5419
5450
  const desktop = true === options.desktop;
5420
- const resolved = {
5421
- ...options,
5422
- projectId: resolveProjectId(options.projectId, process.cwd())
5423
- };
5451
+ const projectIdIn = (dir) => resolveProjectId(options.projectId, dir);
5424
5452
  let htmlEntrySpecifier;
5425
5453
  let root;
5426
5454
  let command;
@@ -5435,7 +5463,9 @@ function reticle(options = {}) {
5435
5463
  let connectDelivered = false;
5436
5464
  let lastPortWarning;
5437
5465
  const resolveLazy = () => {
5438
- const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, process.cwd());
5466
+ const appDir = root ?? process.cwd();
5467
+ const resolved = { ...options, projectId: projectIdIn(appDir) };
5468
+ const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, appDir);
5439
5469
  if (warning !== void 0 && warning !== lastPortWarning) {
5440
5470
  lastPortWarning = warning;
5441
5471
  warn(warning);
@@ -5445,7 +5475,13 @@ function reticle(options = {}) {
5445
5475
  const withToken = token !== void 0 ? { ...withPort, token } : withPort;
5446
5476
  const appRoot = withToken.root ?? root ?? process.cwd();
5447
5477
  const sdkVersion = withToken.sdkVersion ?? sdkPackageVersion(appRoot);
5448
- return { ...withToken, root: appRoot, sdkVersion };
5478
+ const testIdAttribute = withToken.testIdAttribute ?? readConfiguredTestIdAttribute(appRoot);
5479
+ return {
5480
+ ...withToken,
5481
+ root: appRoot,
5482
+ sdkVersion,
5483
+ ...testIdAttribute === void 0 ? {} : { testIdAttribute }
5484
+ };
5449
5485
  };
5450
5486
  const currentConnectSource = () => connectModuleSource(resolveLazy(), root === void 0 ? null : findDevModule(root, import_node_fs6.existsSync));
5451
5487
  let lastServedConnectSource;
package/dist/index.d.cts CHANGED
@@ -64,6 +64,8 @@ export interface ReticleVitePluginOptions {
64
64
  projectId?: string;
65
65
  /** Auth token forwarded to connect when the bridge requires one. */
66
66
  token?: string;
67
+ /** Show Reticle's presenter. Defaults to true; set false to keep the agent connection without the panel. */
68
+ present?: boolean;
67
69
  /** Stamp data-reticle-source for React 19 source mapping. Default true (harmless on React <=18). */
68
70
  sourceMapping?: boolean;
69
71
  /** Auto-inject the dev-gated reticle.connect call. Default true. */
@@ -109,6 +111,8 @@ export interface ReticleVitePluginOptions {
109
111
  * vite.config.
110
112
  */
111
113
  networkBodyMaxChars?: number;
114
+ /** Attribute this app marks controls with instead of `data-testid`. Defaults to `.reticle.json`'s `testIdAttribute`. */
115
+ testIdAttribute?: string;
112
116
  /**
113
117
  * Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
114
118
  *
package/dist/index.d.ts CHANGED
@@ -64,6 +64,8 @@ export interface ReticleVitePluginOptions {
64
64
  projectId?: string;
65
65
  /** Auth token forwarded to connect when the bridge requires one. */
66
66
  token?: string;
67
+ /** Show Reticle's presenter. Defaults to true; set false to keep the agent connection without the panel. */
68
+ present?: boolean;
67
69
  /** Stamp data-reticle-source for React 19 source mapping. Default true (harmless on React <=18). */
68
70
  sourceMapping?: boolean;
69
71
  /** Auto-inject the dev-gated reticle.connect call. Default true. */
@@ -109,6 +111,8 @@ export interface ReticleVitePluginOptions {
109
111
  * vite.config.
110
112
  */
111
113
  networkBodyMaxChars?: number;
114
+ /** Attribute this app marks controls with instead of `data-testid`. Defaults to `.reticle.json`'s `testIdAttribute`. */
115
+ testIdAttribute?: string;
112
116
  /**
113
117
  * Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
114
118
  *
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { ensurePairingToken } from './token/ensure-token.js';
4
4
  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
- import { resolveProjectId } from './project-id.js';
7
+ import { readConfiguredTestIdAttribute, resolveProjectId } from './project-id.js';
8
8
  import { resolveDaemonPort } from './discover-port.js';
9
9
  import { announceDevServer } from './announce.js';
10
10
  import { stampSvelte } from './svelte-source.js';
@@ -259,12 +259,10 @@ export function reticle(options = {}) {
259
259
  const sourceMapping = options.sourceMapping !== false;
260
260
  const inject = options.inject !== false;
261
261
  const desktop = true === options.desktop;
262
- // Resolve the stable projectId once (explicit option, else derived from package.json + cwd) so the
263
- // app is identifiable across port changes with zero config.
264
- const resolved = {
265
- ...options,
266
- projectId: resolveProjectId(options.projectId, process.cwd()),
267
- };
262
+ // The stable projectId (explicit option, else `.reticle.json` / package.json in VITE'S root), so the
263
+ // app is identifiable across port changes with zero config. Read from the root, never the cwd:
264
+ // `vite apps/admin` from a monorepo root announced the root's app and saved flows under it.
265
+ const projectIdIn = (dir) => resolveProjectId(options.projectId, dir);
268
266
  /**
269
267
  * The specifier the HTML points at, e.g. `/src/main.tsx`.
270
268
  *
@@ -315,7 +313,9 @@ export function reticle(options = {}) {
315
313
  */
316
314
  const resolveLazy = () => {
317
315
  // 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());
316
+ const appDir = root ?? process.cwd();
317
+ const resolved = { ...options, projectId: projectIdIn(appDir) };
318
+ const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, appDir);
319
319
  if (warning !== undefined && warning !== lastPortWarning) {
320
320
  lastPortWarning = warning;
321
321
  warn(warning);
@@ -327,7 +327,15 @@ export function reticle(options = {}) {
327
327
  // tree, and they travel in the generated connect call rather than through a `define`.
328
328
  const appRoot = withToken.root ?? root ?? process.cwd();
329
329
  const sdkVersion = withToken.sdkVersion ?? sdkPackageVersion(appRoot);
330
- return { ...withToken, root: appRoot, sdkVersion };
330
+ // `.reticle.json` is where a project names its test-id attribute, read per call like the port so
331
+ // an edit takes effect on the next page load; the option, when given, wins.
332
+ const testIdAttribute = withToken.testIdAttribute ?? readConfiguredTestIdAttribute(appRoot);
333
+ return {
334
+ ...withToken,
335
+ root: appRoot,
336
+ sdkVersion,
337
+ ...(testIdAttribute === undefined ? {} : { testIdAttribute }),
338
+ };
331
339
  };
332
340
  /**
333
341
  * The connect module's source as it would be served RIGHT NOW. Recomputed rather than cached: the
@@ -34,6 +34,13 @@ export declare function readConfiguredProjectId(startDir: string, readFile?: Rea
34
34
  * project's daemon — and the bridge refused it.
35
35
  */
36
36
  export declare function readConfiguredPort(startDir: string, readFile?: ReadFile): number | undefined;
37
+ /**
38
+ * Read `testIdAttribute` from the nearest `.reticle.json`, or undefined: the attribute this project
39
+ * marks controls with when it is not `data-testid` (`data-test-subj`, `data-cy`, ...). The page needs
40
+ * it to resolve `{ testid }`, and the file is the one place the CLI, the daemon and the plugin
41
+ * already agree on, so the plugin forwards it to `connect()` rather than asking for it twice.
42
+ */
43
+ export declare function readConfiguredTestIdAttribute(startDir: string, readFile?: ReadFile): string | undefined;
37
44
  /**
38
45
  * Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
39
46
  * option, then the id `reticle init` recorded in `.reticle.json`, then derivation.
@@ -9,7 +9,7 @@
9
9
  import { createHash } from 'node:crypto';
10
10
  import { dirname, join } from 'node:path';
11
11
  import { readFileSync } from 'node:fs';
12
- import { PROJECT_ID_HASH_LENGTH, projectIdFrom } from '@reticlehq/core';
12
+ import { PROJECT_ID_HASH_LENGTH, TESTID_ATTR_PATTERN, projectIdFrom } from '@reticlehq/core';
13
13
  /** The project config `reticle init` writes, and the id of record it carries. */
14
14
  const RETICLE_CONFIG_BASENAME = '.reticle.json';
15
15
  export { slugifyPackageName } from '@reticlehq/core';
@@ -99,6 +99,15 @@ export function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
99
99
  export function readConfiguredPort(startDir, readFile = readFileOrThrow) {
100
100
  return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'port', readFile, MAX_CONFIG_SEARCH_DEPTH, tcpPort);
101
101
  }
102
+ /**
103
+ * Read `testIdAttribute` from the nearest `.reticle.json`, or undefined: the attribute this project
104
+ * marks controls with when it is not `data-testid` (`data-test-subj`, `data-cy`, ...). The page needs
105
+ * it to resolve `{ testid }`, and the file is the one place the CLI, the daemon and the plugin
106
+ * already agree on, so the plugin forwards it to `connect()` rather than asking for it twice.
107
+ */
108
+ export function readConfiguredTestIdAttribute(startDir, readFile = readFileOrThrow) {
109
+ return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'testIdAttribute', readFile, MAX_CONFIG_SEARCH_DEPTH, (value) => ('string' === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : undefined));
110
+ }
102
111
  /**
103
112
  * Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
104
113
  * option, then the id `reticle init` recorded in `.reticle.json`, then derivation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/vite-plugin",
3
- "version": "3.5.0",
3
+ "version": "3.7.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.5.0",
45
- "@reticlehq/core": "3.5.0"
44
+ "@reticlehq/babel-plugin": "3.7.0",
45
+ "@reticlehq/core": "3.7.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/babel__core": "^7.20.5",
49
49
  "esbuild": "^0.28.2",
50
50
  "svelte": "^5.57.1",
51
51
  "vite": "^8",
52
- "@reticlehq/react": "3.5.0"
52
+ "@reticlehq/react": "3.7.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "vite": ">=4"