@reticlehq/vite-plugin 2.13.1 → 3.1.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.cjs +581 -463
- package/dist/index.d.cts +85 -44
- package/dist/index.d.ts +85 -44
- package/dist/index.js +129 -106
- package/dist/injection-postcondition.d.ts +81 -0
- package/dist/injection-postcondition.js +123 -0
- package/dist/installed.d.ts +2 -1
- package/dist/plugin-name.d.ts +7 -0
- package/dist/plugin-name.js +7 -0
- package/dist/project-id.d.ts +24 -6
- package/dist/project-id.js +67 -22
- package/dist/vitest-browser.d.ts +31 -0
- package/dist/vitest-browser.js +37 -0
- package/dist/watch-ignore.d.ts +18 -0
- package/dist/watch-ignore.js +26 -0
- package/package.json +7 -6
- /package/dist/{ensure-token.d.ts → token/ensure-token.d.ts} +0 -0
- /package/dist/{ensure-token.js → token/ensure-token.js} +0 -0
- /package/dist/{missing-token.d.ts → token/missing-token.d.ts} +0 -0
- /package/dist/{missing-token.js → token/missing-token.js} +0 -0
package/dist/project-id.d.ts
CHANGED
|
@@ -13,13 +13,31 @@ export declare function shortHash(input: string): string;
|
|
|
13
13
|
* Derive the stable projectId from the package name (may be undefined) and the absolute root path.
|
|
14
14
|
*
|
|
15
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
|
|
17
|
-
*
|
|
16
|
+
* session to an app, so what the plugin stamps and what init records must be the same string — ONE
|
|
17
|
+
* implementation, not two copies kept in step by hand.
|
|
18
18
|
*/
|
|
19
19
|
export declare function deriveProjectId(pkgName: string | undefined, rootPath: string): string;
|
|
20
|
+
/** Reads a file, or throws — the one filesystem touch these walkers make, injectable for tests. */
|
|
21
|
+
type ReadFile = (path: string) => string;
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* Read the `projectId` `reticle init` recorded in the nearest `.reticle.json`, or undefined.
|
|
24
|
+
*
|
|
25
|
+
* This is the id of RECORD. The derivation below is a fallback for a project that has never been
|
|
26
|
+
* through `init` — it is not a second opinion, and where the two disagree the config wins.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readConfiguredProjectId(startDir: string, readFile?: ReadFile): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
|
|
31
|
+
* option, then the id `reticle init` recorded in `.reticle.json`, then derivation.
|
|
32
|
+
*
|
|
33
|
+
* The config step is what makes the id survive a dev server that does not share a filesystem with
|
|
34
|
+
* the CLI. Derivation hashes the ABSOLUTE ROOT, so a containerised Vite (`/app`) and the host `init`
|
|
35
|
+
* that wired it produce different ids for one project — the page announces one, the daemon expects
|
|
36
|
+
* the other, and the bridge refuses the connection with `authentication failed`, which names
|
|
37
|
+
* neither. Reading the file `init` already wrote costs one `readFileSync` at config time and makes
|
|
38
|
+
* the two halves agree by construction rather than by coincidence of working directory.
|
|
39
|
+
*
|
|
40
|
+
* `cwd`, `readPkgName` and `readConfiguredId` are injectable so resolution is unit-tested without
|
|
41
|
+
* touching the real filesystem.
|
|
24
42
|
*/
|
|
25
|
-
export declare function resolveProjectId(explicit: string | undefined, cwd: string, readPkgName?: (dir: string) => string | undefined): string;
|
|
43
|
+
export declare function resolveProjectId(explicit: string | undefined, cwd: string, readPkgName?: (dir: string) => string | undefined, readConfiguredId?: (dir: string) => string | undefined): string;
|
package/dist/project-id.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
12
|
import { PROJECT_ID_HASH_LENGTH, projectIdFrom } from '@reticlehq/core';
|
|
13
|
+
/** The project config `reticle init` writes, and the id of record it carries. */
|
|
14
|
+
const RETICLE_CONFIG_BASENAME = '.reticle.json';
|
|
13
15
|
export { slugifyPackageName } from '@reticlehq/core';
|
|
14
16
|
/** A short, stable hex fingerprint of the absolute project root (disambiguates same-named checkouts). */
|
|
15
17
|
export function shortHash(input) {
|
|
@@ -19,30 +21,48 @@ export function shortHash(input) {
|
|
|
19
21
|
* Derive the stable projectId from the package name (may be undefined) and the absolute root path.
|
|
20
22
|
*
|
|
21
23
|
* 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
|
|
23
|
-
*
|
|
24
|
+
* session to an app, so what the plugin stamps and what init records must be the same string — ONE
|
|
25
|
+
* implementation, not two copies kept in step by hand.
|
|
24
26
|
*/
|
|
25
27
|
export function deriveProjectId(pkgName, rootPath) {
|
|
26
28
|
return projectIdFrom(pkgName, rootPath, shortHash);
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const readFileOrThrow = (path) => readFileSync(path, 'utf8');
|
|
31
|
+
/**
|
|
32
|
+
* How far up to look for `.reticle.json`, and WHY it is not the package.json depth below.
|
|
33
|
+
*
|
|
34
|
+
* The same number, for the same reason, as the server's `MAX_CONFIG_SEARCH_DEPTH`: deep enough for
|
|
35
|
+
* an app in `frontend/` or `apps/web/`, a worktree beside its main checkout, and a package inside a
|
|
36
|
+
* monorepo — and shallow enough that a dev server started somewhere unrelated cannot silently adopt
|
|
37
|
+
* a distant ancestor's config and announce another app's identity. The two walkers have to agree,
|
|
38
|
+
* because the whole point of reading this file is that the plugin and the CLI stop disagreeing.
|
|
39
|
+
*/
|
|
40
|
+
const MAX_CONFIG_SEARCH_DEPTH = 6;
|
|
41
|
+
/**
|
|
42
|
+
* How far up to look for `package.json`, which is a different question with a different answer.
|
|
43
|
+
*
|
|
44
|
+
* A package name is only ever used to BUILD an id, never to adopt one, so an over-eager walk here
|
|
45
|
+
* costs a less specific name rather than the wrong app's identity. Left as it was.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_PACKAGE_SEARCH_DEPTH = 50;
|
|
48
|
+
/**
|
|
49
|
+
* Walk up from `startDir` looking for `basename`, and return the first non-empty string `field`
|
|
50
|
+
* yields. Unreadable and unparseable files are skipped rather than fatal: a dev server must start.
|
|
51
|
+
*/
|
|
52
|
+
function readNearestField(startDir, basename, field, readFile, maxDepth) {
|
|
30
53
|
let dir = startDir;
|
|
31
|
-
for (let depth = 0; depth
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
if ('
|
|
37
|
-
|
|
38
|
-
if ('string' === typeof name && name.length > 0)
|
|
39
|
-
return name;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// unreadable package.json → keep walking up
|
|
54
|
+
for (let depth = 0; depth <= maxDepth; depth++) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFile(join(dir, basename)));
|
|
57
|
+
if ('object' === typeof parsed && parsed !== null) {
|
|
58
|
+
const value = parsed[field];
|
|
59
|
+
if ('string' === typeof value && value.length > 0)
|
|
60
|
+
return value;
|
|
44
61
|
}
|
|
45
62
|
}
|
|
63
|
+
catch {
|
|
64
|
+
// missing, unreadable or unparseable → keep walking up
|
|
65
|
+
}
|
|
46
66
|
const parent = dirname(dir);
|
|
47
67
|
if (parent === dir)
|
|
48
68
|
break; // reached filesystem root
|
|
@@ -50,13 +70,38 @@ function readNearestPackageName(startDir) {
|
|
|
50
70
|
}
|
|
51
71
|
return undefined;
|
|
52
72
|
}
|
|
73
|
+
/** Read the `name` from the nearest package.json at or above `startDir`, or undefined if none. */
|
|
74
|
+
function readNearestPackageName(startDir, readFile = readFileOrThrow) {
|
|
75
|
+
return readNearestField(startDir, 'package.json', 'name', readFile, MAX_PACKAGE_SEARCH_DEPTH);
|
|
76
|
+
}
|
|
53
77
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
78
|
+
* Read the `projectId` `reticle init` recorded in the nearest `.reticle.json`, or undefined.
|
|
79
|
+
*
|
|
80
|
+
* This is the id of RECORD. The derivation below is a fallback for a project that has never been
|
|
81
|
+
* through `init` — it is not a second opinion, and where the two disagree the config wins.
|
|
82
|
+
*/
|
|
83
|
+
export function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
|
|
84
|
+
return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'projectId', readFile, MAX_CONFIG_SEARCH_DEPTH);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
|
|
88
|
+
* option, then the id `reticle init` recorded in `.reticle.json`, then derivation.
|
|
89
|
+
*
|
|
90
|
+
* The config step is what makes the id survive a dev server that does not share a filesystem with
|
|
91
|
+
* the CLI. Derivation hashes the ABSOLUTE ROOT, so a containerised Vite (`/app`) and the host `init`
|
|
92
|
+
* that wired it produce different ids for one project — the page announces one, the daemon expects
|
|
93
|
+
* the other, and the bridge refuses the connection with `authentication failed`, which names
|
|
94
|
+
* neither. Reading the file `init` already wrote costs one `readFileSync` at config time and makes
|
|
95
|
+
* the two halves agree by construction rather than by coincidence of working directory.
|
|
96
|
+
*
|
|
97
|
+
* `cwd`, `readPkgName` and `readConfiguredId` are injectable so resolution is unit-tested without
|
|
98
|
+
* touching the real filesystem.
|
|
57
99
|
*/
|
|
58
|
-
export function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName) {
|
|
100
|
+
export function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName, readConfiguredId = readConfiguredProjectId) {
|
|
59
101
|
if (explicit !== undefined && explicit.length > 0)
|
|
60
102
|
return explicit;
|
|
103
|
+
const configured = readConfiguredId(cwd);
|
|
104
|
+
if (configured !== undefined && configured.length > 0)
|
|
105
|
+
return configured;
|
|
61
106
|
return deriveProjectId(readPkgName(cwd), cwd);
|
|
62
107
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling Vitest's browser-mode runner apart from every other Vite server.
|
|
3
|
+
*
|
|
4
|
+
* Small, but it is the discriminator a wrong cut gets wrong in production, so it earns its own file
|
|
5
|
+
* and its own name.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Is THIS server Vitest's browser-mode runner?
|
|
9
|
+
*
|
|
10
|
+
* Vitest browser mode renders each component test into its own iframe served by the same Vite dev
|
|
11
|
+
* server, so `transformIndexHtml` injects into every one of them. The HUD then sits in the test
|
|
12
|
+
* document's hit-test path and intercepts pointer events, and a user who adds Reticle watches their
|
|
13
|
+
* unrelated component tests start timing out on clicks.
|
|
14
|
+
*
|
|
15
|
+
* The signal is `test.browser.enabled` on the RESOLVED config. The first cut used the `VITEST`
|
|
16
|
+
* environment variable and was wrong in a way its own unit tests could not show: `VITEST` means
|
|
17
|
+
* "Vitest is running somewhere in this process", which is ALSO true when a Vitest suite boots an app
|
|
18
|
+
* in order to test it. `frameworks.integration.test.ts` starts a real Vite dev server per example
|
|
19
|
+
* app and asserts Reticle connects; under the env check it injected nothing and the suite failed
|
|
20
|
+
* with "@reticlehq/example-react never connected an Reticle session". CI caught that; three local
|
|
21
|
+
* battery runs did not, because that suite is not the battery.
|
|
22
|
+
*
|
|
23
|
+
* A bare `test` key is wrong the other way — most projects configure Vitest and are not under it.
|
|
24
|
+
* `test.browser.enabled` is true only for the server actually serving browser-mode test iframes,
|
|
25
|
+
* which is the one case with a HUD to suppress.
|
|
26
|
+
*
|
|
27
|
+
* An explicit `inject: true` still beats it: the check is a default chosen on the user's behalf.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isVitestBrowserServer(config: {
|
|
30
|
+
test?: unknown;
|
|
31
|
+
}): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling Vitest's browser-mode runner apart from every other Vite server.
|
|
3
|
+
*
|
|
4
|
+
* Small, but it is the discriminator a wrong cut gets wrong in production, so it earns its own file
|
|
5
|
+
* and its own name.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Is THIS server Vitest's browser-mode runner?
|
|
9
|
+
*
|
|
10
|
+
* Vitest browser mode renders each component test into its own iframe served by the same Vite dev
|
|
11
|
+
* server, so `transformIndexHtml` injects into every one of them. The HUD then sits in the test
|
|
12
|
+
* document's hit-test path and intercepts pointer events, and a user who adds Reticle watches their
|
|
13
|
+
* unrelated component tests start timing out on clicks.
|
|
14
|
+
*
|
|
15
|
+
* The signal is `test.browser.enabled` on the RESOLVED config. The first cut used the `VITEST`
|
|
16
|
+
* environment variable and was wrong in a way its own unit tests could not show: `VITEST` means
|
|
17
|
+
* "Vitest is running somewhere in this process", which is ALSO true when a Vitest suite boots an app
|
|
18
|
+
* in order to test it. `frameworks.integration.test.ts` starts a real Vite dev server per example
|
|
19
|
+
* app and asserts Reticle connects; under the env check it injected nothing and the suite failed
|
|
20
|
+
* with "@reticlehq/example-react never connected an Reticle session". CI caught that; three local
|
|
21
|
+
* battery runs did not, because that suite is not the battery.
|
|
22
|
+
*
|
|
23
|
+
* A bare `test` key is wrong the other way — most projects configure Vitest and are not under it.
|
|
24
|
+
* `test.browser.enabled` is true only for the server actually serving browser-mode test iframes,
|
|
25
|
+
* which is the one case with a HUD to suppress.
|
|
26
|
+
*
|
|
27
|
+
* An explicit `inject: true` still beats it: the check is a default chosen on the user's behalf.
|
|
28
|
+
*/
|
|
29
|
+
export function isVitestBrowserServer(config) {
|
|
30
|
+
const test = config.test;
|
|
31
|
+
if (null === test || 'object' !== typeof test)
|
|
32
|
+
return false;
|
|
33
|
+
const browser = test.browser;
|
|
34
|
+
if (null === browser || 'object' !== typeof browser)
|
|
35
|
+
return false;
|
|
36
|
+
return true === browser.enabled;
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalising Vite's watcher `ignored` list, which is not a list.
|
|
3
|
+
*
|
|
4
|
+
* Small but cohesive: one third-party type shape, one runtime hazard it hides, and the guard that
|
|
5
|
+
* makes the plugin's return assignable without a cast.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Append Reticle's journal pattern to whatever the app already ignored, without assuming it is an array.
|
|
9
|
+
*
|
|
10
|
+
* Vite's `ignored` is `AnymatchMatcher`, so `watch: { ignored: '**\/fixtures/**' }` is a legal
|
|
11
|
+
* config. Spreading that string would explode it into one pattern PER CHARACTER — every one of
|
|
12
|
+
* which matches nothing, so the app's own exclusion is silently dropped and no error is raised. A
|
|
13
|
+
* function matcher is worse: it is not iterable at all, so the spread throws at config time and
|
|
14
|
+
* takes the dev server down, blaming the last plugin to touch the config.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergeIgnored(existing: unknown, ours: RegExp): WatchPattern[];
|
|
17
|
+
/** Vite's `AnymatchPattern`, restated so the return type needs no cast. */
|
|
18
|
+
export type WatchPattern = string | RegExp | ((path: string) => boolean);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalising Vite's watcher `ignored` list, which is not a list.
|
|
3
|
+
*
|
|
4
|
+
* Small but cohesive: one third-party type shape, one runtime hazard it hides, and the guard that
|
|
5
|
+
* makes the plugin's return assignable without a cast.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Append Reticle's journal pattern to whatever the app already ignored, without assuming it is an array.
|
|
9
|
+
*
|
|
10
|
+
* Vite's `ignored` is `AnymatchMatcher`, so `watch: { ignored: '**\/fixtures/**' }` is a legal
|
|
11
|
+
* config. Spreading that string would explode it into one pattern PER CHARACTER — every one of
|
|
12
|
+
* which matches nothing, so the app's own exclusion is silently dropped and no error is raised. A
|
|
13
|
+
* function matcher is worse: it is not iterable at all, so the spread throws at config time and
|
|
14
|
+
* takes the dev server down, blaming the last plugin to touch the config.
|
|
15
|
+
*/
|
|
16
|
+
export function mergeIgnored(existing, ours) {
|
|
17
|
+
if (undefined === existing || null === existing)
|
|
18
|
+
return [ours];
|
|
19
|
+
const listed = Array.isArray(existing) ? existing : [existing];
|
|
20
|
+
// Filtered rather than cast: an entry that is none of the three legal matcher shapes could never
|
|
21
|
+
// have excluded anything, so dropping it loses nothing and keeps the return honest without `any`.
|
|
22
|
+
return [...listed.filter(isWatchPattern), ours];
|
|
23
|
+
}
|
|
24
|
+
function isWatchPattern(value) {
|
|
25
|
+
return 'string' === typeof value || value instanceof RegExp || 'function' === typeof value;
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/vite-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/reticlehq/reticle.git",
|
|
11
|
-
"directory": "
|
|
11
|
+
"directory": "adapters/build/vite"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/reticlehq/reticle#readme",
|
|
14
14
|
"bugs": "https://github.com/reticlehq/reticle/issues",
|
|
@@ -41,14 +41,15 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@babel/core": "^7.26.0",
|
|
44
|
-
"@reticlehq/babel-plugin": "
|
|
45
|
-
"@reticlehq/core": "
|
|
44
|
+
"@reticlehq/babel-plugin": "3.1.0",
|
|
45
|
+
"@reticlehq/core": "3.1.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/babel__core": "^7.20.5",
|
|
49
|
+
"esbuild": "^0.28.2",
|
|
49
50
|
"svelte": "^5.56.10",
|
|
50
51
|
"vite": "^8",
|
|
51
|
-
"
|
|
52
|
+
"@reticlehq/react": "3.1.0"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
55
|
"vite": ">=4"
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"node": ">=20.0.0"
|
|
66
67
|
},
|
|
67
68
|
"scripts": {
|
|
68
|
-
"build": "tsc -b && node scripts/build-cjs.mjs",
|
|
69
|
+
"build": "tsc -b && node ../../../scripts/alias-dist.mjs && node scripts/build-cjs.mjs",
|
|
69
70
|
"typecheck": "tsc -b",
|
|
70
71
|
"lint": "eslint src",
|
|
71
72
|
"test:unit": "vitest run src --passWithNoTests",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|