@reticlehq/vite-plugin 3.1.0 → 3.3.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/index.d.cts CHANGED
@@ -72,9 +72,8 @@ export interface ReticleVitePluginOptions {
72
72
  * This build is an Electron/Tauri renderer. Changes two things a desktop shell needs and a web app
73
73
  * must not get:
74
74
  *
75
- * - The plugin also applies to `vite build`. A packaged desktop renderer IS a production build
76
- * loaded from `file://` or a custom protocol — there is no dev server — so the default
77
- * `apply: 'serve'` drops the plugin entirely and the app ships with no `connect()` at all.
75
+ * - A packaged desktop renderer IS a production build with no dev server, so it needs the real
76
+ * `connect()` wiring during `vite build` — the web default's stub-swap must not apply to it.
78
77
  * - `connect()` is called with `allowInProduction`, because that same renderer reports
79
78
  * NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
80
79
  *
@@ -95,6 +94,20 @@ export interface ReticleVitePluginOptions {
95
94
  * Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`.
96
95
  */
97
96
  captureNetworkBodies?: boolean;
97
+ /**
98
+ * Per-body character cap for captured bodies. Default 8192; clamped to [256, 262144].
99
+ *
100
+ * Reachable here for the reason `captureNetworkBodies` is: the plugin is the only `connect()`
101
+ * most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
102
+ *
103
+ * Raise it to make a NEGATIVE `bodyContains` decidable -- a negation is checked over the whole
104
+ * payload, so on a list endpoint bigger than the cap it is permanently undecidable, and that is
105
+ * the class that proves "this dangerous field is absent from every row" (#799).
106
+ *
107
+ * Also settable as `VITE_RETICLE_BODY_MAX_CHARS=65536`, so one run can raise it without editing
108
+ * vite.config.
109
+ */
110
+ networkBodyMaxChars?: number;
98
111
  /**
99
112
  * Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
100
113
  *
@@ -195,8 +208,6 @@ export interface ReticleVitePlugin {
195
208
  };
196
209
  };
197
210
  };
198
- /** Absent in desktop mode, where the plugin must also run for `vite build`. */
199
- apply?: 'serve';
200
211
  enforce: 'pre';
201
212
  transform: (code: string, id: string) => {
202
213
  code: string;
@@ -313,14 +324,14 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
313
324
  * import { reticle } from '@reticlehq/vite-plugin';
314
325
  * export default defineConfig({ plugins: [react(), reticle()] });
315
326
  *
316
- * `apply: 'serve'` means Vite drops the plugin entirely from `vite build`, so a web production
317
- * bundle is never instrumented — gating is the tool's job, not a user-managed env check.
327
+ * The web plugin also builds, replacing the browser SDK with an inert stub so a default
328
+ * production bundle ships no Reticle runtime code at all.
318
329
  *
319
330
  * `desktop: true` is the ONE documented exception, and it inverts that guarantee deliberately: a
320
- * packaged Electron/Tauri renderer IS a production build with no dev server, so serve-only gating
321
- * would ship an app with no connect() at all. The cost is that the flag hands gating back to the
322
- * caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
323
- * a release binary.
331
+ * packaged Electron/Tauri renderer IS a production build with no dev server, so the web default's
332
+ * stub-replacement would ship an app with no connect() at all. The cost is that the flag hands
333
+ * gating back to the caller — keep it behind your own dev-only build target so an instrumented
334
+ * bundle can never reach a release binary.
324
335
  */
325
336
  /**
326
337
  * The daemon's journal directory, as a matcher every chokidar major honours.
package/dist/index.d.ts CHANGED
@@ -72,9 +72,8 @@ export interface ReticleVitePluginOptions {
72
72
  * This build is an Electron/Tauri renderer. Changes two things a desktop shell needs and a web app
73
73
  * must not get:
74
74
  *
75
- * - The plugin also applies to `vite build`. A packaged desktop renderer IS a production build
76
- * loaded from `file://` or a custom protocol — there is no dev server — so the default
77
- * `apply: 'serve'` drops the plugin entirely and the app ships with no `connect()` at all.
75
+ * - A packaged desktop renderer IS a production build with no dev server, so it needs the real
76
+ * `connect()` wiring during `vite build` — the web default's stub-swap must not apply to it.
78
77
  * - `connect()` is called with `allowInProduction`, because that same renderer reports
79
78
  * NODE_ENV=production and the SDK's prod backstop would otherwise refuse to start.
80
79
  *
@@ -95,6 +94,20 @@ export interface ReticleVitePluginOptions {
95
94
  * Also settable as `VITE_RETICLE_CAPTURE_BODIES=1`.
96
95
  */
97
96
  captureNetworkBodies?: boolean;
97
+ /**
98
+ * Per-body character cap for captured bodies. Default 8192; clamped to [256, 262144].
99
+ *
100
+ * Reachable here for the reason `captureNetworkBodies` is: the plugin is the only `connect()`
101
+ * most apps ever have, so an SDK option the plugin cannot pass is an option that does not exist.
102
+ *
103
+ * Raise it to make a NEGATIVE `bodyContains` decidable -- a negation is checked over the whole
104
+ * payload, so on a list endpoint bigger than the cap it is permanently undecidable, and that is
105
+ * the class that proves "this dangerous field is absent from every row" (#799).
106
+ *
107
+ * Also settable as `VITE_RETICLE_BODY_MAX_CHARS=65536`, so one run can raise it without editing
108
+ * vite.config.
109
+ */
110
+ networkBodyMaxChars?: number;
98
111
  /**
99
112
  * Retain a FAILED request's response body even with `captureNetworkBodies` off. Default true.
100
113
  *
@@ -195,8 +208,6 @@ export interface ReticleVitePlugin {
195
208
  };
196
209
  };
197
210
  };
198
- /** Absent in desktop mode, where the plugin must also run for `vite build`. */
199
- apply?: 'serve';
200
211
  enforce: 'pre';
201
212
  transform: (code: string, id: string) => {
202
213
  code: string;
@@ -313,14 +324,14 @@ export declare function connectModuleSource(options: ReticleVitePluginOptions, d
313
324
  * import { reticle } from '@reticlehq/vite-plugin';
314
325
  * export default defineConfig({ plugins: [react(), reticle()] });
315
326
  *
316
- * `apply: 'serve'` means Vite drops the plugin entirely from `vite build`, so a web production
317
- * bundle is never instrumented — gating is the tool's job, not a user-managed env check.
327
+ * The web plugin also builds, replacing the browser SDK with an inert stub so a default
328
+ * production bundle ships no Reticle runtime code at all.
318
329
  *
319
330
  * `desktop: true` is the ONE documented exception, and it inverts that guarantee deliberately: a
320
- * packaged Electron/Tauri renderer IS a production build with no dev server, so serve-only gating
321
- * would ship an app with no connect() at all. The cost is that the flag hands gating back to the
322
- * caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
323
- * a release binary.
331
+ * packaged Electron/Tauri renderer IS a production build with no dev server, so the web default's
332
+ * stub-replacement would ship an app with no connect() at all. The cost is that the flag hands
333
+ * gating back to the caller — keep it behind your own dev-only build target so an instrumented
334
+ * bundle can never reach a release binary.
324
335
  */
325
336
  /**
326
337
  * The daemon's journal directory, as a matcher every chokidar major honours.
package/dist/index.js CHANGED
@@ -3,14 +3,15 @@ import { missingTokenWarning } from './token/missing-token.js';
3
3
  import { ensurePairingToken } from './token/ensure-token.js';
4
4
  import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { transformSync } from '@babel/core';
7
- import reticleSource from '@reticlehq/babel-plugin';
8
- import { RETICLE_DEFAULT_PORT, RETICLE_RENDER_PREHOOK, bridgeWsUrl, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
6
+ import { RETICLE_RENDER_PREHOOK, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
9
7
  import { resolveProjectId } from './project-id.js';
10
8
  import { discoverDaemonPort } from './discover-port.js';
11
9
  import { announceDevServer } from './announce.js';
12
- import { SVELTE_FILE, stampSvelte } from './svelte-source.js';
10
+ import { stampSvelte } from './svelte-source.js';
11
+ import { ignoredFileNotice, optsOutOfStamping, shouldStamp, shouldStampSvelte, stamp, } from './stamping.js';
13
12
  import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, optimizerOptions, } from './installed.js';
13
+ import { RETICLE_DISABLED_STUB_CODE, RETICLE_DISABLED_STUB, isReticleDisabledWebBuild, } from './disabled-browser-stub.js';
14
+ import { connectArgs } from './connect-args.js';
14
15
  import { createInjectionWatch, notInjectedMessage, } from './injection-postcondition.js';
15
16
  import { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
16
17
  export { RETICLE_VITE_PLUGIN_NAME } from './plugin-name.js';
@@ -26,11 +27,6 @@ const RETICLE_SENSOR = '@reticlehq/browser';
26
27
  * file it lives in.
27
28
  */
28
29
  export const RETICLE_TOKEN_GLOBAL = '__RETICLE_TOKEN__';
29
- /** Files we stamp with source info — JSX/TSX only. */
30
- const JSX_FILE = /\.[jt]sx$/;
31
- /** Rollup virtual-module ids start with a NUL byte; never transform those. */
32
- const VIRTUAL_PREFIX = '\0';
33
- const NODE_MODULES = 'node_modules';
34
30
  /**
35
31
  * The connect code is served as a real module (not an inline <script>) so that Vite's import
36
32
  * pipeline resolves the bare `@reticlehq/react` specifier. An inline injected script is NOT run through
@@ -132,40 +128,6 @@ function isHtmlEntry(id, specifier, root) {
132
128
  // buildEnd post-condition means a wrong match cannot pass unnoticed as "nothing happened".
133
129
  return candidate.endsWith(target.startsWith('/') ? target : `/${target}`);
134
130
  }
135
- /** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
136
- function stampableId(id) {
137
- if (id.startsWith(VIRTUAL_PREFIX))
138
- return null;
139
- if (id.includes(NODE_MODULES))
140
- return null;
141
- // Strip any query suffix (?worker, ?raw,...) before matching the extension.
142
- return id.split('?')[0] ?? id;
143
- }
144
- function shouldStamp(id) {
145
- const clean = stampableId(id);
146
- return clean !== null && JSX_FILE.test(clean);
147
- }
148
- /** A `.svelte` single-file component, which needs the Svelte stamper rather than Babel. */
149
- function shouldStampSvelte(id) {
150
- const clean = stampableId(id);
151
- return clean !== null && SVELTE_FILE.test(clean);
152
- }
153
- function stamp(code, id) {
154
- const out = transformSync(code, {
155
- filename: id,
156
- plugins: [reticleSource],
157
- parserOpts: { plugins: ['jsx', 'typescript'] },
158
- sourceMaps: true,
159
- configFile: false,
160
- babelrc: false,
161
- });
162
- if (out?.code === undefined || null === out.code)
163
- return null;
164
- return {
165
- code: out.code,
166
- map: out.map === undefined || null === out.map ? null : JSON.stringify(out.map),
167
- };
168
- }
169
131
  /**
170
132
  * Read the daemon's auto-provisioned pairing token (~/.reticle/pairing-token, or the
171
133
  * RETICLE_PAIRING_TOKEN_DIR override) so the served app can present it. Node-side only — a browser
@@ -196,60 +158,6 @@ function warnIfTokenMissing(token) {
196
158
  }
197
159
  return token;
198
160
  }
199
- /** Build the `reticle.connect` argument literal — only includes keys the user set. */
200
- function connectArgs(options) {
201
- const args = {};
202
- const port = options.port ?? RETICLE_DEFAULT_PORT;
203
- if (port !== RETICLE_DEFAULT_PORT)
204
- args['url'] = bridgeWsUrl(port);
205
- if (options.session !== undefined)
206
- args['session'] = options.session;
207
- if (options.projectId !== undefined)
208
- args['projectId'] = options.projectId;
209
- if (options.token !== undefined)
210
- args['token'] = options.token;
211
- // Passed as connect ARGUMENTS, not as a `define`. A define substitutes a bare identifier in the
212
- // source it transforms; the SDK reads these as `globalThis[NAME]`, a dynamic lookup no define can
213
- // ever reach — so defining them looked right, shipped, and did nothing. Baking them into the
214
- // generated connect call is a literal in generated source: no bundler subtleties, works the same
215
- // in dev and in a desktop build.
216
- if (options.root !== undefined && options.root.length > 0)
217
- args['root'] = options.root;
218
- if (options.sdkVersion !== undefined && options.sdkVersion.length > 0) {
219
- args['sdkVersion'] = options.sdkVersion;
220
- }
221
- // A desktop renderer is a production build by construction; without this the SDK's prod backstop
222
- // refuses to connect and the app is silently uninstrumented.
223
- if (true === options.desktop)
224
- args['allowInProduction'] = true;
225
- // Env wins nothing — it only turns the flag ON, so a config that never set it can still be
226
- // switched on for one debugging session without editing vite.config and restarting the mental
227
- // model with it.
228
- if (true === options.captureNetworkBodies || '1' === process.env['VITE_RETICLE_CAPTURE_BODIES']) {
229
- args['captureNetworkBodies'] = true;
230
- }
231
- // Only the OPT-OUT is announced, never the default. The daemon reads absence as "unknown", so
232
- // sending `true` here would say nothing it does not already assume — while sending `false` is the
233
- // one fact a red verdict needs to stop prescribing a plugin this project has deliberately muted.
234
- if (false === options.sourceMapping)
235
- args['sourceMapping'] = false;
236
- // The one option that defaults ON, so the env var and the config flag both DISABLE rather than
237
- // enable. Emitted only when switched off; the default stays implicit in the SDK.
238
- if (false === options.captureErrorBodies || '1' === process.env['VITE_RETICLE_NO_ERROR_BODIES']) {
239
- args['captureErrorBodies'] = false;
240
- }
241
- // Same shape, same reason. Off unless asked for, in a config or for one session.
242
- if (true === options.exposePresenter || '1' === process.env['VITE_RETICLE_EXPOSE_PRESENTER']) {
243
- args['exposePresenter'] = true;
244
- }
245
- // Same shape, same reason: without it an app that cannot be served on localhost has no way to
246
- // reach the SDK option at all. The pairing token still applies — see the option's docstring.
247
- if (true === options.allowNonLocalhost ||
248
- '1' === process.env['VITE_RETICLE_ALLOW_NON_LOCALHOST']) {
249
- args['allowNonLocalhost'] = true;
250
- }
251
- return Object.keys(args).length > 0 ? JSON.stringify(args) : '';
252
- }
253
161
  /** The body of the connect module — real imports, resolved by Vite when the module is served. */
254
162
  /**
255
163
  * The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
@@ -333,14 +241,14 @@ export function connectModuleSource(options, devModule = null) {
333
241
  * import { reticle } from '@reticlehq/vite-plugin';
334
242
  * export default defineConfig({ plugins: [react(), reticle()] });
335
243
  *
336
- * `apply: 'serve'` means Vite drops the plugin entirely from `vite build`, so a web production
337
- * bundle is never instrumented — gating is the tool's job, not a user-managed env check.
244
+ * The web plugin also builds, replacing the browser SDK with an inert stub so a default
245
+ * production bundle ships no Reticle runtime code at all.
338
246
  *
339
247
  * `desktop: true` is the ONE documented exception, and it inverts that guarantee deliberately: a
340
- * packaged Electron/Tauri renderer IS a production build with no dev server, so serve-only gating
341
- * would ship an app with no connect() at all. The cost is that the flag hands gating back to the
342
- * caller — keep it behind your own dev-only build target so an instrumented bundle can never reach
343
- * a release binary.
248
+ * packaged Electron/Tauri renderer IS a production build with no dev server, so the web default's
249
+ * stub-replacement would ship an app with no connect() at all. The cost is that the flag hands
250
+ * gating back to the caller — keep it behind your own dev-only build target so an instrumented
251
+ * bundle can never reach a release binary.
344
252
  */
345
253
  /**
346
254
  * The daemon's journal directory, as a matcher every chokidar major honours.
@@ -378,6 +286,8 @@ export function reticle(options = {}) {
378
286
  const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
379
287
  /** Whether connect() actually reached a module — asserted at buildEnd, never assumed. */
380
288
  let injected = false;
289
+ /** Files already announced as opted out of stamping: one line each, for the life of the server. */
290
+ const announcedIgnored = new Set();
381
291
  /**
382
292
  * Whether Vite ever asked us to transform the app's HTML.
383
293
  *
@@ -426,10 +336,6 @@ export function reticle(options = {}) {
426
336
  });
427
337
  return {
428
338
  name: RETICLE_VITE_PLUGIN_NAME,
429
- // Web: serve-only, so a production bundle can never carry the SDK — gating is the tool's job.
430
- // Desktop: a packaged renderer IS a production build with no dev server, so the plugin must also
431
- // run for `vite build` or the shipped app has no connect() at all.
432
- ...(true === options.desktop ? {} : { apply: 'serve' }),
433
339
  enforce: 'pre',
434
340
  /**
435
341
  * Declare the SDK itself and the optimizer cache fingerprint.
@@ -511,6 +417,8 @@ export function reticle(options = {}) {
511
417
  };
512
418
  },
513
419
  transform(code, id) {
420
+ if (isReticleDisabledWebBuild(desktop, command))
421
+ return null;
514
422
  // Desktop injection: prepend connect() to the HTML's own entry module. It is a REAL module, so
515
423
  // its bare `@reticlehq/react` import resolves through the normal pipeline in both dev and
516
424
  // build — which a virtual <script src> only ever did in dev.
@@ -522,6 +430,16 @@ export function reticle(options = {}) {
522
430
  }
523
431
  if (!sourceMapping)
524
432
  return null;
433
+ // Decided HERE, ahead of both stampers, so the Svelte path honours the marker too and so the
434
+ // opt-out is announced once regardless of which stamper would have run. The Babel plugin reads
435
+ // the same marker on its own for the callers that reach it without Vite (#853).
436
+ if (optsOutOfStamping(code, id)) {
437
+ if (!announcedIgnored.has(id)) {
438
+ announcedIgnored.add(id);
439
+ warn(ignoredFileNotice(id));
440
+ }
441
+ return null;
442
+ }
525
443
  // `.svelte` runs on the RAW component source, which is only still markup because this plugin
526
444
  // declares `enforce: 'pre'` and therefore transforms before @sveltejs/vite-plugin-svelte. No
527
445
  // map: the insertions are within a line and never move one, and a wrong map is worse than none.
@@ -534,6 +452,9 @@ export function reticle(options = {}) {
534
452
  return stamp(code, id);
535
453
  },
536
454
  resolveId(id, importer) {
455
+ if (isReticleDisabledWebBuild(desktop, command) && id === RETICLE_SENSOR) {
456
+ return RETICLE_DISABLED_STUB;
457
+ }
537
458
  // Desktop: remember the module the HTML points at, so `transform` can prepend connect() into
538
459
  // it. A packaged build has no dev server, so the serve-time trick below — a <script src> at a
539
460
  // virtual URL — would emit a tag pointing at a file that does not exist. That shipped an app
@@ -549,6 +470,9 @@ export function reticle(options = {}) {
549
470
  return inject && id === RETICLE_CONNECT_MODULE ? RETICLE_CONNECT_MODULE : null;
550
471
  },
551
472
  load(id) {
473
+ if (isReticleDisabledWebBuild(desktop, command) && id === RETICLE_DISABLED_STUB) {
474
+ return RETICLE_DISABLED_STUB_CODE;
475
+ }
552
476
  if (!inject || id !== RETICLE_CONNECT_MODULE)
553
477
  return null;
554
478
  const source = currentConnectSource();
@@ -672,6 +596,8 @@ export function reticle(options = {}) {
672
596
  checkHtmlHookForTest: watch.checkHtmlHookRan,
673
597
  injectionWatchForTest: watch,
674
598
  transformIndexHtml() {
599
+ if (isReticleDisabledWebBuild(desktop, command))
600
+ return [];
675
601
  htmlTransformed = true;
676
602
  if (desktop && inject && 'serve' === command)
677
603
  watch.armDesktopCheck();
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Which files get a `data-reticle-source` stamp, and how.
3
+ *
4
+ * Split out of index.ts when that file crossed the line cap while gaining the per-file opt-out.
5
+ * This is one concern — a module id comes in, and either a stamper runs on it, nothing does, or the
6
+ * file has asked to be left alone — and it changes for one reason (the source-pointer feature)
7
+ * rather than for the dozen the plugin's lifecycle does. The Svelte stamper is its sibling module;
8
+ * the JSX one is `@reticlehq/babel-plugin`, which also reads the opt-out on its own for the callers
9
+ * that reach it without Vite.
10
+ */
11
+ /** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
12
+ export declare function stampableId(id: string): string | null;
13
+ export declare function shouldStamp(id: string): boolean;
14
+ /** A `.svelte` single-file component, which needs the Svelte stamper rather than Babel. */
15
+ export declare function shouldStampSvelte(id: string): boolean;
16
+ export declare function stamp(code: string, id: string): {
17
+ code: string;
18
+ map: string | null;
19
+ } | null;
20
+ /**
21
+ * Has this file asked not to be stamped?
22
+ *
23
+ * Decided ahead of BOTH stampers, so the Svelte path honours the marker too and so the opt-out is
24
+ * announced once regardless of which stamper would have run. Only a file one of them would have
25
+ * touched can opt out — an id that was never stampable has nothing to announce (#853).
26
+ */
27
+ export declare function optsOutOfStamping(code: string, id: string): boolean;
28
+ /**
29
+ * The one line said when a file opts out of source stamping with `@reticle-ignore`.
30
+ *
31
+ * Not silent, on purpose. An opt-out nobody is told about is an opt-out somebody forgets, and the
32
+ * `file:line` it would have produced quietly stops resolving months later with nothing to point at
33
+ * the comment. Said once per file, in the dev-server output where the person who added the comment
34
+ * will see it. Exported for the test that pins the wording.
35
+ */
36
+ export declare function ignoredFileNotice(id: string): string;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Which files get a `data-reticle-source` stamp, and how.
3
+ *
4
+ * Split out of index.ts when that file crossed the line cap while gaining the per-file opt-out.
5
+ * This is one concern — a module id comes in, and either a stamper runs on it, nothing does, or the
6
+ * file has asked to be left alone — and it changes for one reason (the source-pointer feature)
7
+ * rather than for the dozen the plugin's lifecycle does. The Svelte stamper is its sibling module;
8
+ * the JSX one is `@reticlehq/babel-plugin`, which also reads the opt-out on its own for the callers
9
+ * that reach it without Vite.
10
+ */
11
+ import { transformSync } from '@babel/core';
12
+ import reticleSource from '@reticlehq/babel-plugin';
13
+ import { isSourceStampingIgnored } from '@reticlehq/babel-plugin/ignore';
14
+ import { RETICLE_IGNORE_MARKER } from '@reticlehq/core';
15
+ import { SVELTE_FILE } from './svelte-source.js';
16
+ /** Files we stamp with source info — JSX/TSX only. */
17
+ const JSX_FILE = /\.[jt]sx$/;
18
+ /** Rollup virtual-module ids start with a NUL byte; never transform those. */
19
+ const VIRTUAL_PREFIX = '\0';
20
+ const NODE_MODULES = 'node_modules';
21
+ /** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
22
+ export function stampableId(id) {
23
+ if (id.startsWith(VIRTUAL_PREFIX))
24
+ return null;
25
+ if (id.includes(NODE_MODULES))
26
+ return null;
27
+ // Strip any query suffix (?worker, ?raw,...) before matching the extension.
28
+ return id.split('?')[0] ?? id;
29
+ }
30
+ /**
31
+ * A test file, by the names every JS test runner agrees on.
32
+ *
33
+ * `.test.`/`.spec.` before the extension, or anywhere under a `__tests__` directory. Anchored to a
34
+ * path SEGMENT and a dot so `TestBanner.tsx`, `latest.tsx` and `contest.tsx` stay ordinary
35
+ * components — they are somebody's app, and skipping them would silently cost source mapping.
36
+ */
37
+ const TEST_FILE = /(?:\.(?:test|spec)\.[^./]+$)|(?:^|\/)__tests__\//;
38
+ export function shouldStamp(id) {
39
+ const clean = stampableId(id);
40
+ if (null === clean)
41
+ return false;
42
+ /*
43
+ * A test file is not the app under test.
44
+ *
45
+ * Nothing reads `data-reticle-source` on one — an agent inspects the app's DOM, never a test's —
46
+ * so stamping them is pure cost, charged per file on every run, and reported from the field as
47
+ * nearly doubling a large jsdom suite and failing a test that passed without instrumentation:
48
+ * inserting attributes into JSX is exactly what an assertion on rendered output notices (#995).
49
+ *
50
+ * Cut on the NAME rather than on "are we under Vitest": `vitest-browser.ts` records what happened
51
+ * the last time that was cut on the `VITEST` env var, which also reads true when a Vitest suite
52
+ * boots an app in order to test it. A name check cannot make that mistake.
53
+ */
54
+ if (TEST_FILE.test(clean))
55
+ return false;
56
+ return JSX_FILE.test(clean);
57
+ }
58
+ /** A `.svelte` single-file component, which needs the Svelte stamper rather than Babel. */
59
+ export function shouldStampSvelte(id) {
60
+ const clean = stampableId(id);
61
+ return clean !== null && SVELTE_FILE.test(clean);
62
+ }
63
+ export function stamp(code, id) {
64
+ const out = transformSync(code, {
65
+ filename: id,
66
+ plugins: [reticleSource],
67
+ parserOpts: { plugins: ['jsx', 'typescript'] },
68
+ sourceMaps: true,
69
+ configFile: false,
70
+ babelrc: false,
71
+ });
72
+ if (out?.code === undefined || null === out.code)
73
+ return null;
74
+ return {
75
+ code: out.code,
76
+ map: out.map === undefined || null === out.map ? null : JSON.stringify(out.map),
77
+ };
78
+ }
79
+ /**
80
+ * Has this file asked not to be stamped?
81
+ *
82
+ * Decided ahead of BOTH stampers, so the Svelte path honours the marker too and so the opt-out is
83
+ * announced once regardless of which stamper would have run. Only a file one of them would have
84
+ * touched can opt out — an id that was never stampable has nothing to announce (#853).
85
+ */
86
+ export function optsOutOfStamping(code, id) {
87
+ return (shouldStamp(id) || shouldStampSvelte(id)) && isSourceStampingIgnored(code);
88
+ }
89
+ /**
90
+ * The one line said when a file opts out of source stamping with `@reticle-ignore`.
91
+ *
92
+ * Not silent, on purpose. An opt-out nobody is told about is an opt-out somebody forgets, and the
93
+ * `file:line` it would have produced quietly stops resolving months later with nothing to point at
94
+ * the comment. Said once per file, in the dev-server output where the person who added the comment
95
+ * will see it. Exported for the test that pins the wording.
96
+ */
97
+ export function ignoredFileNotice(id) {
98
+ return `[reticle] ${id} opts out of source stamping (${RETICLE_IGNORE_MARKER} on its first line) — elements in it will have no file:line.`;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/vite-plugin",
3
- "version": "3.1.0",
3
+ "version": "3.3.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.1.0",
45
- "@reticlehq/core": "3.1.0"
44
+ "@reticlehq/babel-plugin": "3.3.0",
45
+ "@reticlehq/core": "3.3.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/babel__core": "^7.20.5",
49
49
  "esbuild": "^0.28.2",
50
50
  "svelte": "^5.56.10",
51
51
  "vite": "^8",
52
- "@reticlehq/react": "3.1.0"
52
+ "@reticlehq/react": "3.3.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "vite": ">=4"