@reticlehq/vite-plugin 2.2.1 → 2.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.
@@ -4,4 +4,3 @@
4
4
  * so the selection is unit-tested without a real ~/.reticle or real processes.
5
5
  */
6
6
  export declare function discoverDaemonPort(projectId: string | undefined, home?: string, alive?: (pid: number) => boolean): number | undefined;
7
- //# sourceMappingURL=discover-port.d.ts.map
@@ -46,4 +46,3 @@ export function discoverDaemonPort(projectId, home = join(homedir(), ReticleDir.
46
46
  }
47
47
  return pickDaemonPort(entries, projectId, alive) ?? undefined;
48
48
  }
49
- //# sourceMappingURL=discover-port.js.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  export declare const RETICLE_VITE_PLUGIN_NAME = "reticle";
2
+ /**
3
+ * Compile-time global carrying the daemon's pairing token, for connects the plugin does not write
4
+ * itself. The bridge requires the token even on localhost, and nothing in a browser can read the
5
+ * file it lives in.
6
+ */
7
+ export declare const RETICLE_TOKEN_GLOBAL = "__RETICLE_TOKEN__";
2
8
  /**
3
9
  * The connect code is served as a real module (not an inline <script>) so that Vite's import
4
10
  * pipeline resolves the bare `@reticlehq/react` specifier. An inline injected script is NOT run through
@@ -6,6 +12,15 @@ export declare const RETICLE_VITE_PLUGIN_NAME = "reticle";
6
12
  * by the injected <script src> and served by the load hook below.
7
13
  */
8
14
  export declare const RETICLE_CONNECT_MODULE = "/@reticle-connect";
