@reticlehq/vite-plugin 2.3.0 → 2.5.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/discover-port.js +1 -1
- package/dist/ensure-token.d.ts +1 -0
- package/dist/ensure-token.js +47 -0
- package/dist/index.d.ts +58 -1
- package/dist/index.js +184 -28
- package/dist/installed.d.ts +70 -0
- package/dist/installed.js +184 -0
- package/dist/missing-token.d.ts +12 -0
- package/dist/missing-token.js +19 -0
- package/dist/project-id.d.ts +5 -6
- package/dist/project-id.js +11 -19
- package/dist/svelte-source.d.ts +65 -0
- package/dist/svelte-source.js +166 -0
- package/package.json +8 -5
package/dist/discover-port.js
CHANGED
|
@@ -33,7 +33,7 @@ export function discoverDaemonPort(projectId, home = join(homedir(), ReticleDir.
|
|
|
33
33
|
return undefined; // no ~/.reticle yet
|
|
34
34
|
}
|
|
35
35
|
for (const file of files) {
|
|
36
|
-
if (daemonRegistryPort(file)
|
|
36
|
+
if (null === daemonRegistryPort(file))
|
|
37
37
|
continue;
|
|
38
38
|
try {
|
|
39
39
|
const parsed = DaemonRegistryEntrySchema.safeParse(JSON.parse(readFileSync(join(home, file), 'utf8')));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensurePairingToken(dir: string): string | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the pairing token, or create it — whichever process gets there first.
|
|
3
|
+
*
|
|
4
|
+
* The token is read ONCE, when Vite resolves its config, and inlined as `__RETICLE_TOKEN__`. The
|
|
5
|
+
* daemon is what normally writes it. So a dev server started BEFORE the daemon froze an empty
|
|
6
|
+
* string, every page it served was refused by the bridge, and no session ever appeared — with the
|
|
7
|
+
* SDK loading and the socket opening, so nothing looked broken. Bisected against a real SvelteKit
|
|
8
|
+
* fixture, where it presented as a regression.
|
|
9
|
+
*
|
|
10
|
+
* Restarting the dev server was the only cure, because by the time the daemon minted a token the
|
|
11
|
+
* empty value had already been baked in.
|
|
12
|
+
*
|
|
13
|
+
* The daemon READS-OR-CREATES this file (see the server's `readOrCreatePairingToken`, kept "stable
|
|
14
|
+
* across restarts so a plugin-injected page keeps working after the daemon bounces"), so the plugin
|
|
15
|
+
* doing the same makes the two agree whichever order they start in. Same path, same 0600 mode, same
|
|
16
|
+
* random generation. An existing token is never replaced: overwriting would invalidate every page
|
|
17
|
+
* the daemon has already handed one to.
|
|
18
|
+
*/
|
|
19
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { randomBytes } from 'node:crypto';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
/** Matches the server's token size. */
|
|
23
|
+
const TOKEN_BYTES = 32;
|
|
24
|
+
const TOKEN_FILE = 'pairing-token';
|
|
25
|
+
export function ensurePairingToken(dir) {
|
|
26
|
+
try {
|
|
27
|
+
const existing = readFileSync(join(dir, TOKEN_FILE), 'utf8').trim();
|
|
28
|
+
if (existing.length > 0)
|
|
29
|
+
return existing;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* missing or unreadable — fall through and create one */
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const token = randomBytes(TOKEN_BYTES).toString('hex');
|
|
36
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
37
|
+
const path = join(dir, TOKEN_FILE);
|
|
38
|
+
writeFileSync(path, token, { encoding: 'utf8', mode: 0o600 });
|
|
39
|
+
// writeFile keeps a pre-existing looser mode, so set it explicitly — same as the daemon does.
|
|
40
|
+
chmodSync(path, 0o600);
|
|
41
|
+
return token;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// A dev server must still start. Degrading to the previous tokenless behaviour is correct.
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
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
|
|
@@ -18,6 +24,16 @@ export declare const RENDER_PREHOOK_SOURCE = "(function(){try{\nvar K='__REACT_D
|
|
|
18
24
|
export interface ReticleVitePluginOptions {
|
|
19
25
|
/** Bridge WebSocket port. Defaults to the SDK default; only baked into connect when non-default. */
|
|
20
26
|
port?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Project root, so React's absolute `_debugSource.fileName` reports repo-relative. Resolved from
|
|
29
|
+
* the Vite config at injection time; set it only to override.
|
|
30
|
+
*/
|
|
31
|
+
root?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The installed SDK's version, so a pair skewed against the daemon can name itself instead of
|
|
34
|
+
* surfacing as a bare -32000. Read from the installed package; set it only to override.
|
|
35
|
+
*/
|
|
36
|
+
sdkVersion?: string;
|
|
21
37
|
/** Stable session label for the bridge. Defaults to the SDK's auto-generated id. */
|
|
22
38
|
session?: string;
|
|
23
39
|
/**
|
|
@@ -79,11 +95,18 @@ export interface ReticleVitePlugin {
|
|
|
79
95
|
config?: (config: {
|
|
80
96
|
optimizeDeps?: {
|
|
81
97
|
include?: string[];
|
|
98
|
+
esbuildOptions?: {
|
|
99
|
+
define?: Record<string, string>;
|
|
100
|
+
};
|
|
82
101
|
};
|
|
102
|
+
define?: Record<string, string>;
|
|
103
|
+
root?: string;
|
|
83
104
|
}) => {
|
|
84
105
|
optimizeDeps: {
|
|
85
106
|
include: string[];
|
|
107
|
+
[optionsKey: string]: unknown;
|
|
86
108
|
};
|
|
109
|
+
define: Record<string, string>;
|
|
87
110
|
};
|
|
88
111
|
/** Absent in desktop mode, where the plugin must also run for `vite build`. */
|
|
89
112
|
apply?: 'serve';
|
|
@@ -114,9 +137,43 @@ interface HtmlTag {
|
|
|
114
137
|
/** `head-prepend` is required for the render pre-hook: it must run before any module script. */
|
|
115
138
|
injectTo: 'body' | 'head-prepend';
|
|
116
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Name each CJS dep ONLY in the bare form, and only when the app root can resolve it.
|
|
142
|
+
*
|
|
143
|
+
* This used to try three layouts and emit Vite's nested `a > b > c` form when a hoisted lookup
|
|
144
|
+
* failed. The guard asked the wrong question: it tested NODE resolvability, walking the chain
|
|
145
|
+
* segment by segment, and under pnpm that succeeds exactly where Vite fails. Measured on the
|
|
146
|
+
* sveltekit fixture:
|
|
147
|
+
*
|
|
148
|
+
* ['@testing-library/dom'] -> null
|
|
149
|
+
* ['@reticlehq/browser', '@testing-library/dom'] -> null
|
|
150
|
+
* ['@reticlehq/react', '@reticlehq/browser', '@testing-library/dom'] -> emitted
|
|
151
|
+
*
|
|
152
|
+
* So we emitted the three-segment chain, Vite could not follow it, and the boot warning this
|
|
153
|
+
* function exists to prevent appeared anyway — `Failed to resolve dependency: …, present in
|
|
154
|
+
* optimizeDeps.include`, pointing at Reticle, naming a package the developer has never heard of,
|
|
155
|
+
* and forcing a full re-optimization on every cold start. The comment stated the rule correctly and
|
|
156
|
+
* the code broke it.
|
|
157
|
+
*
|
|
158
|
+
* Dropping the nested form loses nothing that matters: the SDK itself is still pre-bundled, and Vite
|
|
159
|
+
* follows its imports when it does that, so these deps are handled as part of it. Naming them
|
|
160
|
+
* separately was belt-and-braces for a locally-aliased SDK, where the bare form resolves anyway.
|
|
161
|
+
*/
|
|
162
|
+
export declare function cjsDepIncludes(appRoot: string, canResolve?: (dep: string) => boolean): string[];
|
|
117
163
|
export declare function readPairingToken(): string | undefined;
|
|
118
164
|
/** The body of the connect module — real imports, resolved by Vite when the module is served. */
|
|
119
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
|
|
167
|
+
*
|
|
168
|
+
* It is imported by CONVENTION rather than by patching the app's entry file. The connect is injected
|
|
169
|
+
* into a virtual module, so there is nowhere for a user to add these calls without `init` editing
|
|
170
|
+
* `src/main.tsx` — an edit to the file people actually own, for something that is opt-in enrichment.
|
|
171
|
+
* Convention costs one `existsSync` and leaves their entry untouched.
|
|
172
|
+
*/
|
|
173
|
+
export declare const RETICLE_DEV_MODULE_CANDIDATES: readonly ["src/reticle-dev.ts", "src/reticle-dev.js", "src/reticle-dev.tsx", "src/reticle-dev.jsx"];
|
|
174
|
+
/** The app's dev module, as an importable path — or null when the app has none. */
|
|
175
|
+
export declare function findDevModule(root: string, exists: (p: string) => boolean): string | null;
|
|
176
|
+
export declare function connectModuleSource(options: ReticleVitePluginOptions, devModule?: string | null): string;
|
|
120
177
|
/**
|
|
121
178
|
* Reticle Vite plugin. Add to your `plugins` array and the entire integration is done:
|
|
122
179
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { missingTokenWarning } from './missing-token.js';
|
|
3
|
+
import { ensurePairingToken } from './ensure-token.js';
|
|
2
4
|
import { homedir } from 'node:os';
|
|
3
5
|
import { join } from 'node:path';
|
|
4
6
|
import { transformSync } from '@babel/core';
|
|
5
7
|
import reticleSource from '@reticlehq/babel-plugin';
|
|
6
|
-
import { RETICLE_DEFAULT_PORT, RETICLE_RENDER_PREHOOK, bridgeWsUrl, ReticleDir, ReticleEnv, } from '@reticlehq/core';
|
|
8
|
+
import { RETICLE_DEFAULT_PORT, RETICLE_RENDER_PREHOOK, bridgeWsUrl, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
|
|
7
9
|
import { resolveProjectId } from './project-id.js';
|
|
8
10
|
import { discoverDaemonPort } from './discover-port.js';
|
|
11
|
+
import { SVELTE_FILE, stampSvelte } from './svelte-source.js';
|
|
12
|
+
import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, } from './installed.js';
|
|
9
13
|
export const RETICLE_VITE_PLUGIN_NAME = 'reticle';
|
|
10
14
|
// The React kit the host app imports the SDK from. It re-exports the browser sensor, so a single
|
|
11
15
|
// specifier yields both `reticle` (connect) and `install` (the React adapter). NOT `@reticlehq/core`
|
|
12
16
|
// — that is the isomorphic foundation and exports neither.
|
|
13
17
|
const RETICLE_PACKAGE = '@reticlehq/react';
|
|
18
|
+
/**
|
|
19
|
+
* Compile-time global carrying the daemon's pairing token, for connects the plugin does not write
|
|
20
|
+
* itself. The bridge requires the token even on localhost, and nothing in a browser can read the
|
|
21
|
+
* file it lives in.
|
|
22
|
+
*/
|
|
23
|
+
export const RETICLE_TOKEN_GLOBAL = '__RETICLE_TOKEN__';
|
|
14
24
|
/** Files we stamp with source info — JSX/TSX only. */
|
|
15
25
|
const JSX_FILE = /\.[jt]sx$/;
|
|
16
26
|
/** Rollup virtual-module ids start with a NUL byte; never transform those. */
|
|
@@ -77,14 +87,23 @@ function isHtmlEntry(id, specifier, root) {
|
|
|
77
87
|
// buildEnd post-condition means a wrong match cannot pass unnoticed as "nothing happened".
|
|
78
88
|
return candidate.endsWith(target.startsWith('/') ? target : `/${target}`);
|
|
79
89
|
}
|
|
80
|
-
|
|
90
|
+
/** A module id we may stamp at all: not virtual, not a dependency. Extension decides which stamper. */
|
|
91
|
+
function stampableId(id) {
|
|
81
92
|
if (id.startsWith(VIRTUAL_PREFIX))
|
|
82
|
-
return
|
|
93
|
+
return null;
|
|
83
94
|
if (id.includes(NODE_MODULES))
|
|
84
|
-
return
|
|
95
|
+
return null;
|
|
85
96
|
// Strip any query suffix (?worker, ?raw,...) before matching the extension.
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
return id.split('?')[0] ?? id;
|
|
98
|
+
}
|
|
99
|
+
function shouldStamp(id) {
|
|
100
|
+
const clean = stampableId(id);
|
|
101
|
+
return clean !== null && JSX_FILE.test(clean);
|
|
102
|
+
}
|
|
103
|
+
/** A `.svelte` single-file component, which needs the Svelte stamper rather than Babel. */
|
|
104
|
+
function shouldStampSvelte(id) {
|
|
105
|
+
const clean = stampableId(id);
|
|
106
|
+
return clean !== null && SVELTE_FILE.test(clean);
|
|
88
107
|
}
|
|
89
108
|
function stamp(code, id) {
|
|
90
109
|
const out = transformSync(code, {
|
|
@@ -95,11 +114,11 @@ function stamp(code, id) {
|
|
|
95
114
|
configFile: false,
|
|
96
115
|
babelrc: false,
|
|
97
116
|
});
|
|
98
|
-
if (out?.code === undefined || out.code
|
|
117
|
+
if (out?.code === undefined || null === out.code)
|
|
99
118
|
return null;
|
|
100
119
|
return {
|
|
101
120
|
code: out.code,
|
|
102
|
-
map: out.map === undefined || out.map
|
|
121
|
+
map: out.map === undefined || null === out.map ? null : JSON.stringify(out.map),
|
|
103
122
|
};
|
|
104
123
|
}
|
|
105
124
|
/**
|
|
@@ -116,16 +135,58 @@ const SDK_CJS_DEPS = {
|
|
|
116
135
|
TESTING_LIBRARY: '@testing-library/dom',
|
|
117
136
|
ARIA_QUERY: 'aria-query',
|
|
118
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* Name each CJS dep ONLY in the bare form, and only when the app root can resolve it.
|
|
140
|
+
*
|
|
141
|
+
* This used to try three layouts and emit Vite's nested `a > b > c` form when a hoisted lookup
|
|
142
|
+
* failed. The guard asked the wrong question: it tested NODE resolvability, walking the chain
|
|
143
|
+
* segment by segment, and under pnpm that succeeds exactly where Vite fails. Measured on the
|
|
144
|
+
* sveltekit fixture:
|
|
145
|
+
*
|
|
146
|
+
* ['@testing-library/dom'] -> null
|
|
147
|
+
* ['@reticlehq/browser', '@testing-library/dom'] -> null
|
|
148
|
+
* ['@reticlehq/react', '@reticlehq/browser', '@testing-library/dom'] -> emitted
|
|
149
|
+
*
|
|
150
|
+
* So we emitted the three-segment chain, Vite could not follow it, and the boot warning this
|
|
151
|
+
* function exists to prevent appeared anyway — `Failed to resolve dependency: …, present in
|
|
152
|
+
* optimizeDeps.include`, pointing at Reticle, naming a package the developer has never heard of,
|
|
153
|
+
* and forcing a full re-optimization on every cold start. The comment stated the rule correctly and
|
|
154
|
+
* the code broke it.
|
|
155
|
+
*
|
|
156
|
+
* Dropping the nested form loses nothing that matters: the SDK itself is still pre-bundled, and Vite
|
|
157
|
+
* follows its imports when it does that, so these deps are handled as part of it. Naming them
|
|
158
|
+
* separately was belt-and-braces for a locally-aliased SDK, where the bare form resolves anyway.
|
|
159
|
+
*/
|
|
160
|
+
export function cjsDepIncludes(appRoot,
|
|
161
|
+
// Injected so the rule can be tested hermetically. Real module resolution is not: under vitest,
|
|
162
|
+
// `createRequire` from a directory that does not exist still resolves packages out of the runner's
|
|
163
|
+
// own graph, so a filesystem-based test of "nothing is reachable here" silently asserts nothing.
|
|
164
|
+
canResolve = (dep) => null !== resolvableChain([dep], appRoot)) {
|
|
165
|
+
return [SDK_CJS_DEPS.TESTING_LIBRARY, SDK_CJS_DEPS.ARIA_QUERY].filter(canResolve);
|
|
166
|
+
}
|
|
119
167
|
export function readPairingToken() {
|
|
120
168
|
const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
|
|
121
169
|
const dir = override !== undefined && override.length > 0 ? override : join(homedir(), ReticleDir.ROOT);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
170
|
+
// Read-or-CREATE, matching the daemon. Reading alone meant a dev server started before the daemon
|
|
171
|
+
// baked in an empty token and every page it served was refused — with the SDK loading and the
|
|
172
|
+
// socket opening, so nothing looked broken. See ensure-token for the bisect.
|
|
173
|
+
return ensurePairingToken(dir);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Pass the token through, saying so once when it is absent.
|
|
177
|
+
*
|
|
178
|
+
* Warned HERE rather than at connect time because this is the moment the value is frozen: by the
|
|
179
|
+
* time the app is refused, the empty string was inlined minutes ago and restarting the dev server is
|
|
180
|
+
* the only fix. Once per config resolve, so a watch-mode rebuild does not repeat it.
|
|
181
|
+
*/
|
|
182
|
+
let tokenWarned = false;
|
|
183
|
+
function warnIfTokenMissing(token) {
|
|
184
|
+
const warning = missingTokenWarning(token);
|
|
185
|
+
if (warning !== undefined && !tokenWarned) {
|
|
186
|
+
tokenWarned = true;
|
|
187
|
+
console.warn(warning);
|
|
128
188
|
}
|
|
189
|
+
return token;
|
|
129
190
|
}
|
|
130
191
|
/** Build the `reticle.connect` argument literal — only includes keys the user set. */
|
|
131
192
|
function connectArgs(options) {
|
|
@@ -139,22 +200,57 @@ function connectArgs(options) {
|
|
|
139
200
|
args['projectId'] = options.projectId;
|
|
140
201
|
if (options.token !== undefined)
|
|
141
202
|
args['token'] = options.token;
|
|
203
|
+
// Passed as connect ARGUMENTS, not as a `define`. A define substitutes a bare identifier in the
|
|
204
|
+
// source it transforms; the SDK reads these as `globalThis[NAME]`, a dynamic lookup no define can
|
|
205
|
+
// ever reach — so defining them looked right, shipped, and did nothing. Baking them into the
|
|
206
|
+
// generated connect call is a literal in generated source: no bundler subtleties, works the same
|
|
207
|
+
// in dev and in a desktop build.
|
|
208
|
+
if (options.root !== undefined && options.root.length > 0)
|
|
209
|
+
args['root'] = options.root;
|
|
210
|
+
if (options.sdkVersion !== undefined && options.sdkVersion.length > 0) {
|
|
211
|
+
args['sdkVersion'] = options.sdkVersion;
|
|
212
|
+
}
|
|
142
213
|
// A desktop renderer is a production build by construction; without this the SDK's prod backstop
|
|
143
214
|
// refuses to connect and the app is silently uninstrumented.
|
|
144
|
-
if (options.desktop
|
|
215
|
+
if (true === options.desktop)
|
|
145
216
|
args['allowInProduction'] = true;
|
|
146
217
|
// Env wins nothing — it only turns the flag ON, so a config that never set it can still be
|
|
147
218
|
// switched on for one debugging session without editing vite.config and restarting the mental
|
|
148
219
|
// model with it.
|
|
149
|
-
if (options.captureNetworkBodies
|
|
220
|
+
if (true === options.captureNetworkBodies || '1' === process.env['VITE_RETICLE_CAPTURE_BODIES']) {
|
|
150
221
|
args['captureNetworkBodies'] = true;
|
|
151
222
|
}
|
|
152
223
|
return Object.keys(args).length > 0 ? JSON.stringify(args) : '';
|
|
153
224
|
}
|
|
154
225
|
/** The body of the connect module — real imports, resolved by Vite when the module is served. */
|
|
155
|
-
|
|
226
|
+
/**
|
|
227
|
+
* The conventional app-side dev module: `registerStore` / `registerCapabilities` live here.
|
|
228
|
+
*
|
|
229
|
+
* It is imported by CONVENTION rather than by patching the app's entry file. The connect is injected
|
|
230
|
+
* into a virtual module, so there is nowhere for a user to add these calls without `init` editing
|
|
231
|
+
* `src/main.tsx` — an edit to the file people actually own, for something that is opt-in enrichment.
|
|
232
|
+
* Convention costs one `existsSync` and leaves their entry untouched.
|
|
233
|
+
*/
|
|
234
|
+
export const RETICLE_DEV_MODULE_CANDIDATES = [
|
|
235
|
+
'src/reticle-dev.ts',
|
|
236
|
+
'src/reticle-dev.js',
|
|
237
|
+
'src/reticle-dev.tsx',
|
|
238
|
+
'src/reticle-dev.jsx',
|
|
239
|
+
];
|
|
240
|
+
/** The app's dev module, as an importable path — or null when the app has none. */
|
|
241
|
+
export function findDevModule(root, exists) {
|
|
242
|
+
for (const rel of RETICLE_DEV_MODULE_CANDIDATES) {
|
|
243
|
+
if (exists(`${root}/${rel}`))
|
|
244
|
+
return `/${rel}`;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
export function connectModuleSource(options, devModule = null) {
|
|
156
249
|
const args = connectArgs(options);
|
|
157
|
-
|
|
250
|
+
const base = `import { reticle, install } from '${RETICLE_PACKAGE}';\ninstall();\nreticle.connect(${args});\n`;
|
|
251
|
+
// AFTER connect: registerStore subscribes through the live SDK, and registering before there is a
|
|
252
|
+
// session to report into drops the first diffs.
|
|
253
|
+
return null === devModule ? base : `${base}import('${devModule}');\n`;
|
|
158
254
|
}
|
|
159
255
|
/**
|
|
160
256
|
* Reticle Vite plugin. Add to your `plugins` array and the entire integration is done:
|
|
@@ -174,7 +270,7 @@ export function connectModuleSource(options) {
|
|
|
174
270
|
export function reticle(options = {}) {
|
|
175
271
|
const sourceMapping = options.sourceMapping !== false;
|
|
176
272
|
const inject = options.inject !== false;
|
|
177
|
-
const desktop = options.desktop
|
|
273
|
+
const desktop = true === options.desktop;
|
|
178
274
|
// Resolve the stable projectId once (explicit option, else derived from package.json + cwd) so the
|
|
179
275
|
// app is identifiable across port changes with zero config.
|
|
180
276
|
const resolved = {
|
|
@@ -205,7 +301,12 @@ export function reticle(options = {}) {
|
|
|
205
301
|
const port = resolved.port ?? discoverDaemonPort(resolved.projectId);
|
|
206
302
|
const withPort = port !== undefined ? { ...resolved, port } : resolved;
|
|
207
303
|
const token = withPort.token ?? readPairingToken();
|
|
208
|
-
|
|
304
|
+
const withToken = token !== undefined ? { ...withPort, token } : withPort;
|
|
305
|
+
// Resolved here for the same reason as the token: these are Node-side facts about the installed
|
|
306
|
+
// tree, and they travel in the generated connect call rather than through a `define`.
|
|
307
|
+
const appRoot = withToken.root ?? root ?? process.cwd();
|
|
308
|
+
const sdkVersion = withToken.sdkVersion ?? sdkPackageVersion(appRoot);
|
|
309
|
+
return { ...withToken, root: appRoot, sdkVersion };
|
|
209
310
|
};
|
|
210
311
|
/**
|
|
211
312
|
* The BUILD message. A build always runs every transform, so "my transform never ran" and "the
|
|
@@ -243,7 +344,7 @@ export function reticle(options = {}) {
|
|
|
243
344
|
// Web: serve-only, so a production bundle can never carry the SDK — gating is the tool's job.
|
|
244
345
|
// Desktop: a packaged renderer IS a production build with no dev server, so the plugin must also
|
|
245
346
|
// run for `vite build` or the shipped app has no connect() at all.
|
|
246
|
-
...(options.desktop
|
|
347
|
+
...(true === options.desktop ? {} : { apply: 'serve' }),
|
|
247
348
|
enforce: 'pre',
|
|
248
349
|
/**
|
|
249
350
|
* Declare the SDK's CJS runtime deps so Vite pre-bundles them.
|
|
@@ -256,15 +357,59 @@ export function reticle(options = {}) {
|
|
|
256
357
|
* developer has never heard of. Measured on the react-admin demo with the SDK aliased to a local
|
|
257
358
|
* checkout: zero sessions, and it looked like the app was failing to render.
|
|
258
359
|
*
|
|
259
|
-
* Declaring them is free when Vite would have found them anyway
|
|
360
|
+
* Declaring them is free when Vite would have found them anyway — but only when they are
|
|
361
|
+
* actually installed. Naming a package that is not there makes Vite log `Failed to resolve
|
|
362
|
+
* dependency: …, present in optimizeDeps.include` on every boot, which is a scary line pointing
|
|
363
|
+
* at Reticle for a problem that does not exist. SvelteKit apps hit exactly that: nothing in that
|
|
364
|
+
* tree depends on @testing-library/dom.
|
|
260
365
|
*/
|
|
261
366
|
config(config) {
|
|
367
|
+
// Everything below asks what the APP has installed, so every lookup is rooted here and never
|
|
368
|
+
// at the plugin's own location. Vite defaults an omitted root to the cwd; so do we.
|
|
369
|
+
const appRoot = config.root ?? process.cwd();
|
|
262
370
|
return {
|
|
371
|
+
// Expose the daemon's pairing token to hand-written connects in the same Vite app. The
|
|
372
|
+
// plugin's own injected connect gets the token directly, but a connect the USER writes —
|
|
373
|
+
// SvelteKit's client hook, a custom entry — had no way to reach a file only Node can read,
|
|
374
|
+
// so it called connect() with no credential and the bridge answered "authentication
|
|
375
|
+
// failed". Empty until the daemon has provisioned one; the page reloads once it has.
|
|
376
|
+
define: {
|
|
377
|
+
...(config.define ?? {}),
|
|
378
|
+
[RETICLE_TOKEN_GLOBAL]: JSON.stringify(warnIfTokenMissing(readPairingToken()) ?? ''),
|
|
379
|
+
// Lets the SDK report React's absolute `_debugSource.fileName` as a repo-relative path,
|
|
380
|
+
// so source looks the same whichever React version an app is on.
|
|
381
|
+
// Kept for HAND-WRITTEN connects (SvelteKit's hook, a custom entry): those live in app
|
|
382
|
+
// source, where a define does substitute. The plugin's own injected connect passes both as
|
|
383
|
+
// arguments instead — see connectArgs.
|
|
384
|
+
[RETICLE_ROOT_GLOBAL]: JSON.stringify(appRoot),
|
|
385
|
+
[RETICLE_SDK_VERSION_GLOBAL]: JSON.stringify(sdkPackageVersion(appRoot)),
|
|
386
|
+
},
|
|
263
387
|
optimizeDeps: {
|
|
388
|
+
// Part of the cache key, not of the build: changing it is what makes Vite notice that the
|
|
389
|
+
// SDK on disk is not the SDK it pre-bundled. See sdkBuildFingerprint.
|
|
390
|
+
//
|
|
391
|
+
// Under the key THIS Vite wants. Vite 7 moved the optimizer to rolldown and deprecated
|
|
392
|
+
// `esbuildOptions`, warning on every boot — a warning attributed to the plugin that set
|
|
393
|
+
// it, which is us.
|
|
394
|
+
[optimizerOptionsKey(viteMajor(appRoot))]: {
|
|
395
|
+
...(config.optimizeDeps?.esbuildOptions ?? {}),
|
|
396
|
+
define: {
|
|
397
|
+
...(config.optimizeDeps?.esbuildOptions?.define ?? {}),
|
|
398
|
+
__RETICLE_SDK_BUILD__: JSON.stringify(sdkBuildFingerprint(appRoot)),
|
|
399
|
+
},
|
|
400
|
+
},
|
|
264
401
|
include: [
|
|
265
402
|
...(config.optimizeDeps?.include ?? []),
|
|
266
|
-
|
|
267
|
-
|
|
403
|
+
// The SDK ITSELF. Without this, Vite does not learn about @reticlehq/react until the
|
|
404
|
+
// injected connect module is requested — mid-flight, on the very first page load. It
|
|
405
|
+
// then pre-bundles it and forces a full reload, and the connect is lost in that reload:
|
|
406
|
+
// no WebSocket, no session, no console message. The FIRST load after `reticle init` —
|
|
407
|
+
// the one the whole product is judged on — silently did nothing, and it worked on the
|
|
408
|
+
// next refresh, which is the worst possible shape for a bug like this.
|
|
409
|
+
RETICLE_PACKAGE,
|
|
410
|
+
// Only in a form that resolves — see above; a name Vite cannot resolve produces a boot
|
|
411
|
+
// warning that blames Reticle, and a forced re-optimization on every cold start.
|
|
412
|
+
...cjsDepIncludes(appRoot),
|
|
268
413
|
],
|
|
269
414
|
},
|
|
270
415
|
};
|
|
@@ -279,7 +424,16 @@ export function reticle(options = {}) {
|
|
|
279
424
|
const stamped = sourceMapping && shouldStamp(id) ? stamp(withConnect, id) : null;
|
|
280
425
|
return stamped ?? { code: withConnect, map: null };
|
|
281
426
|
}
|
|
282
|
-
if (!sourceMapping
|
|
427
|
+
if (!sourceMapping)
|
|
428
|
+
return null;
|
|
429
|
+
// `.svelte` runs on the RAW component source, which is only still markup because this plugin
|
|
430
|
+
// declares `enforce: 'pre'` and therefore transforms before @sveltejs/vite-plugin-svelte. No
|
|
431
|
+
// map: the insertions are within a line and never move one, and a wrong map is worse than none.
|
|
432
|
+
if (shouldStampSvelte(id)) {
|
|
433
|
+
const stamped = stampSvelte(code, id);
|
|
434
|
+
return null === stamped ? null : { code: stamped, map: null };
|
|
435
|
+
}
|
|
436
|
+
if (!shouldStamp(id))
|
|
283
437
|
return null;
|
|
284
438
|
return stamp(code, id);
|
|
285
439
|
},
|
|
@@ -301,7 +455,9 @@ export function reticle(options = {}) {
|
|
|
301
455
|
load(id) {
|
|
302
456
|
if (!inject || id !== RETICLE_CONNECT_MODULE)
|
|
303
457
|
return null;
|
|
304
|
-
|
|
458
|
+
// Resolved at load, not at config: the file may be created after the dev server starts.
|
|
459
|
+
const devModule = root === undefined ? null : findDevModule(root, existsSync);
|
|
460
|
+
return connectModuleSource(resolveLazy(), devModule);
|
|
305
461
|
},
|
|
306
462
|
configResolved(config) {
|
|
307
463
|
root = config.root;
|
|
@@ -322,7 +478,7 @@ export function reticle(options = {}) {
|
|
|
322
478
|
// In serve, the HTML is sent BEFORE the browser requests the entry module, so the check has to
|
|
323
479
|
// be deferred — asserting here would fire on every healthy start. Unref'd so a dev server is
|
|
324
480
|
// never held open by it.
|
|
325
|
-
if (desktop && inject &&
|
|
481
|
+
if (desktop && inject && 'serve' === command) {
|
|
326
482
|
const timer = setTimeout(checkInjected, DEV_INJECTION_GRACE_MS);
|
|
327
483
|
timer.unref?.();
|
|
328
484
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-side questions about what is actually installed in the app.
|
|
3
|
+
*
|
|
4
|
+
* All three answer "what is on disk right now" rather than "what does the plugin do", and each exists
|
|
5
|
+
* because guessing was wrong in a way that reached a user: a declared-but-absent dependency logs a
|
|
6
|
+
* resolve failure on every boot, an unnoticed SDK change leaves stale code in the browser, and an
|
|
7
|
+
* unreported version makes a skewed pair surface as a bare -32000.
|
|
8
|
+
*
|
|
9
|
+
* All three resolve from the APP ROOT, never from this file. The SDK is the user's dependency and
|
|
10
|
+
* deliberately not one of this plugin's, so a resolve based on `import.meta.url` walks the PLUGIN's
|
|
11
|
+
* node_modules — which under pnpm's strict layout does not contain the SDK at all. Every one of these
|
|
12
|
+
* then fell into its own catch and returned the "not installed" answer: no `sdkVersion` on the HELLO,
|
|
13
|
+
* a CONSTANT build fingerprint (so Vite never re-bundled a changed SDK — the exact stale-bundle
|
|
14
|
+
* false-negative the fingerprint exists to prevent), and no optimizeDeps entry for the SDK itself.
|
|
15
|
+
* Silently, for every pnpm user, with no error anywhere.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Whether a package can be resolved from the app. Used to avoid declaring an optimizeDeps entry for
|
|
19
|
+
* something the app does not have, which Vite reports as a resolve failure on every boot.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isResolvable(specifier: string, from?: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Vite's nested include form — `a > b > c` — for a chain that fully resolves, else null.
|
|
24
|
+
*
|
|
25
|
+
* A single-segment chain is just the bare specifier, so this also answers "can the app see it
|
|
26
|
+
* itself". Each further segment is resolved from the PREVIOUS package's directory, which is what
|
|
27
|
+
* Vite does with the `>` form and the only way to name a dependency the app root cannot see: under
|
|
28
|
+
* pnpm (and npm's nested layout) `@testing-library/dom` belongs to `@reticlehq/browser`, not to the
|
|
29
|
+
* user's app. Naming it bare made Vite fail to resolve it and force a re-optimization on every cold
|
|
30
|
+
* boot. SvelteKit ships `svelte > clsx` for the same reason.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolvableChain(chain: readonly string[], from: string): string | null;
|
|
33
|
+
/** The installed SDK's package version, for the HELLO's `sdkVersion`. Node-side only. */
|
|
34
|
+
export declare function sdkPackageVersion(from?: string): string;
|
|
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
|
+
export declare function sdkBuildFingerprint(from?: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* The installed Vite's major version, or null when it cannot be read.
|
|
51
|
+
*
|
|
52
|
+
* Asked from the APP's root, never the plugin's: the plugin and the app can resolve different Vites
|
|
53
|
+
* in a monorepo, and the one whose deprecation warnings the user sees is the app's.
|
|
54
|
+
*/
|
|
55
|
+
export declare function viteMajor(from?: string): number | null;
|
|
56
|
+
/** Vite renamed the dep-optimizer's passthrough options when it moved to rolldown. */
|
|
57
|
+
export declare const OPTIMIZER_OPTIONS_KEY: {
|
|
58
|
+
readonly ESBUILD: "esbuildOptions";
|
|
59
|
+
readonly ROLLDOWN: "rolldownOptions";
|
|
60
|
+
};
|
|
61
|
+
export type OptimizerOptionsKey = (typeof OPTIMIZER_OPTIONS_KEY)[keyof typeof OPTIMIZER_OPTIONS_KEY];
|
|
62
|
+
/**
|
|
63
|
+
* Which key carries optimizer options on this Vite.
|
|
64
|
+
*
|
|
65
|
+
* Vite 7 moved the dep optimizer to rolldown and deprecated `optimizeDeps.esbuildOptions`, warning
|
|
66
|
+
* on every boot to use `rolldownOptions` instead — a warning attributed to whichever plugin set it,
|
|
67
|
+
* which is us. Unknown versions get the older key: a deprecation notice is a much smaller failure
|
|
68
|
+
* than an option the installed Vite has never heard of.
|
|
69
|
+
*/
|
|
70
|
+
export declare function optimizerOptionsKey(major: number | null): OptimizerOptionsKey;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-side questions about what is actually installed in the app.
|
|
3
|
+
*
|
|
4
|
+
* All three answer "what is on disk right now" rather than "what does the plugin do", and each exists
|
|
5
|
+
* because guessing was wrong in a way that reached a user: a declared-but-absent dependency logs a
|
|
6
|
+
* resolve failure on every boot, an unnoticed SDK change leaves stale code in the browser, and an
|
|
7
|
+
* unreported version makes a skewed pair surface as a bare -32000.
|
|
8
|
+
*
|
|
9
|
+
* All three resolve from the APP ROOT, never from this file. The SDK is the user's dependency and
|
|
10
|
+
* deliberately not one of this plugin's, so a resolve based on `import.meta.url` walks the PLUGIN's
|
|
11
|
+
* node_modules — which under pnpm's strict layout does not contain the SDK at all. Every one of these
|
|
12
|
+
* then fell into its own catch and returned the "not installed" answer: no `sdkVersion` on the HELLO,
|
|
13
|
+
* a CONSTANT build fingerprint (so Vite never re-bundled a changed SDK — the exact stale-bundle
|
|
14
|
+
* false-negative the fingerprint exists to prevent), and no optimizeDeps entry for the SDK itself.
|
|
15
|
+
* Silently, for every pnpm user, with no error anywhere.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
/** The React kit the host app imports the SDK from. Mirrors the constant in index.ts. */
|
|
21
|
+
const RETICLE_PACKAGE = '@reticlehq/react';
|
|
22
|
+
/** How far up from the resolved entry to look for the manifest beside it. */
|
|
23
|
+
const MANIFEST_SEARCH_DEPTH = 5;
|
|
24
|
+
/**
|
|
25
|
+
* A `require` rooted at the app rather than at this plugin. `from` is Vite's `config.root`; the
|
|
26
|
+
* `process.cwd()` default matches how Vite itself defaults the root when the config omits it.
|
|
27
|
+
*/
|
|
28
|
+
function requireFromApp(from) {
|
|
29
|
+
return createRequire(join(from, 'package.json'));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether a package can be resolved from the app. Used to avoid declaring an optimizeDeps entry for
|
|
33
|
+
* something the app does not have, which Vite reports as a resolve failure on every boot.
|
|
34
|
+
*/
|
|
35
|
+
export function isResolvable(specifier, from = process.cwd()) {
|
|
36
|
+
try {
|
|
37
|
+
requireFromApp(from).resolve(specifier);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The directory holding `specifier`'s own manifest, resolved from `from`. Null when it is not there.
|
|
46
|
+
*
|
|
47
|
+
* Resolves the entry and walks up to the manifest beside it rather than requiring
|
|
48
|
+
* `<pkg>/package.json` directly: a package with an `exports` map may refuse that subpath, and our
|
|
49
|
+
* own packages do.
|
|
50
|
+
*/
|
|
51
|
+
function packageDirOf(specifier, from) {
|
|
52
|
+
try {
|
|
53
|
+
let dir = dirname(requireFromApp(from).resolve(specifier));
|
|
54
|
+
for (let up = 0; up < MANIFEST_SEARCH_DEPTH; up++) {
|
|
55
|
+
const candidate = join(dir, 'package.json');
|
|
56
|
+
if (existsSync(candidate)) {
|
|
57
|
+
const parsed = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
58
|
+
if (parsed.name === specifier)
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
const parent = dirname(dir);
|
|
62
|
+
if (parent === dir)
|
|
63
|
+
break;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Unresolvable from here — the caller's next chain, or nothing.
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Vite's nested include form — `a > b > c` — for a chain that fully resolves, else null.
|
|
74
|
+
*
|
|
75
|
+
* A single-segment chain is just the bare specifier, so this also answers "can the app see it
|
|
76
|
+
* itself". Each further segment is resolved from the PREVIOUS package's directory, which is what
|
|
77
|
+
* Vite does with the `>` form and the only way to name a dependency the app root cannot see: under
|
|
78
|
+
* pnpm (and npm's nested layout) `@testing-library/dom` belongs to `@reticlehq/browser`, not to the
|
|
79
|
+
* user's app. Naming it bare made Vite fail to resolve it and force a re-optimization on every cold
|
|
80
|
+
* boot. SvelteKit ships `svelte > clsx` for the same reason.
|
|
81
|
+
*/
|
|
82
|
+
export function resolvableChain(chain, from) {
|
|
83
|
+
let base = from;
|
|
84
|
+
for (const segment of chain) {
|
|
85
|
+
const dir = packageDirOf(segment, base);
|
|
86
|
+
if (null === dir)
|
|
87
|
+
return null;
|
|
88
|
+
base = dir;
|
|
89
|
+
}
|
|
90
|
+
return 0 === chain.length ? null : chain.join(' > ');
|
|
91
|
+
}
|
|
92
|
+
/** The installed SDK's package version, for the HELLO's `sdkVersion`. Node-side only. */
|
|
93
|
+
export function sdkPackageVersion(from = process.cwd()) {
|
|
94
|
+
const require_ = requireFromApp(from);
|
|
95
|
+
// Preferred: the package exports its own manifest. Newer SDKs do.
|
|
96
|
+
try {
|
|
97
|
+
const pkg = require_(`${RETICLE_PACKAGE}/package.json`);
|
|
98
|
+
if ('string' === typeof pkg.version)
|
|
99
|
+
return pkg.version;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Falls through — see below.
|
|
103
|
+
}
|
|
104
|
+
// Fallback, and it is load-bearing rather than defensive: an OLDER SDK has no `./package.json`
|
|
105
|
+
// in its exports map, and an older SDK is precisely the skew we are trying to name. Resolve the
|
|
106
|
+
// main entry instead and walk up to the manifest beside it.
|
|
107
|
+
try {
|
|
108
|
+
let dir = dirname(require_.resolve(RETICLE_PACKAGE));
|
|
109
|
+
for (let up = 0; up < MANIFEST_SEARCH_DEPTH; up++) {
|
|
110
|
+
const candidate = join(dir, 'package.json');
|
|
111
|
+
if (existsSync(candidate)) {
|
|
112
|
+
const parsed = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
113
|
+
if ('string' === typeof parsed.version)
|
|
114
|
+
return parsed.version;
|
|
115
|
+
}
|
|
116
|
+
const parent = dirname(dir);
|
|
117
|
+
if (parent === dir)
|
|
118
|
+
break;
|
|
119
|
+
dir = parent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Unresolvable (not installed yet, exotic layout) — report nothing rather than guessing.
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A fingerprint of the installed SDK build, mixed into `optimizeDeps` so Vite re-bundles when the
|
|
129
|
+
* SDK changes.
|
|
130
|
+
*
|
|
131
|
+
* Vite's dep-optimizer cache is keyed on the `optimizeDeps` config and the lockfile — NOT on the
|
|
132
|
+
* contents of the packages it bundled. Upgrade the SDK in place (a patched dist, a linked checkout,
|
|
133
|
+
* an overlay) and the version in `package.json` can stay the same, so Vite keeps serving the OLD
|
|
134
|
+
* pre-bundled copy out of `node_modules/.vite` across dev-server restarts. The fix you just shipped
|
|
135
|
+
* is simply not in the browser, and it looks like the fix does not work. That cost a real
|
|
136
|
+
* false-negative during this bug hunt, and every user upgrading in place hits the same thing.
|
|
137
|
+
*
|
|
138
|
+
* Size+mtime is enough: it changes whenever the bundle does and costs one `stat`.
|
|
139
|
+
*/
|
|
140
|
+
export function sdkBuildFingerprint(from = process.cwd()) {
|
|
141
|
+
try {
|
|
142
|
+
const entry = requireFromApp(from).resolve(RETICLE_PACKAGE);
|
|
143
|
+
const { size, mtimeMs } = statSync(entry);
|
|
144
|
+
return `${String(size)}-${String(Math.trunc(mtimeMs))}`;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Unresolvable (not installed yet, exotic layout) — a constant is still correct, just inert.
|
|
148
|
+
return 'unknown';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The installed Vite's major version, or null when it cannot be read.
|
|
153
|
+
*
|
|
154
|
+
* Asked from the APP's root, never the plugin's: the plugin and the app can resolve different Vites
|
|
155
|
+
* in a monorepo, and the one whose deprecation warnings the user sees is the app's.
|
|
156
|
+
*/
|
|
157
|
+
export function viteMajor(from = process.cwd()) {
|
|
158
|
+
try {
|
|
159
|
+
const pkg = requireFromApp(from)('vite/package.json');
|
|
160
|
+
const major = parseInt((pkg.version ?? '').split('.')[0] ?? '', 10);
|
|
161
|
+
return isNaN(major) ? null : major;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Vite renamed the dep-optimizer's passthrough options when it moved to rolldown. */
|
|
168
|
+
export const OPTIMIZER_OPTIONS_KEY = {
|
|
169
|
+
ESBUILD: 'esbuildOptions',
|
|
170
|
+
ROLLDOWN: 'rolldownOptions',
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Which key carries optimizer options on this Vite.
|
|
174
|
+
*
|
|
175
|
+
* Vite 7 moved the dep optimizer to rolldown and deprecated `optimizeDeps.esbuildOptions`, warning
|
|
176
|
+
* on every boot to use `rolldownOptions` instead — a warning attributed to whichever plugin set it,
|
|
177
|
+
* which is us. Unknown versions get the older key: a deprecation notice is a much smaller failure
|
|
178
|
+
* than an option the installed Vite has never heard of.
|
|
179
|
+
*/
|
|
180
|
+
export function optimizerOptionsKey(major) {
|
|
181
|
+
return null !== major && 7 <= major
|
|
182
|
+
? OPTIMIZER_OPTIONS_KEY.ROLLDOWN
|
|
183
|
+
: OPTIMIZER_OPTIONS_KEY.ESBUILD;
|
|
184
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one build-time condition that guarantees a runtime failure.
|
|
3
|
+
*
|
|
4
|
+
* The pairing token is read from disk ONCE, when Vite resolves its config, and inlined as
|
|
5
|
+
* `__RETICLE_TOKEN__`. The daemon is what writes that file — so a dev server started BEFORE the
|
|
6
|
+
* daemon bakes in an empty token, and every app it serves opens a WebSocket the bridge then refuses.
|
|
7
|
+
*
|
|
8
|
+
* Nothing about that looks broken from outside: the SDK module loads, the socket opens, and then a
|
|
9
|
+
* session simply never appears. Restarting the dev server is the whole fix, and no other layer is in
|
|
10
|
+
* a position to say so — by the time the failure is observable, the value was baked in minutes ago.
|
|
11
|
+
*/
|
|
12
|
+
export declare function missingTokenWarning(token: string | undefined): string | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one build-time condition that guarantees a runtime failure.
|
|
3
|
+
*
|
|
4
|
+
* The pairing token is read from disk ONCE, when Vite resolves its config, and inlined as
|
|
5
|
+
* `__RETICLE_TOKEN__`. The daemon is what writes that file — so a dev server started BEFORE the
|
|
6
|
+
* daemon bakes in an empty token, and every app it serves opens a WebSocket the bridge then refuses.
|
|
7
|
+
*
|
|
8
|
+
* Nothing about that looks broken from outside: the SDK module loads, the socket opens, and then a
|
|
9
|
+
* session simply never appears. Restarting the dev server is the whole fix, and no other layer is in
|
|
10
|
+
* a position to say so — by the time the failure is observable, the value was baked in minutes ago.
|
|
11
|
+
*/
|
|
12
|
+
export function missingTokenWarning(token) {
|
|
13
|
+
if (token !== undefined && token.length > 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
return ('[reticle] no pairing token was available when this dev server started, so the app will connect ' +
|
|
16
|
+
'and be refused — you will see NO SESSION even though the SDK loads and the socket opens. ' +
|
|
17
|
+
'The token is written by the Reticle daemon: start it (`reticle serve`, or let your agent start ' +
|
|
18
|
+
'it) and then RESTART this dev server, because the value is inlined at config time.');
|
|
19
|
+
}
|
package/dist/project-id.d.ts
CHANGED
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
* package.json name plus a short hash of its absolute root — human-readable AND unique per checkout,
|
|
7
7
|
* and unchanged when the port shifts. An explicit `projectId` option always overrides.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
* Turn a package name into an id-safe slug: scoped names lose the `@scope/` punctuation
|
|
11
|
-
* (`@acme/web` → `acme-web`), everything non-alphanumeric collapses to single dashes, edges trimmed.
|
|
12
|
-
*/
|
|
13
|
-
export declare function slugifyPackageName(name: string): string;
|
|
9
|
+
export { slugifyPackageName } from '@reticlehq/core';
|
|
14
10
|
/** A short, stable hex fingerprint of the absolute project root (disambiguates same-named checkouts). */
|
|
15
11
|
export declare function shortHash(input: string): string;
|
|
16
12
|
/**
|
|
17
13
|
* Derive the stable projectId from the package name (may be undefined) and the absolute root path.
|
|
18
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* The rule itself is core's `projectIdFrom`, shared with `reticle init`: this id is what scopes a
|
|
16
|
+
* session to an app, so what the plugin stamps and what init records must be the same string. They
|
|
17
|
+
* used to be two identical copies kept in step by a comment.
|
|
19
18
|
*/
|
|
20
19
|
export declare function deriveProjectId(pkgName: string | undefined, rootPath: string): string;
|
|
21
20
|
/**
|
package/dist/project-id.js
CHANGED
|
@@ -7,31 +7,23 @@
|
|
|
7
7
|
* and unchanged when the port shifts. An explicit `projectId` option always overrides.
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
|
-
import {
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* (`@acme/web` → `acme-web`), everything non-alphanumeric collapses to single dashes, edges trimmed.
|
|
15
|
-
*/
|
|
16
|
-
export function slugifyPackageName(name) {
|
|
17
|
-
return name
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replace(/^@/, '')
|
|
20
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
21
|
-
.replace(/^-+|-+$/g, '');
|
|
22
|
-
}
|
|
12
|
+
import { PROJECT_ID_HASH_LENGTH, projectIdFrom } from '@reticlehq/core';
|
|
13
|
+
export { slugifyPackageName } from '@reticlehq/core';
|
|
23
14
|
/** A short, stable hex fingerprint of the absolute project root (disambiguates same-named checkouts). */
|
|
24
15
|
export function shortHash(input) {
|
|
25
|
-
return createHash('sha1').update(input).digest('hex').slice(0,
|
|
16
|
+
return createHash('sha1').update(input).digest('hex').slice(0, PROJECT_ID_HASH_LENGTH);
|
|
26
17
|
}
|
|
27
18
|
/**
|
|
28
19
|
* Derive the stable projectId from the package name (may be undefined) and the absolute root path.
|
|
29
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* The rule itself is core's `projectIdFrom`, shared with `reticle init`: this id is what scopes a
|
|
22
|
+
* session to an app, so what the plugin stamps and what init records must be the same string. They
|
|
23
|
+
* used to be two identical copies kept in step by a comment.
|
|
30
24
|
*/
|
|
31
25
|
export function deriveProjectId(pkgName, rootPath) {
|
|
32
|
-
|
|
33
|
-
const base = fromName.length > 0 ? fromName : slugifyPackageName(basename(rootPath)) || 'app';
|
|
34
|
-
return `${base}-${shortHash(rootPath)}`;
|
|
26
|
+
return projectIdFrom(pkgName, rootPath, shortHash);
|
|
35
27
|
}
|
|
36
28
|
/** Read the `name` from the nearest package.json at or above `startDir`, or undefined if none. */
|
|
37
29
|
function readNearestPackageName(startDir) {
|
|
@@ -41,9 +33,9 @@ function readNearestPackageName(startDir) {
|
|
|
41
33
|
if (existsSync(pkgPath)) {
|
|
42
34
|
try {
|
|
43
35
|
const parsed = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
44
|
-
if (typeof parsed
|
|
36
|
+
if ('object' === typeof parsed && parsed !== null) {
|
|
45
37
|
const name = parsed['name'];
|
|
46
|
-
if (typeof name
|
|
38
|
+
if ('string' === typeof name && name.length > 0)
|
|
47
39
|
return name;
|
|
48
40
|
}
|
|
49
41
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
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
|
+
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
|
+
/**
|
|
57
|
+
* Stamp `data-reticle-source="file:line:column"` on every host element of a `.svelte` component.
|
|
58
|
+
*
|
|
59
|
+
* Returns null — never throws — when the compiler is absent or the component does not parse. In dev
|
|
60
|
+
* the transform runs on every keystroke, so a half-typed component is the NORMAL case; failing the
|
|
61
|
+
* build over one would make Reticle the reason the dev server is red, for a feature that is pure
|
|
62
|
+
* enrichment on top of an app that otherwise works.
|
|
63
|
+
*/
|
|
64
|
+
export declare function stampSvelte(code: string, id: string, load?: LoadSvelteCompiler): string | null;
|
|
65
|
+
export {};
|
|
@@ -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 ('\n' === source[i]) {
|
|
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
|
+
function sourcePathFor(id, cwd = process.cwd()) {
|
|
83
|
+
return relative(cwd, id).replace(/\\/g, '/');
|
|
84
|
+
}
|
|
85
|
+
function isElementNode(value) {
|
|
86
|
+
if (null === value || typeof value !== 'object')
|
|
87
|
+
return false;
|
|
88
|
+
const node = value;
|
|
89
|
+
return ('string' === typeof node.type &&
|
|
90
|
+
HOST_ELEMENT_TYPES.has(node.type) &&
|
|
91
|
+
'string' === typeof node.name &&
|
|
92
|
+
'number' === typeof node.start);
|
|
93
|
+
}
|
|
94
|
+
function isAlreadyStamped(node) {
|
|
95
|
+
return (node.attributes ?? []).some((attr) => attr !== null &&
|
|
96
|
+
'object' === typeof attr &&
|
|
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 (null === value || 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 (null === compiler)
|
|
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 (0 === elements.length)
|
|
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.
|
|
3
|
+
"version": "2.5.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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
".": {
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
27
|
"default": "./dist/index.js"
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"dist",
|
|
@@ -34,11 +35,12 @@
|
|
|
34
35
|
],
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"@babel/core": "^7.26.0",
|
|
37
|
-
"@reticlehq/babel-plugin": "2.
|
|
38
|
-
"@reticlehq/core": "2.
|
|
38
|
+
"@reticlehq/babel-plugin": "2.5.0",
|
|
39
|
+
"@reticlehq/core": "2.5.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/babel__core": "^7.20.5",
|
|
43
|
+
"svelte": "^5.56.8",
|
|
42
44
|
"vite": "^8"
|
|
43
45
|
},
|
|
44
46
|
"peerDependencies": {
|
|
@@ -59,6 +61,7 @@
|
|
|
59
61
|
"build": "tsc -b",
|
|
60
62
|
"typecheck": "tsc -b",
|
|
61
63
|
"lint": "eslint src",
|
|
62
|
-
"test:unit": "vitest run src --passWithNoTests"
|
|
64
|
+
"test:unit": "vitest run src --passWithNoTests",
|
|
65
|
+
"test:coverage": "vitest run src --passWithNoTests --coverage.enabled --coverage.provider=v8 --coverage.reporter=text-summary --coverage.include='src/**/*.ts' --coverage.exclude='src/**/*.test.ts'"
|
|
63
66
|
}
|
|
64
67
|
}
|