15
+ /**
16
+ * The pre-hook, as source for an inline <head> script.
17
+ *
18
+ * Deliberately dependency-free ES5 in a try/catch: it runs before anything else on the page, so it
19
+ * must not assume a bundler, a module system, or that React is present at all. It installs a faithful
20
+ * devtools hook (React calls `inject` and expects a renderer id back, and stores the renderer) and
21
+ * counts commits into a buffer the module-side meter adopts later.
22
+ */
23
+ export declare const RENDER_PREHOOK_SOURCE = "(function(){try{\nvar K='__REACT_DEVTOOLS_GLOBAL_HOOK__',P='__reticleRenderPreHook';\nif(globalThis[P])return;\nvar B={commits:0,sinks:[]};\nglobalThis[P]=B;\nvar fire=function(){B.commits++;for(var i=0;i<B.sinks.length;i++){try{B.sinks[i].apply(null,arguments);}catch(e){}}};\nvar h=globalThis[K];\nif(h===undefined){\nglobalThis[K]={supportsFiber:true,renderers:new Map(),inject:function(r){var id=this.renderers.size+1;this.renderers.set(id,r);return id;},\nonScheduleFiberRoot:function(){},onCommitFiberRoot:fire,onPostCommitFiberRoot:function(){},onCommitFiberUnmount:function(){}};\n}else{var prev=h.onCommitFiberRoot;h.onCommitFiberRoot=function(){try{fire.apply(null,arguments);}catch(e){}\nif(typeof prev==='function')return prev.apply(this,arguments);};}\n}catch(e){}})();";
9
24
  export interface ReticleVitePluginOptions {
10
25
  /** Bridge WebSocket port. Defaults to the SDK default; only baked into connect when non-default. */
11
26
  port?: number;
@@ -22,43 +37,126 @@ export interface ReticleVitePluginOptions {
22
37
  sourceMapping?: boolean;
23
38
  /** Auto-inject the dev-gated reticle.connect call. Default true. */
24
39
  inject?: boolean;
40
+ /**
41
+ * This build is an Electron/Tauri renderer. Changes two things a desktop shell needs and a web app
42
+ * must not get:
43
+ *
44
+ * - The plugin also applies to `vite build`. A packaged desktop renderer IS a production build
45
+ * loaded from `file://` or a custom protocol — there is no dev server — so the default
46
+ * `apply: 'serve'` drops the plugin entirely and the app ships with no `connect()` at all.
47
+ * - `connect()` is called with `allowInProduction`, because that same renderer reports
48
+ * NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
49
+ *
50
+ * Off by default and never inferred: turning it on means an instrumented production BUNDLE, which
51
+ * is exactly what a web app must never ship. Keep it behind your own dev-only build (a dev target,
52
+ * or `process.env.NODE_ENV !== 'production'` in vite.config) so it cannot reach a release binary.
53
+ */
54
+ desktop?: boolean;
55
+ /**
56
+ * Record request/response BODIES on `reticle_network`, not just method/url/status.
57
+ *
58
+ * Off by default because a body is the one part of a request that routinely carries a card
59
+ * number, a token or a customer's address, and the daemon journals what it is told.
60
+ *
61
+ * It matters that this is reachable at all. The SDK has supported `captureNetworkBodies` on
62
+ * `connect()` since bodies existed, but the plugin — the documented one-line integration, and the
63
+ * only `connect()` most apps ever have — had no way to pass it, and calling `connect()` a second
64
+ * time is a no-op. So for every app wired the recommended way, a payload was unreachable: on a
65
+ * real payments dashboard, a refund POSTing `amount: 1187.01` into a paise field (a 100x
66
+ * under-refund) was visible to Playwright's request inspector and invisible here.
67
+ *
68
+ * Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`, so it can be turned on for one debugging
69
+ * session without editing vite.config.
70
+ */
71
+ captureNetworkBodies?: boolean;
72
+ /**
73
+ * Where a diagnostic goes. Defaults to the console; injected so the dev-mode injection check is
74
+ * testable without capturing global console output.
75
+ */
76
+ onWarn?: (message: string) => void;
25
77
  }
26
78
  /** Structural Vite plugin shape — avoids a hard dependency on `vite` while staying assignable to its `Plugin`. */
27
79
  export interface ReticleVitePlugin {
28
80
  name: string;
29
- apply: 'serve';
81
+ /**
82
+ * Vite's `config` hook. Used to declare the SDK's CJS runtime deps for pre-bundling — see the
83
+ * implementation for why omitting them makes the whole SDK fail to load on linked setups.
84
+ */
85
+ config?: (config: {
86
+ optimizeDeps?: {
87
+ include?: string[];
88
+ esbuildOptions?: {
89
+ define?: Record<string, string>;
90
+ };
91
+ };
92
+ define?: Record<string, string>;
93
+ root?: string;
94
+ }) => {
95
+ optimizeDeps: {
96
+ include: string[];
97
+ esbuildOptions: {
98
+ define: Record<string, string>;
99
+ };
100
+ };
101
+ define: Record<string, string>;
102
+ };
103
+ /** Absent in desktop mode, where the plugin must also run for `vite build`. */
104
+ apply?: 'serve';
30
105
  enforce: 'pre';
31
106
  transform: (code: string, id: string) => {
32
107
  code: string;
33
108
  map: string | null;
34
109
  } | null;
35
- resolveId: (id: string) => string | null;
110
+ resolveId: (id: string, importer?: string) => string | null;
36
111
  load: (id: string) => string | null;
37
112
  transformIndexHtml: (html: string) => HtmlTag[];
113
+ /** Vite hands over the resolved config; used to resolve the HTML entry exactly. */
114
+ configResolved?: (config: {
115
+ root?: string;
116
+ command?: string;
117
+ }) => void;
118
+ /** Build-time post-condition: desktop injection must have happened. */
119
+ buildEnd?: () => void;
120
+ /** Runs the dev-mode injection check immediately. Test seam for the deferred timer. */
121
+ checkInjectedForTest?: () => void;
38
122
  }
39
123
  interface HtmlTag {
40
124
  tag: string;
41
- attrs: Record<string, string>;
42
- injectTo: 'body';
125
+ /** Absent on an inline script, which carries its source in `children` instead. */
126
+ attrs?: Record<string, string>;
127
+ /** Inline source, for a tag that has no `src`. */
128
+ children?: string;
129
+ /** `head-prepend` is required for the render pre-hook: it must run before any module script. */
130
+ injectTo: 'body' | 'head-prepend';
43
131
  }
44
- /**
45
- * Read the daemon's auto-provisioned pairing token (~/.reticle/pairing-token, or the
46
- * RETICLE_PAIRING_TOKEN_DIR override) so the served app can present it. Node-side only — a browser
47
- * sandbox can't read the file, which is exactly why a rogue localhost app can't forge it. Best-effort:
48
- * undefined if the daemon hasn't started yet (the page reloads once it has). Exported for testing.
49
- */
50
132
  export declare function readPairingToken(): string | undefined;
51
133
  /** The body of the connect module — real imports, resolved by Vite when the module is served. */
52
- export declare function connectModuleSource(options: ReticleVitePluginOptions): string;
134
+ /**
135
+ * The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
136
+ *
137
+ * It is imported by CONVENTION rather than by patching the app's entry file. The connect is injected
138
+ * into a virtual module, so there is nowhere for a user to add these calls without `init` editing
139
+ * `src/main.tsx` — an edit to the file people actually own, for something that is opt-in enrichment.
140
+ * Convention costs one `existsSync` and leaves their entry untouched.
141
+ */
142
+ export declare const RETICLE_DEV_MODULE_CANDIDATES: readonly ["src/reticle-dev.ts", "src/reticle-dev.js", "src/reticle-dev.tsx", "src/reticle-dev.jsx"];
143
+ /** The app's dev module, as an importable path — or null when the app has none. */
144
+ export declare function findDevModule(root: string, exists: (p: string) => boolean): string | null;
145
+ export declare function connectModuleSource(options: ReticleVitePluginOptions, devModule?: string | null): string;
53
146
  /**
54
147
  * Reticle Vite plugin. Add to your `plugins` array and the entire integration is done:
55
148
  *
56
149
  * import { reticle } from '@reticlehq/vite-plugin';
57
150
  * export default defineConfig({ plugins: [react(), reticle()] });
58
151
  *
59
- * `apply: 'serve'` means Vite drops the plugin entirely from `vite build` production bundles
60
- * are never instrumented. Gating is the tool's job, not a user-managed env check.
152
+ * `apply: 'serve'` means Vite drops the plugin entirely from `vite build`, so a web production
153
+ * bundle is never instrumented gating is the tool's job, not a user-managed env check.
154
+ *
155
+ * `desktop: true` is the ONE documented exception, and it inverts that guarantee deliberately: a
156
+ * packaged Electron/Tauri renderer IS a production build with no dev server, so serve-only gating
157
+ * would ship an app with no connect() at all. The cost is that the flag hands gating back to the
158
+ * caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
159
+ * a release binary.
61
160
  */
62
161
  export declare function reticle(options?: ReticleVitePluginOptions): ReticleVitePlugin;
63
162
  export {};
64
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,16 +1,61 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { transformSync } from '@babel/core';
5
5
  import reticleSource from '@reticlehq/babel-plugin';
6
- import { RETICLE_DEFAULT_PORT, bridgeWsUrl, ReticleDir, ReticleEnv } from '@reticlehq/core';
6
+ import { RETICLE_DEFAULT_PORT, RETICLE_RENDER_PREHOOK, bridgeWsUrl, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, } from '@reticlehq/core';
7
7
  import { resolveProjectId } from './project-id.js';
8
8
  import { discoverDaemonPort } from './discover-port.js';
9
+ import { SVELTE_FILE, stampSvelte } from './svelte-source.js';
10
+ import { createRequire } from 'node:module';
9
11
  export const RETICLE_VITE_PLUGIN_NAME = 'reticle';
10
12
  // The React kit the host app imports the SDK from. It re-exports the browser sensor, so a single
11
13
  // specifier yields both `reticle` (connect) and `install` (the React adapter). NOT `@reticlehq/core`
12
14
  // — that is the isomorphic foundation and exports neither.
13
15
  const RETICLE_PACKAGE = '@reticlehq/react';
16
+ /**
17
+ * Compile-time global carrying the daemon's pairing token, for connects the plugin does not write
18
+ * itself. The bridge requires the token even on localhost, and nothing in a browser can read the
19
+ * file it lives in.
20
+ */
21
+ export const RETICLE_TOKEN_GLOBAL = '__RETICLE_TOKEN__';
22
+ /**
23
+ * Whether a package can be resolved from this process. Used to avoid declaring an optimizeDeps entry
24
+ * for something the app does not have, which Vite reports as a resolve failure on every boot.
25
+ */
26
+ function isResolvable(specifier) {
27
+ try {
28
+ createRequire(import.meta.url).resolve(specifier);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /**
36
+ * A fingerprint of the installed SDK build, mixed into `optimizeDeps` so Vite re-bundles when the
37
+ * SDK changes.
38
+ *
39
+ * Vite's dep-optimizer cache is keyed on the `optimizeDeps` config and the lockfile — NOT on the
40
+ * contents of the packages it bundled. Upgrade the SDK in place (a patched dist, a linked checkout,
41
+ * an overlay) and the version in `package.json` can stay the same, so Vite keeps serving the OLD
42
+ * pre-bundled copy out of `node_modules/.vite` across dev-server restarts. The fix you just shipped
43
+ * is simply not in the browser, and it looks like the fix does not work. That cost a real
44
+ * false-negative during this bug hunt, and every user upgrading in place hits the same thing.
45
+ *
46
+ * Size+mtime is enough: it changes whenever the bundle does and costs one `stat`.
47
+ */
48
+ function sdkBuildFingerprint() {
49
+ try {
50
+ const entry = createRequire(import.meta.url).resolve(RETICLE_PACKAGE);
51
+ const { size, mtimeMs } = statSync(entry);
52
+ return `${String(size)}-${String(Math.trunc(mtimeMs))}`;
53
+ }
54
+ catch {
55
+ // Unresolvable (not installed yet, exotic layout) — a constant is still correct, just inert.
56
+ return 'unknown';
57
+ }
58
+ }
14
59
  /** Files we stamp with source info — JSX/TSX only. */
15
60
  const JSX_FILE = /\.[jt]sx$/;
16
61
  /** Rollup virtual-module ids start with a NUL byte; never transform those. */
@@ -23,14 +68,77 @@ const NODE_MODULES = 'node_modules';
23
68
  * by the injected <script src> and served by the load hook below.
24
69
  */
25
70
  export const RETICLE_CONNECT_MODULE = '/@reticle-connect';
26
- function shouldStamp(id) {
27
- if (id.startsWith(VIRTUAL_PREFIX))
71
+ /**
72
+ * The pre-hook, as source for an inline <head> script.
73
+ *
74
+ * Deliberately dependency-free ES5 in a try/catch: it runs before anything else on the page, so it
75
+ * must not assume a bundler, a module system, or that React is present at all. It installs a faithful
76
+ * devtools hook (React calls `inject` and expects a renderer id back, and stores the renderer) and
77
+ * counts commits into a buffer the module-side meter adopts later.
78
+ */
79
+ export const RENDER_PREHOOK_SOURCE = `(function(){try{
80
+ var K='__REACT_DEVTOOLS_GLOBAL_HOOK__',P='${RETICLE_RENDER_PREHOOK}';
81
+ if(globalThis[P])return;
82
+ var B={commits:0,sinks:[]};
83
+ globalThis[P]=B;
84
+ var fire=function(){B.commits++;for(var i=0;i<B.sinks.length;i++){try{B.sinks[i].apply(null,arguments);}catch(e){}}};
85
+ var h=globalThis[K];
86
+ if(h===undefined){
87
+ globalThis[K]={supportsFiber:true,renderers:new Map(),inject:function(r){var id=this.renderers.size+1;this.renderers.set(id,r);return id;},
88
+ onScheduleFiberRoot:function(){},onCommitFiberRoot:fire,onPostCommitFiberRoot:function(){},onCommitFiberUnmount:function(){}};
89
+ }else{var prev=h.onCommitFiberRoot;h.onCommitFiberRoot=function(){try{fire.apply(null,arguments);}catch(e){}
90
+ if(typeof prev==='function')return prev.apply(this,arguments);};}
91
+ }catch(e){}})();`;
92
+ /**
93
+ * How long after serving the HTML to wait before concluding the entry was never injected.
94
+ *
95
+ * Generous on purpose: the browser has to request the entry, and a cold dev server transforming a
96
+ * large app can take a moment. A false warning would train people to ignore a real one.
97
+ */
98
+ const DEV_INJECTION_GRACE_MS = 10_000;
99
+ /**
100
+ * Is this resolved module id the one the HTML referenced?
101
+ *
102
+ * `resolveId` sees the specifier (`/src/main.tsx`); `transform` sees the absolute path
103
+ * (`/Users/me/app/src/main.tsx`). A suffix match is what bridges them. Any query suffix
104
+ * (`?html-proxy`, `?t=...`) is stripped first so a re-transformed module still matches.
105
+ */
106
+ function isHtmlEntry(id, specifier, root) {
107
+ if (specifier === undefined)
28
108
  return false;
109
+ const clean = (value) => value.split('?')[0] ?? value;
110
+ const target = clean(specifier);
111
+ const candidate = clean(id);
112
+ if (candidate === target)
113
+ return true;
114
+ // EXACT when the resolved root is known: Vite reports the HTML's script as a root-relative
115
+ // specifier (`/src/main.tsx`) while `transform` sees the absolute path, and joining the two is a
116
+ // real resolution rather than a guess. Suffix matching alone would also inject into
117
+ // `/other/src/main.tsx`, a different file that merely ends the same way.
118
+ if (root !== undefined && target.startsWith('/')) {
119
+ return candidate === `${root.replace(/\/$/, '')}${target}`;
120
+ }
121
+ // Fallback for the rare case Vite never reported a root — still better than not injecting, and the
122
+ // buildEnd post-condition means a wrong match cannot pass unnoticed as "nothing happened".
123
+ return candidate.endsWith(target.startsWith('/') ? target : `/${target}`);
124
+ }
125
+ /** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
126
+ function stampableId(id) {
127
+ if (id.startsWith(VIRTUAL_PREFIX))
128
+ return null;
29
129
  if (id.includes(NODE_MODULES))
30
- return false;
130
+ return null;
31
131
  // Strip any query suffix (?worker, ?raw,...) before matching the extension.
32
- const clean = id.split('?')[0] ?? id;
33
- return JSX_FILE.test(clean);
132
+ return id.split('?')[0] ?? id;
133
+ }
134
+ function shouldStamp(id) {
135
+ const clean = stampableId(id);
136
+ return clean !== null && JSX_FILE.test(clean);
137
+ }
138
+ /** A `.svelte` single-file component, which needs the Svelte stamper rather than Babel. */
139
+ function shouldStampSvelte(id) {
140
+ const clean = stampableId(id);
141
+ return clean !== null && SVELTE_FILE.test(clean);
34
142
  }
35
143
  function stamp(code, id) {
36
144
  const out = transformSync(code, {
@@ -54,6 +162,14 @@ function stamp(code, id) {
54
162
  * sandbox can't read the file, which is exactly why a rogue localhost app can't forge it. Best-effort:
55
163
  * undefined if the daemon hasn't started yet (the page reloads once it has). Exported for testing.
56
164
  */
165
+ /**
166
+ * CJS packages the browser SDK needs at runtime. Named, because a bare string here is exactly the
167
+ * kind of thing that silently rots when a dependency is renamed.
168
+ */
169
+ const SDK_CJS_DEPS = {
170
+ TESTING_LIBRARY: '@testing-library/dom',
171
+ ARIA_QUERY: 'aria-query',
172
+ };
57
173
  export function readPairingToken() {
58
174
  const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
59
175
  const dir = override !== undefined && override.length > 0 ? override : join(homedir(), ReticleDir.ROOT);
@@ -77,12 +193,47 @@ function connectArgs(options) {
77
193
  args['projectId'] = options.projectId;
78
194
  if (options.token !== undefined)
79
195
  args['token'] = options.token;
196
+ // A desktop renderer is a production build by construction; without this the SDK's prod backstop
197
+ // refuses to connect and the app is silently uninstrumented.
198
+ if (options.desktop === true)
199
+ args['allowInProduction'] = true;
200
+ // Env wins nothing — it only turns the flag ON, so a config that never set it can still be
201
+ // switched on for one debugging session without editing vite.config and restarting the mental
202
+ // model with it.
203
+ if (options.captureNetworkBodies === true || process.env['VITE_RETICLE_CAPTURE_BODIES'] === '1') {
204
+ args['captureNetworkBodies'] = true;
205
+ }
80
206
  return Object.keys(args).length > 0 ? JSON.stringify(args) : '';
81
207
  }
82
208
  /** The body of the connect module — real imports, resolved by Vite when the module is served. */
83
- export function connectModuleSource(options) {
209
+ /**
210
+ * The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
211
+ *
212
+ * It is imported by CONVENTION rather than by patching the app's entry file. The connect is injected
213
+ * into a virtual module, so there is nowhere for a user to add these calls without `init` editing
214
+ * `src/main.tsx` — an edit to the file people actually own, for something that is opt-in enrichment.
215
+ * Convention costs one `existsSync` and leaves their entry untouched.
216
+ */
217
+ export const RETICLE_DEV_MODULE_CANDIDATES = [
218
+ 'src/reticle-dev.ts',
219
+ 'src/reticle-dev.js',
220
+ 'src/reticle-dev.tsx',
221
+ 'src/reticle-dev.jsx',
222
+ ];
223
+ /** The app's dev module, as an importable path — or null when the app has none. */
224
+ export function findDevModule(root, exists) {
225
+ for (const rel of RETICLE_DEV_MODULE_CANDIDATES) {
226
+ if (exists(`${root}/${rel}`))
227
+ return `/${rel}`;
228
+ }
229
+ return null;
230
+ }
231
+ export function connectModuleSource(options, devModule = null) {
84
232
  const args = connectArgs(options);
85
- return `import { reticle, install } from '${RETICLE_PACKAGE}';\ninstall();\nreticle.connect(${args});\n`;
233
+ const base = `import { reticle, install } from '${RETICLE_PACKAGE}';\ninstall();\nreticle.connect(${args});\n`;
234
+ // AFTER connect: registerStore subscribes through the live SDK, and registering before there is a
235
+ // session to report into drops the first diffs.
236
+ return devModule === null ? base : `${base}import('${devModule}');\n`;
86
237
  }
87
238
  /**
88
239
  * Reticle Vite plugin. Add to your `plugins` array and the entire integration is done:
@@ -90,28 +241,180 @@ export function connectModuleSource(options) {
90
241
  * import { reticle } from '@reticlehq/vite-plugin';
91
242
  * export default defineConfig({ plugins: [react(), reticle()] });
92
243
  *
93
- * `apply: 'serve'` means Vite drops the plugin entirely from `vite build` production bundles
94
- * are never instrumented. Gating is the tool's job, not a user-managed env check.
244
+ * `apply: 'serve'` means Vite drops the plugin entirely from `vite build`, so a web production
245
+ * bundle is never instrumented gating is the tool's job, not a user-managed env check.
246
+ *
247
+ * `desktop: true` is the ONE documented exception, and it inverts that guarantee deliberately: a
248
+ * packaged Electron/Tauri renderer IS a production build with no dev server, so serve-only gating
249
+ * would ship an app with no connect() at all. The cost is that the flag hands gating back to the
250
+ * caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
251
+ * a release binary.
95
252
  */
96
253
  export function reticle(options = {}) {
97
254
  const sourceMapping = options.sourceMapping !== false;
98
255
  const inject = options.inject !== false;
256
+ const desktop = options.desktop === true;
99
257
  // Resolve the stable projectId once (explicit option, else derived from package.json + cwd) so the
100
258
  // app is identifiable across port changes with zero config.
101
259
  const resolved = {
102
260
  ...options,
103
261
  projectId: resolveProjectId(options.projectId, process.cwd()),
104
262
  };
263
+ /**
264
+ * The specifier the HTML points at, e.g. `/src/main.tsx`.
265
+ *
266
+ * Stored UNRESOLVED, because that is what `resolveId` receives — while `transform` is handed the
267
+ * absolute resolved path. Comparing the two directly never matches, and the failure is silent:
268
+ * the bundle simply ships with no connect() in it. Hence `isHtmlEntry`'s suffix comparison.
269
+ */
270
+ let htmlEntrySpecifier;
271
+ /** Vite's resolved project root, for exact entry resolution. Undefined until configResolved. */
272
+ let root;
273
+ /** 'serve' | 'build'. The dev check only applies to serve; buildEnd covers the other. */
274
+ let command;
275
+ const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
276
+ /** Whether connect() actually reached a module — asserted at buildEnd, never assumed. */
277
+ let injected = false;
278
+ /**
279
+ * Resolve port + token at the moment of injection, not at plugin construction. By the time a
280
+ * module is served or built the daemon is up and has written its pairing token; resolving early
281
+ * would bake in `undefined` and the app would fail auth on every connect.
282
+ */
283
+ const resolveLazy = () => {
284
+ const port = resolved.port ?? discoverDaemonPort(resolved.projectId);
285
+ const withPort = port !== undefined ? { ...resolved, port } : resolved;
286
+ const token = withPort.token ?? readPairingToken();
287
+ return token !== undefined ? { ...withPort, token } : withPort;
288
+ };
289
+ /**
290
+ * The BUILD message. A build always runs every transform, so "my transform never ran" and "the
291
+ * bundle has no connect()" are the same statement there, and stating it as a certainty is correct.
292
+ */
293
+ const notInjectedMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not inject reticle.connect(): the HTML entry module was ` +
294
+ 'never matched, so this app carries no instrumentation and will never connect. Check that ' +
295
+ 'index.html references your entry with a <script type="module" src="...">, or pass ' +
296
+ '`inject: false` and call reticle.connect() yourself.';
297
+ /**
298
+ * The DEV message, which must be weaker — and this is the whole reason the two are separate.
299
+ *
300
+ * In serve, `injected` records "my transform ran THIS session", which is not the same as "the app
301
+ * has no connect()". Vite serves an unchanged module straight from its transform cache, so on a
302
+ * warm cache the transform never runs, the flag stays false, and the old wording announced that
303
+ * the app "will never connect" while the served entry demonstrably contained the injection —
304
+ * verified by fetching it from the dev server. A false alarm, in the tool whose entire argument is
305
+ * that it does not raise them.
306
+ *
307
+ * So dev reports what it actually knows: unconfirmed, with the benign explanation first.
308
+ */
309
+ const unconfirmedInjectionMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not confirm reticle.connect() was injected: the HTML entry ` +
310
+ 'module was not transformed this session. That is expected when Vite served it from its ' +
311
+ 'transform cache. If the app does not appear in `reticle status`, restart the dev server with ' +
312
+ '`--force` to bypass the cache, then check that index.html references your entry with a ' +
313
+ '<script type="module" src="...">.';
314
+ /** Warn (never throw) in dev — a running dev server should report the doubt, not die of it. */
315
+ const checkInjected = () => {
316
+ if (!desktop || !inject || injected)
317
+ return;
318
+ warn(unconfirmedInjectionMessage());
319
+ };
105
320
  return {
106
321
  name: RETICLE_VITE_PLUGIN_NAME,
107
- apply: 'serve',
322
+ // Web: serve-only, so a production bundle can never carry the SDK — gating is the tool's job.
323
+ // Desktop: a packaged renderer IS a production build with no dev server, so the plugin must also
324
+ // run for `vite build` or the shipped app has no connect() at all.
325
+ ...(options.desktop === true ? {} : { apply: 'serve' }),
108
326
  enforce: 'pre',
327
+ /**
328
+ * Declare the SDK's CJS runtime deps so Vite pre-bundles them.
329
+ *
330
+ * `@testing-library/dom` is what `by: role` matching runs on, and it pulls CJS `aria-query`. When
331
+ * the SDK resolves from OUTSIDE the app's root — a linked package, a pnpm workspace, `npm link`,
332
+ * a monorepo alias — Vite skips pre-bundling and the named import dies with "does not provide an
333
+ * export named 'elementRoles'". That takes the WHOLE SDK down before connect() runs, so there is
334
+ * no session, no Reticle-side error, and only a console SyntaxError naming a package the
335
+ * developer has never heard of. Measured on the react-admin demo with the SDK aliased to a local
336
+ * checkout: zero sessions, and it looked like the app was failing to render.
337
+ *
338
+ * Declaring them is free when Vite would have found them anyway — but only when they are
339
+ * actually installed. Naming a package that is not there makes Vite log `Failed to resolve
340
+ * dependency: …, present in optimizeDeps.include` on every boot, which is a scary line pointing
341
+ * at Reticle for a problem that does not exist. SvelteKit apps hit exactly that: nothing in that
342
+ * tree depends on @testing-library/dom.
343
+ */
344
+ config(config) {
345
+ return {
346
+ // Expose the daemon's pairing token to hand-written connects in the same Vite app. The
347
+ // plugin's own injected connect gets the token directly, but a connect the USER writes —
348
+ // SvelteKit's client hook, a custom entry — had no way to reach a file only Node can read,
349
+ // so it called connect() with no credential and the bridge answered "authentication
350
+ // failed". Empty until the daemon has provisioned one; the page reloads once it has.
351
+ define: {
352
+ ...(config.define ?? {}),
353
+ [RETICLE_TOKEN_GLOBAL]: JSON.stringify(readPairingToken() ?? ''),
354
+ // Lets the SDK report React's absolute `_debugSource.fileName` as a repo-relative path,
355
+ // so source looks the same whichever React version an app is on.
356
+ [RETICLE_ROOT_GLOBAL]: JSON.stringify(config.root ?? process.cwd()),
357
+ },
358
+ optimizeDeps: {
359
+ // Part of the cache key, not of the build: changing it is what makes Vite notice that the
360
+ // SDK on disk is not the SDK it pre-bundled. See sdkBuildFingerprint.
361
+ esbuildOptions: {
362
+ ...(config.optimizeDeps?.esbuildOptions ?? {}),
363
+ define: {
364
+ ...(config.optimizeDeps?.esbuildOptions?.define ?? {}),
365
+ __RETICLE_SDK_BUILD__: JSON.stringify(sdkBuildFingerprint()),
366
+ },
367
+ },
368
+ include: [
369
+ ...(config.optimizeDeps?.include ?? []),
370
+ // The SDK ITSELF. Without this, Vite does not learn about @reticlehq/react until the
371
+ // injected connect module is requested — mid-flight, on the very first page load. It
372
+ // then pre-bundles it and forces a full reload, and the connect is lost in that reload:
373
+ // no WebSocket, no session, no console message. The FIRST load after `reticle init` —
374
+ // the one the whole product is judged on — silently did nothing, and it worked on the
375
+ // next refresh, which is the worst possible shape for a bug like this.
376
+ RETICLE_PACKAGE,
377
+ // Only if present — see above; naming an absent package produces a boot warning that
378
+ // blames Reticle for nothing.
379
+ ...[SDK_CJS_DEPS.TESTING_LIBRARY, SDK_CJS_DEPS.ARIA_QUERY].filter(isResolvable),
380
+ ],
381
+ },
382
+ };
383
+ },
109
384
  transform(code, id) {
110
- if (!sourceMapping || !shouldStamp(id))
385
+ // Desktop injection: prepend connect() to the HTML's own entry module. It is a REAL module, so
386
+ // its bare `@reticlehq/react` import resolves through the normal pipeline in both dev and
387
+ // build — which a virtual <script src> only ever did in dev.
388
+ if (desktop && inject && isHtmlEntry(id, htmlEntrySpecifier, root)) {
389
+ injected = true;
390
+ const withConnect = `${connectModuleSource(resolveLazy())}\n${code}`;
391
+ const stamped = sourceMapping && shouldStamp(id) ? stamp(withConnect, id) : null;
392
+ return stamped ?? { code: withConnect, map: null };
393
+ }
394
+ if (!sourceMapping)
395
+ return null;
396
+ // `.svelte` runs on the RAW component source, which is only still markup because this plugin
397
+ // declares `enforce: 'pre'` and therefore transforms before @sveltejs/vite-plugin-svelte. No
398
+ // map: the insertions are within a line and never move one, and a wrong map is worse than none.
399
+ if (shouldStampSvelte(id)) {
400
+ const stamped = stampSvelte(code, id);
401
+ return stamped === null ? null : { code: stamped, map: null };
402
+ }
403
+ if (!shouldStamp(id))
111
404
  return null;
112
405
  return stamp(code, id);
113
406
  },
114
- resolveId(id) {
407
+ resolveId(id, importer) {
408
+ // Desktop: remember the module the HTML points at, so `transform` can prepend connect() into
409
+ // it. A packaged build has no dev server, so the serve-time trick below — a <script src> at a
410
+ // virtual URL — would emit a tag pointing at a file that does not exist. That shipped an app
411
+ // with a dead script and NO instrumentation, which is worse than not injecting at all.
412
+ // `includes`, not `endsWith`: in a BUILD Vite rewrites the html entry through an html-proxy
413
+ // id (`/index.html?html-proxy&index=0.js`), so an endsWith check silently never matches and
414
+ // nothing is injected — which is exactly how this shipped a bundle with no connect() in it.
415
+ if (desktop && inject && importer !== undefined && importer.includes('.html')) {
416
+ htmlEntrySpecifier = id;
417
+ }
115
418
  // Return the id verbatim so Vite serves it back to load (the bare imports inside it then
116
419
  // go through normal resolution). No NUL prefix: the browser requests it as a URL.
117
420
  return inject && id === RETICLE_CONNECT_MODULE ? RETICLE_CONNECT_MODULE : null;
@@ -119,23 +422,49 @@ export function reticle(options = {}) {
119
422
  load(id) {
120
423
  if (!inject || id !== RETICLE_CONNECT_MODULE)
121
424
  return null;
122
- // Resolve the daemon port lazily per request (like the token): an explicit port wins; else find
123
- // the daemon serving THIS projectId in the discovery registry, so no hand-reconciled port. Falls
124
- // back to the default when nothing matches (connectArgs omits url ⇒ SDK uses the default).
125
- const port = resolved.port ?? discoverDaemonPort(resolved.projectId);
126
- const withPort = port !== undefined ? { ...resolved, port } : resolved;
127
- // Read the token lazily per request: by the time the browser loads the app the daemon is up and
128
- // has written it. An explicit token option still wins. Undefined ⇒ omitted (page reloads once up).
129
- const token = withPort.token ?? readPairingToken();
130
- return connectModuleSource(token !== undefined ? { ...withPort, token } : withPort);
425
+ // Resolved at load, not at config: the file may be created after the dev server starts.
426
+ const devModule = root === undefined ? null : findDevModule(root, existsSync);
427
+ return connectModuleSource(resolveLazy(), devModule);
428
+ },
429
+ configResolved(config) {
430
+ root = config.root;
431
+ command = config.command;
432
+ },
433
+ /**
434
+ * Desktop injection is silent when it misses — the bundle simply has no connect() in it and the
435
+ * app looks wired while reporting nothing. That happened twice while this was being built. A
436
+ * build that could not instrument must fail loudly instead of shipping a binary that lies.
437
+ */
438
+ buildEnd() {
439
+ if (!desktop || !inject || injected)
440
+ return;
441
+ throw new Error(notInjectedMessage());
131
442
  },
443
+ checkInjectedForTest: checkInjected,
132
444
  transformIndexHtml() {
133
- if (!inject)
445
+ // In serve, the HTML is sent BEFORE the browser requests the entry module, so the check has to
446
+ // be deferred — asserting here would fire on every healthy start. Unref'd so a dev server is
447
+ // never held open by it.
448
+ if (desktop && inject && command === 'serve') {
449
+ const timer = setTimeout(checkInjected, DEV_INJECTION_GRACE_MS);
450
+ timer.unref?.();
451
+ }
452
+ // Desktop injects via the entry module instead (see transform) — a tag here would be a dead
453
+ // URL in a packaged build.
454
+ if (!inject || desktop)
134
455
  return [];
135
456
  return [
457
+ // A CLASSIC inline script in <head>, and it has to be both.
458
+ //
459
+ // React reads `__REACT_DEVTOOLS_GLOBAL_HOOK__` when its renderer injects — which happens as
460
+ // soon as react-dom evaluates. A `type="module"` script runs in document order AFTER the
461
+ // app's entry module, so the connect module below can never install the hook in time.
462
+ // Measured on two independent Vite apps: the hook existed with our callback attached and
463
+ // `renderers.size === 0`, so the render meter counted zero forever while the docs advertised
464
+ // commit counts. This runs during parse, before any module, and the meter adopts its buffer.
465
+ { tag: 'script', children: RENDER_PREHOOK_SOURCE, injectTo: 'head-prepend' },
136
466
  { tag: 'script', attrs: { type: 'module', src: RETICLE_CONNECT_MODULE }, injectTo: 'body' },
137
467
  ];
138
468
  },
139
469
  };
140
470
  }
141
- //# sourceMappingURL=index.js.map
@@ -24,4 +24,3 @@ export declare function deriveProjectId(pkgName: string | undefined, rootPath: s
24
24
  * unit-tested without touching the real filesystem.
25
25
  */
26
26
  export declare function resolveProjectId(explicit: string | undefined, cwd: string, readPkgName?: (dir: string) => string | undefined): string;
27
- //# sourceMappingURL=project-id.d.ts.map
@@ -68,4 +68,3 @@ export function resolveProjectId(explicit, cwd, readPkgName = readNearestPackage
68
68
  return explicit;
69
69
  return deriveProjectId(readPkgName(cwd), cwd);
70
70
  }
71
- //# sourceMappingURL=project-id.js.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `data-reticle-source` for `.svelte` single-file components.
3
+ *
4
+ * A SvelteKit app onboards, connects, and gets DOM, network, console, routing and storage — and then
5
+ * every verdict comes back without the `file:line` the whole product leads with, because the JSX
6
+ * stamper is Babel and a `.svelte` file is not JavaScript. This is the Svelte half.
7
+ *
8
+ * ## Where in the pipeline
9
+ *
10
+ * A Vite `transform` on the RAW component source, in the Reticle plugin, which already declares
11
+ * `enforce: 'pre'` — so it runs before `@sveltejs/vite-plugin-svelte` and hands the compiler markup
12
+ * that already carries the attribute. Getting the order wrong is the failure mode worth naming: run
13
+ * it after, and there is no markup left to stamp, only generated JavaScript whose original line
14
+ * numbers are gone. A Svelte *preprocessor* would also work and would be equally correct, but it has
15
+ * to be wired into `svelte.config.js` by the user, and an integration nobody enables is an
16
+ * integration that does not exist.
17
+ *
18
+ * ## Why the compiler is loaded the way it is
19
+ *
20
+ * `svelte` must never become a dependency of this package: a React project installing the Vite
21
+ * plugin must not acquire a Svelte compiler, and its build must be unaffected. So the compiler is
22
+ * resolved lazily, from the APP's directory, only when a `.svelte` id actually arrives, and its
23
+ * absence returns null rather than throwing. Same shape as the optional `playwright` import in the
24
+ * server's pool launcher.
25
+ *
26
+ * Resolution is synchronous (`createRequire`) rather than `await import()` because Svelte ships a CJS
27
+ * build of its compiler and the plugin's `transform` is synchronous — making the whole hook async to
28
+ * fetch an optional dependency would change the contract of every other path through it.
29
+ *
30
+ * ## Scope
31
+ *
32
+ * The attribute only. Mapping a DOM node to the COMPONENT that rendered it — what `@reticlehq/react`
33
+ * does through the fiber tree — is a separate and much larger problem, and is not attempted here.
34
+ */
35
+ /** Files this module stamps. `.svelte.ts` (a runes module) is code, not markup — excluded. */
36
+ export declare const SVELTE_FILE: RegExp;
37
+ /** The part of `svelte/compiler` this module uses. Structural, so `svelte` stays un-imported. */
38
+ export interface SvelteCompilerLike {
39
+ parse: (source: string, options?: {
40
+ modern?: boolean;
41
+ }) => unknown;
42
+ }
43
+ /** How the compiler is obtained. Injected in tests; the default resolves it from the app. */
44
+ export type LoadSvelteCompiler = () => SvelteCompilerLike | null;
45
+ /**
46
+ * A character offset as Babel would report it: 1-based line, 0-based column.
47
+ *
48
+ * Matching Babel exactly is not cosmetic — `parseSourceAttr` in the browser SDK reads one format,
49
+ * and a Svelte pointer that was 0-based on line would land every verdict one line off while looking
50
+ * completely plausible.
51
+ */
52
+ export declare function offsetToLineColumn(source: string, offset: number): {
53
+ line: number;
54
+ column: number;
55
+ };
56
+ /** Project-relative, forward-slashed. A pointer must be the same string on Windows as on Linux. */
57
+ export declare function sourcePathFor(id: string, cwd?: string): string;
58
+ /**
59
+ * Stamp `data-reticle-source="file:line:column"` on every host element of a `.svelte` component.
60
+ *
61
+ * Returns null — never throws — when the compiler is absent or the component does not parse. In dev
62
+ * the transform runs on every keystroke, so a half-typed component is the NORMAL case; failing the
63
+ * build over one would make Reticle the reason the dev server is red, for a feature that is pure
64
+ * enrichment on top of an app that otherwise works.
65
+ */
66
+ export declare function stampSvelte(code: string, id: string, load?: LoadSvelteCompiler): string | null;
@@ -0,0 +1,166 @@
1
+ import { createRequire } from 'node:module';
2
+ import { relative } from 'node:path';
3
+ import { DATA_RETICLE_SOURCE_ATTR } from '@reticlehq/core';
4
+ /**
5
+ * `data-reticle-source` for `.svelte` single-file components.
6
+ *
7
+ * A SvelteKit app onboards, connects, and gets DOM, network, console, routing and storage — and then
8
+ * every verdict comes back without the `file:line` the whole product leads with, because the JSX
9
+ * stamper is Babel and a `.svelte` file is not JavaScript. This is the Svelte half.
10
+ *
11
+ * ## Where in the pipeline
12
+ *
13
+ * A Vite `transform` on the RAW component source, in the Reticle plugin, which already declares
14
+ * `enforce: 'pre'` — so it runs before `@sveltejs/vite-plugin-svelte` and hands the compiler markup
15
+ * that already carries the attribute. Getting the order wrong is the failure mode worth naming: run
16
+ * it after, and there is no markup left to stamp, only generated JavaScript whose original line
17
+ * numbers are gone. A Svelte *preprocessor* would also work and would be equally correct, but it has
18
+ * to be wired into `svelte.config.js` by the user, and an integration nobody enables is an
19
+ * integration that does not exist.
20
+ *
21
+ * ## Why the compiler is loaded the way it is
22
+ *
23
+ * `svelte` must never become a dependency of this package: a React project installing the Vite
24
+ * plugin must not acquire a Svelte compiler, and its build must be unaffected. So the compiler is
25
+ * resolved lazily, from the APP's directory, only when a `.svelte` id actually arrives, and its
26
+ * absence returns null rather than throwing. Same shape as the optional `playwright` import in the
27
+ * server's pool launcher.
28
+ *
29
+ * Resolution is synchronous (`createRequire`) rather than `await import()` because Svelte ships a CJS
30
+ * build of its compiler and the plugin's `transform` is synchronous — making the whole hook async to
31
+ * fetch an optional dependency would change the contract of every other path through it.
32
+ *
33
+ * ## Scope
34
+ *
35
+ * The attribute only. Mapping a DOM node to the COMPONENT that rendered it — what `@reticlehq/react`
36
+ * does through the fiber tree — is a separate and much larger problem, and is not attempted here.
37
+ */
38
+ /** Files this module stamps. `.svelte.ts` (a runes module) is code, not markup — excluded. */
39
+ export const SVELTE_FILE = /\.svelte$/;
40
+ /** Element node types that are a real place in the DOM, in Svelte 5's AST and Svelte 4's. */
41
+ const HOST_ELEMENT_TYPES = new Set(['RegularElement', 'Element']);
42
+ /** The `parent` back-reference some AST shapes carry; following it would walk forever. */
43
+ const PARENT_KEY = 'parent';
44
+ /** Resolved once per process: `null` means "looked, not installed" and must not be retried per file. */
45
+ let cachedCompiler;
46
+ function defaultLoadCompiler() {
47
+ if (cachedCompiler !== undefined)
48
+ return cachedCompiler;
49
+ cachedCompiler = null;
50
+ // From the APP's root first. The plugin may be linked, hoisted, or in a pnpm store far from the
51
+ // project, and the compiler that matters is the one the app's own Svelte plugin will use.
52
+ for (const from of [`${process.cwd()}/package.json`, import.meta.url]) {
53
+ try {
54
+ cachedCompiler = createRequire(from)('svelte/compiler');
55
+ break;
56
+ }
57
+ catch {
58
+ // Not resolvable from here — try the next origin, then give up silently.
59
+ }
60
+ }
61
+ return cachedCompiler;
62
+ }
63
+ /**
64
+ * A character offset as Babel would report it: 1-based line, 0-based column.
65
+ *
66
+ * Matching Babel exactly is not cosmetic — `parseSourceAttr` in the browser SDK reads one format,
67
+ * and a Svelte pointer that was 0-based on line would land every verdict one line off while looking
68
+ * completely plausible.
69
+ */
70
+ export function offsetToLineColumn(source, offset) {
71
+ let line = 1;
72
+ let lineStart = 0;
73
+ for (let i = 0; i < offset && i < source.length; i++) {
74
+ if (source[i] === '\n') {
75
+ line += 1;
76
+ lineStart = i + 1;
77
+ }
78
+ }
79
+ return { line, column: offset - lineStart };
80
+ }
81
+ /** Project-relative, forward-slashed. A pointer must be the same string on Windows as on Linux. */
82
+ export function sourcePathFor(id, cwd = process.cwd()) {
83
+ return relative(cwd, id).replace(/\\/g, '/');
84
+ }
85
+ function isElementNode(value) {
86
+ if (value === null || typeof value !== 'object')
87
+ return false;
88
+ const node = value;
89
+ return (typeof node.type === 'string' &&
90
+ HOST_ELEMENT_TYPES.has(node.type) &&
91
+ typeof node.name === 'string' &&
92
+ typeof node.start === 'number');
93
+ }
94
+ function isAlreadyStamped(node) {
95
+ return (node.attributes ?? []).some((attr) => attr !== null &&
96
+ typeof attr === 'object' &&
97
+ attr.name === DATA_RETICLE_SOURCE_ATTR);
98
+ }
99
+ /**
100
+ * Collect every host element in the AST.
101
+ *
102
+ * A GENERIC walk over the object graph rather than a switch over Svelte's block types. `{#if}`,
103
+ * `{#each}`, `{#await}`, `{#key}`, `{#snippet}` and whatever the next release adds each nest their
104
+ * children under a differently-named field, and a hand-written descent silently misses the ones it
105
+ * has not heard of — elements inside an `{#each}` would simply never be stamped, with nothing to
106
+ * indicate they were skipped.
107
+ */
108
+ function collectElements(root) {
109
+ const found = [];
110
+ const seen = new Set();
111
+ const visit = (value) => {
112
+ if (value === null || typeof value !== 'object' || seen.has(value))
113
+ return;
114
+ seen.add(value);
115
+ if (Array.isArray(value)) {
116
+ for (const item of value)
117
+ visit(item);
118
+ return;
119
+ }
120
+ if (isElementNode(value) && !isAlreadyStamped(value))
121
+ found.push(value);
122
+ for (const [key, child] of Object.entries(value)) {
123
+ if (key !== PARENT_KEY)
124
+ visit(child);
125
+ }
126
+ };
127
+ visit(root);
128
+ return found;
129
+ }
130
+ /**
131
+ * Stamp `data-reticle-source="file:line:column"` on every host element of a `.svelte` component.
132
+ *
133
+ * Returns null — never throws — when the compiler is absent or the component does not parse. In dev
134
+ * the transform runs on every keystroke, so a half-typed component is the NORMAL case; failing the
135
+ * build over one would make Reticle the reason the dev server is red, for a feature that is pure
136
+ * enrichment on top of an app that otherwise works.
137
+ */
138
+ export function stampSvelte(code, id, load = defaultLoadCompiler) {
139
+ const compiler = load();
140
+ if (compiler === null)
141
+ return null;
142
+ let ast;
143
+ try {
144
+ // `modern: true` selects Svelte 5's AST; Svelte 4 ignores the option and returns its own shape.
145
+ // `collectElements` accepts both, so one call covers either major without a version probe.
146
+ ast = compiler.parse(code, { modern: true });
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ const elements = collectElements(ast);
152
+ if (elements.length === 0)
153
+ return null;
154
+ const file = sourcePathFor(id);
155
+ // Insert from the LAST element backwards: every insertion shifts the offsets after it, and
156
+ // applying them in source order would put each stamp progressively further from its own tag.
157
+ const ordered = [...elements].sort((a, b) => b.start - a.start);
158
+ let out = code;
159
+ for (const node of ordered) {
160
+ const { line, column } = offsetToLineColumn(code, node.start);
161
+ const insertAt = node.start + 1 + node.name.length;
162
+ const attribute = ` ${DATA_RETICLE_SOURCE_ATTR}="${file}:${String(line)}:${String(column)}"`;
163
+ out = `${out.slice(0, insertAt)}${attribute}${out.slice(insertAt)}`;
164
+ }
165
+ return out;
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/vite-plugin",
3
- "version": "2.2.1",
3
+ "version": "2.4.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",
@@ -34,12 +34,13 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@babel/core": "^7.26.0",
37
- "@reticlehq/babel-plugin": "2.2.1",
38
- "@reticlehq/core": "2.2.1"
37
+ "@reticlehq/babel-plugin": "2.4.0",
38
+ "@reticlehq/core": "2.4.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/babel__core": "^7.20.5",
42
- "vite": "^7"
42
+ "svelte": "^5.56.8",
43
+ "vite": "^8"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "vite": ">=4"
@@ -1 +0,0 @@
1
- {"version":3,"file":"discover-port.d.ts","sourceRoot":"","sources":["../src/discover-port.ts"],"names":[],"mappings":"AA2BA;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,GAAE,MAAyC,EAC/C,KAAK,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAiB,GACxC,MAAM,GAAG,SAAS,CAoBpB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"discover-port.js","sourceRoot":"","sources":["../src/discover-port.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,UAAU,GAEX,MAAM,iBAAiB,CAAC;AAEzB,qGAAqG;AACrG,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,SAA6B,EAC7B,OAAe,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,IAAI,CAAC,EAC/C,QAAkC,OAAO;IAEzC,MAAM,OAAO,GAA0B,EAAE,CAAC;IAC1C,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,CAAC,oBAAoB;IACxC,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS;QAChD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,yBAAyB,CAAC,SAAS,CAChD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CACnD,CAAC;YACF,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,kCAAkC;QACpC,CAAC;IACH,CAAC;IACD,OAAO,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,SAAS,CAAC;AAChE,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,YAAY,CAAC;AAYlD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D,MAAM,WAAW,wBAAwB;IACvC,oGAAoG;IACpG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oGAAoG;IACpG,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oEAAoE;IACpE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,kHAAkH;AAClH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IACrF,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACzC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACpC,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EAAE,CAAC;CACjD;AAED,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;CAClB;AA0BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAUrD;AAaD,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CAG7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CAyCjF"}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,aAAa,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,CAAC,MAAM,wBAAwB,GAAG,SAAS,CAAC;AAElD,iGAAiG;AACjG,qGAAqG;AACrG,2DAA2D;AAC3D,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAC3C,sDAAsD;AACtD,MAAM,QAAQ,GAAG,WAAW,CAAC;AAC7B,8EAA8E;AAC9E,MAAM,cAAc,GAAG,IAAI,CAAC;AAC5B,MAAM,YAAY,GAAG,cAAc,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAqC1D,SAAS,WAAW,CAAC,EAAU;IAC7B,IAAI,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IAChD,IAAI,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,4EAA4E;IAC5E,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,KAAK,CAAC,IAAY,EAAE,EAAU;IACrC,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,EAAE;QAC9B,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,CAAC,aAAa,CAAC;QACxB,UAAU,EAAE,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE;QAC9C,UAAU,EAAE,IAAI;QAChB,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IACH,IAAI,GAAG,EAAE,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9D,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,GAAG,EAAE,GAAG,CAAC,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC;KAChF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC3D,MAAM,GAAG,GACP,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9F,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACpF,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,SAAS,WAAW,CAAC,OAAiC;IACpD,MAAM,IAAI,GAAoC,EAAE,CAAC;IACjD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,oBAAoB,CAAC;IAClD,IAAI,IAAI,KAAK,oBAAoB;QAAE,IAAI,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IACnE,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,IAAI,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IACrE,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;QAAE,IAAI,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;IAC3E,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,IAAI,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC;IAC/D,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAClE,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,mBAAmB,CAAC,OAAiC;IACnE,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,qCAAqC,eAAe,mCAAmC,IAAI,MAAM,CAAC;AAC3G,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,UAAoC,EAAE;IAC5D,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,KAAK,KAAK,CAAC;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC;IACxC,mGAAmG;IACnG,4DAA4D;IAC5D,MAAM,QAAQ,GAA6B;QACzC,GAAG,OAAO;QACV,SAAS,EAAE,gBAAgB,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;KAC9D,CAAC;IACF,OAAO;QACL,IAAI,EAAE,wBAAwB;QAC9B,KAAK,EAAE,OAAO;QACd,OAAO,EAAE,KAAK;QACd,SAAS,CAAC,IAAI,EAAE,EAAE;YAChB,IAAI,CAAC,aAAa,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;gBAAE,OAAO,IAAI,CAAC;YACpD,OAAO,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACzB,CAAC;QACD,SAAS,CAAC,EAAE;YACV,yFAAyF;YACzF,kFAAkF;YAClF,OAAO,MAAM,IAAI,EAAE,KAAK,sBAAsB,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,EAAE;YACL,IAAI,CAAC,MAAM,IAAI,EAAE,KAAK,sBAAsB;gBAAE,OAAO,IAAI,CAAC;YAC1D,gGAAgG;YAChG,iGAAiG;YACjG,2FAA2F;YAC3F,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACrE,MAAM,QAAQ,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;YACvE,gGAAgG;YAChG,mGAAmG;YACnG,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,IAAI,gBAAgB,EAAE,CAAC;YACnD,OAAO,mBAAmB,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACtF,CAAC;QACD,kBAAkB;YAChB,IAAI,CAAC,MAAM;gBAAE,OAAO,EAAE,CAAC;YACvB,OAAO;gBACL,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAsB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;aAC5F,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"project-id.d.ts","sourceRoot":"","sources":["../src/project-id.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,yGAAyG;AACzG,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIrF;AAyBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,GAAG,EAAE,MAAM,EACX,WAAW,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAkC,GACxE,MAAM,CAGR"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"project-id.js","sourceRoot":"","sources":["../src/project-id.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI;SACR,WAAW,EAAE;SACb,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;SACjB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAA2B,EAAE,QAAgB;IAC3E,MAAM,QAAQ,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,IAAI,KAAK,CAAC;IAC9F,OAAO,GAAG,IAAI,IAAI,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC1C,CAAC;AAED,kGAAkG;AAClG,SAAS,sBAAsB,CAAC,QAAgB;IAC9C,IAAI,GAAG,GAAG,QAAQ,CAAC;IACnB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC;QACxC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAC1C,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;gBAClE,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;oBAClD,MAAM,IAAI,GAAI,MAAkC,CAAC,MAAM,CAAC,CAAC;oBACzD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;wBAAE,OAAO,IAAI,CAAC;gBAC/D,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,4CAA4C;YAC9C,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM,CAAC,0BAA0B;QACrD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAA4B,EAC5B,GAAW,EACX,cAAmD,sBAAsB;IAEzE,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACnE,OAAO,eAAe,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;AAChD,CAAC"}