snoop-ui 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Snoop UI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ <div align=center>
2
+ <img width="763" height="237" alt="snoop_banner" src="https://github.com/user-attachments/assets/d15a6912-95fe-44d7-98e9-079a25636374" />
3
+ </div>
4
+ <br>
5
+
6
+ **Swagger UI for your React components.** Install it, open `/ui`, and see what
7
+ you shipped and what is wrong with it.
8
+
9
+ Snoop is a local, no-AI inspector for React apps. It scans a project, lists every
10
+ component with its real geometry and style sources, flags style problems with
11
+ deterministic rules, and serves the result from a Vite dev plugin.
12
+
13
+ > **Status: pre-release, M4 style provenance.** The package is not
14
+ > installable yet. The scanner discovers exported components and writes a
15
+ > deterministic manifest, `snoop init` / `snoop scan` work from the repository,
16
+ > and the Vite plugin serves the read-only dashboard at `/ui` with a live
17
+ > manifest API and WebSocket bridge. The runtime agent measures the running app,
18
+ > draws the overlay, and maps a click back to the component that rendered it.
19
+ > The detail panel now labels each computed style with its source file, line,
20
+ > selector, and exact-or-inferred confidence for inline, global CSS, CSS
21
+ > Modules, and Tailwind.
22
+
23
+ ## What the MVP will do
24
+
25
+ - Scan a React project and build a deterministic component manifest.
26
+ - Serve `/ui` with a component tree, geometry, and style provenance.
27
+ - Measure real boxes and matched CSS in the running app.
28
+ - Jump from a click in the app to the component that rendered the element.
29
+ - Flag token matches, near matches, and undefined CSS variables with severity
30
+ and confidence.
31
+ - Keep known issues quiet with a baseline.
32
+ - Stay local: no telemetry, no network calls, no AI.
33
+
34
+ ## Documentation
35
+
36
+ - [Documentation index](docs/README.md)
37
+ - [Quickstart](docs/quickstart.md)
38
+ - [Concepts](docs/concepts.md)
39
+ - [Product and strategy handbook](handbook.md)
40
+ - [Engineering instructions for agents](AGENTS.md)
41
+
42
+ ## Development
43
+
44
+ ```bash
45
+ pnpm install
46
+ pnpm check
47
+ ```
48
+
49
+ Individual commands:
50
+
51
+ ```bash
52
+ pnpm build
53
+ pnpm test
54
+ pnpm typecheck
55
+ pnpm lint
56
+ pnpm docs:links
57
+ pnpm pack:smoke
58
+ ```
59
+
60
+ The package remains private with an unresolved license until the publishing
61
+ decision is recorded. See [CONTRIBUTING.md](CONTRIBUTING.md).
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "snoop-ui",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.10.0",
7
+ "engines": { "node": ">=22.18.0" },
8
+ "description": "A local, no-AI inspector for React apps: component geometry, style sources, and deterministic diagnostics.",
9
+ "keywords": [
10
+ "react",
11
+ "vite",
12
+ "devtools",
13
+ "css",
14
+ "tailwind",
15
+ "inspector"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/snoop-ui/snoop-ui.git"
20
+ },
21
+ "homepage": "https://github.com/snoop-ui/snoop-ui#readme",
22
+ "bin": {
23
+ "snoop": "./packages/cli/dist/cli.js"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./packages/core/dist/index.d.ts",
28
+ "import": "./packages/core/dist/index.js"
29
+ },
30
+ "./vite": {
31
+ "types": "./packages/adapter-vite/dist/vite.d.ts",
32
+ "import": "./packages/adapter-vite/dist/vite.js"
33
+ },
34
+ "./schemas/*": "./schemas/*"
35
+ },
36
+ "files": [
37
+ "packages/core/dist",
38
+ "packages/runtime/dist",
39
+ "packages/adapter-vite/dist",
40
+ "packages/cli/dist",
41
+ "schemas",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "scripts": {
46
+ "build": "pnpm -r run build",
47
+ "build:core": "pnpm --filter @snoop-ui/core run build",
48
+ "build:runtime": "pnpm --filter @snoop-ui/runtime run build",
49
+ "build:unplugin": "pnpm --filter @snoop-ui/unplugin run build",
50
+ "build:adapter-vite": "pnpm --filter @snoop-ui/adapter-vite run build",
51
+ "build:adapter-next": "pnpm --filter @snoop-ui/adapter-next run build",
52
+ "build:adapter-remix": "pnpm --filter @snoop-ui/adapter-remix run build",
53
+ "build:adapter-tanstack": "pnpm --filter @snoop-ui/adapter-tanstack run build",
54
+ "build:cli": "pnpm --filter @snoop-ui/cli run build",
55
+ "dev": "pnpm --filter @snoop-ui/core run dev & pnpm --filter @snoop-ui/adapter-vite run dev",
56
+ "test": "pnpm -r run test",
57
+ "test:watch": "pnpm -r run test:watch",
58
+ "test:browser": "pnpm --filter @snoop-ui/core run test:browser",
59
+ "typecheck": "pnpm -r run typecheck",
60
+ "lint": "pnpm -r run lint",
61
+ "format": "pnpm -r run format",
62
+ "format:check": "pnpm -r run format:check",
63
+ "schemas": "pnpm -r run schemas",
64
+ "bundle:size": "pnpm -r run bundle:size",
65
+ "docs:links": "pnpm -r run docs:links",
66
+ "licenses:check": "pnpm -r run licenses:check",
67
+ "pack:smoke": "pnpm --filter @snoop-ui/cli run pack:smoke",
68
+ "publint": "npx publint",
69
+ "attw": "npx attw --pack . --profile esm-only",
70
+ "check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build && pnpm run bundle:size && pnpm run test:browser && pnpm run schemas && pnpm run publint && pnpm run docs:links && pnpm run licenses:check"
71
+ },
72
+ "devDependencies": {
73
+ "@arethetypeswrong/cli": "^0.18.5",
74
+ "@changesets/cli": "^3.0.3",
75
+ "@eslint/js": "^10.0.1",
76
+ "@preact/preset-vite": "^2.10.6",
77
+ "@types/babel__traverse": "7.28.0",
78
+ "@types/node": "^22.20.5",
79
+ "@types/react": "^19.3.0",
80
+ "@types/react-dom": "^19.3.0",
81
+ "@types/ws": "^8.18.2",
82
+ "eslint": "^10.14.0",
83
+ "eslint-config-prettier": "^10.1.8",
84
+ "globals": "^16.5.0",
85
+ "prettier": "^3.10.0",
86
+ "publint": "^0.3.25",
87
+ "react": "^19.3.1",
88
+ "react-dom": "^19.3.1",
89
+ "tsup": "^9.0.0",
90
+ "typescript": "^6.0.3",
91
+ "typescript-eslint": "^8.72.2",
92
+ "vite": "^8.3.8",
93
+ "vitest": "^5.1.0"
94
+ }
95
+ }
@@ -0,0 +1,29 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface SnoopPluginOptions {
4
+ /**
5
+ * Where the dashboard is served. Defaults to the config value (`/ui`).
6
+ */
7
+ basePath?: string;
8
+ /**
9
+ * The app URL loaded inside the dashboard preview iframe. Defaults to `/`.
10
+ */
11
+ appUrl?: string;
12
+ /** Override content-hash caching. Enabled by default. */
13
+ cache?: boolean;
14
+ /**
15
+ * Inject the runtime agent into the app. Enabled by default.
16
+ */
17
+ runtime?: boolean;
18
+ /**
19
+ * Override the built dashboard directory. Used by tests and unusual installs.
20
+ * @internal
21
+ */
22
+ clientDir?: string;
23
+ }
24
+ declare function snoop(options?: SnoopPluginOptions): Plugin;
25
+ /** Read original -> generated class names from Vite's CSS Modules JS output. */
26
+ declare function parseCssModuleExports(code: string): Record<string, string> | undefined;
27
+
28
+ export { parseCssModuleExports, snoop };
29
+ export type { SnoopPluginOptions };
@@ -0,0 +1,281 @@
1
+ // src/vite.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { basename, dirname, extname, join, relative, resolve, sep } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { loadConfig } from "@snoop-ui/core/config.js";
6
+ import { createSnoopMiddleware, normalizeBasePath } from "@snoop-ui/core/server/http.js";
7
+ import { createScanService } from "@snoop-ui/core/server/service.js";
8
+ import { createSnoopWebSocket } from "@snoop-ui/core/server/ws.js";
9
+ import { shouldTagFile, tagSource } from "@snoop-ui/core/runtime/tagging.js";
10
+ import { toPosixPath } from "@snoop-ui/core/utils/paths.js";
11
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
12
+ var VIRTUAL_AGENT_ID = "virtual:snoop-agent";
13
+ var RESOLVED_AGENT_ID = `\0${VIRTUAL_AGENT_ID}`;
14
+ var MISSING_AGENT_MODULE = `console.warn('[snoop] The runtime agent bundle is missing. Run \`pnpm build\` in the Snoop package.');`;
15
+ function snoop(options = {}) {
16
+ let dispose;
17
+ let root = process.cwd();
18
+ return {
19
+ name: "snoop-ui",
20
+ apply: "serve",
21
+ // The tagging transform has to see the original JSX, before the React
22
+ // plugin compiles it away.
23
+ enforce: "pre",
24
+ configResolved(config) {
25
+ root = config.root;
26
+ },
27
+ resolveId(id) {
28
+ if (id === VIRTUAL_AGENT_ID || id === RESOLVED_AGENT_ID) {
29
+ return RESOLVED_AGENT_ID;
30
+ }
31
+ return void 0;
32
+ },
33
+ load(id) {
34
+ if (id === RESOLVED_AGENT_ID) {
35
+ return loadAgentModule();
36
+ }
37
+ return void 0;
38
+ },
39
+ transform: {
40
+ order: "pre",
41
+ handler(code, id) {
42
+ return transformSource(code, id, root);
43
+ }
44
+ },
45
+ // The agent script has to be injected before Vite's own HTML transform
46
+ // runs. Only then does Vite rewrite the inline module into a proxied
47
+ // module request, which is what makes `virtual:snoop-agent` resolve.
48
+ transformIndexHtml: {
49
+ order: "pre",
50
+ handler() {
51
+ if (options.runtime === false) {
52
+ return [];
53
+ }
54
+ return [
55
+ {
56
+ tag: "script",
57
+ attrs: { type: "module" },
58
+ children: `import '${VIRTUAL_AGENT_ID}';`,
59
+ injectTo: "body"
60
+ }
61
+ ];
62
+ }
63
+ },
64
+ async configureServer(server) {
65
+ const cwd = server.config.root;
66
+ const config = await loadConfig({ cwd });
67
+ const basePath = normalizeBasePath(options.basePath ?? config.basePath);
68
+ const appUrl = options.appUrl ?? config.appUrl;
69
+ const clientDir = options.clientDir ?? findClientDir();
70
+ const service = createScanService({
71
+ cwd,
72
+ include: config.include,
73
+ exclude: config.exclude,
74
+ tokens: config.tokens,
75
+ ...options.cache !== void 0 ? { cache: options.cache } : {},
76
+ resolveCssModules: (files) => resolveCssModules(server, files),
77
+ onError(error) {
78
+ server.config.logger.error(
79
+ `[snoop] scan failed: ${error instanceof Error ? error.message : String(error)}`
80
+ );
81
+ }
82
+ });
83
+ const bridge = createSnoopWebSocket({ service });
84
+ server.middlewares.use(
85
+ createSnoopMiddleware({
86
+ service,
87
+ clientDir,
88
+ basePath,
89
+ appUrl,
90
+ ...config.editor ? { editor: config.editor } : {},
91
+ agents: bridge
92
+ })
93
+ );
94
+ const onUpgrade = (req, socket, head) => {
95
+ bridge.handleUpgrade(req, socket, head);
96
+ };
97
+ server.httpServer?.on("upgrade", onUpgrade);
98
+ let timer;
99
+ const scheduleRefresh = () => {
100
+ if (timer) {
101
+ clearTimeout(timer);
102
+ }
103
+ timer = setTimeout(() => {
104
+ timer = void 0;
105
+ void service.refresh();
106
+ }, 120);
107
+ };
108
+ const onFileChange = (_event, file) => {
109
+ if (shouldRefresh(file, cwd)) {
110
+ scheduleRefresh();
111
+ }
112
+ };
113
+ server.watcher.on("all", onFileChange);
114
+ dispose = () => {
115
+ if (timer) {
116
+ clearTimeout(timer);
117
+ timer = void 0;
118
+ }
119
+ server.watcher.off("all", onFileChange);
120
+ server.httpServer?.off("upgrade", onUpgrade);
121
+ bridge.close();
122
+ service.dispose();
123
+ };
124
+ server.httpServer?.once("close", () => {
125
+ dispose?.();
126
+ dispose = void 0;
127
+ });
128
+ warnIfExposed(server);
129
+ if (server.httpServer?.listening) {
130
+ void service.refresh();
131
+ } else if (server.httpServer) {
132
+ server.httpServer.once("listening", () => void service.refresh());
133
+ } else {
134
+ void service.refresh();
135
+ }
136
+ },
137
+ closeBundle() {
138
+ dispose?.();
139
+ dispose = void 0;
140
+ }
141
+ };
142
+ }
143
+ function transformSource(code, id, root) {
144
+ const [filePath] = id.split("?");
145
+ if (!filePath || !filePath.startsWith("/") || filePath.includes("/node_modules/")) {
146
+ return void 0;
147
+ }
148
+ if (!shouldTagFile(filePath)) {
149
+ return void 0;
150
+ }
151
+ const relativePath = relative(root, filePath);
152
+ if (!relativePath || relativePath.startsWith("..")) {
153
+ return void 0;
154
+ }
155
+ const file = toPosixPath(relativePath);
156
+ const result = tagSource(code, { file, extension: extname(filePath) });
157
+ if (result.tags.length === 0) {
158
+ return void 0;
159
+ }
160
+ return { code: result.code, map: null };
161
+ }
162
+ async function resolveCssModules(server, files) {
163
+ const maps = {};
164
+ await Promise.all(
165
+ files.map(async (file) => {
166
+ try {
167
+ const result = await server.transformRequest(`/${file}`);
168
+ const map = parseCssModuleExports(result?.code ?? "");
169
+ if (map) {
170
+ maps[file] = map;
171
+ }
172
+ } catch {
173
+ }
174
+ })
175
+ );
176
+ return maps;
177
+ }
178
+ function parseCssModuleExports(code) {
179
+ const named = /* @__PURE__ */ new Map();
180
+ const namedPattern = /export const (\w+) = ("(?:[^"\\]|\\.)*");/g;
181
+ for (const match of code.matchAll(namedPattern)) {
182
+ const name = match[1];
183
+ const value = match[2];
184
+ if (name && value) {
185
+ named.set(name, decodeStringLiteral(value));
186
+ }
187
+ }
188
+ const defaultMatch = /export default\s*(\{[\s\S]*?\});?\s*(?:import\.meta|$)/.exec(code);
189
+ if (!defaultMatch?.[1]) {
190
+ return named.size > 0 ? Object.fromEntries(named) : void 0;
191
+ }
192
+ const body = defaultMatch[1];
193
+ const map = {};
194
+ const entryPattern = /(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|(\w+))\s*:\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|(\w+))/g;
195
+ for (const match of body.matchAll(entryPattern)) {
196
+ const key = match[1] ?? match[2] ?? match[3];
197
+ const quotedValue = match[4] ?? match[5];
198
+ const reference = match[6];
199
+ if (!key) {
200
+ continue;
201
+ }
202
+ const value = quotedValue !== void 0 ? decodeStringLiteral(quotedValue) : named.get(reference ?? "");
203
+ if (value) {
204
+ map[key] = value;
205
+ }
206
+ }
207
+ return Object.keys(map).length > 0 ? map : named.size > 0 ? Object.fromEntries(named) : void 0;
208
+ }
209
+ function decodeStringLiteral(value) {
210
+ const unquoted = /^(['"]).*\1$/s.test(value) ? value.slice(1, -1) : value;
211
+ return unquoted.replace(/\\(.)/g, "$1");
212
+ }
213
+ function loadAgentModule() {
214
+ const file = findAgentFile();
215
+ if (!file) {
216
+ return MISSING_AGENT_MODULE;
217
+ }
218
+ return readFileSync(file, "utf8");
219
+ }
220
+ function findAgentFile() {
221
+ const here = dirname(fileURLToPath(import.meta.url));
222
+ const candidates = [
223
+ // Bundled: dist/vite.js -> dist/runtime/agent.js.
224
+ resolve(here, "runtime", "agent.js"),
225
+ // Source layout: packages/adapter-vite/src/vite.ts -> packages/runtime/src/agent.ts
226
+ resolve(here, "..", "..", "runtime", "src", "agent.ts"),
227
+ // Monorepo root: packages/runtime/dist/agent.js
228
+ resolve(here, "..", "..", "..", "runtime", "dist", "agent.js")
229
+ ];
230
+ for (const candidate of candidates) {
231
+ if (existsSync(candidate)) {
232
+ return candidate;
233
+ }
234
+ }
235
+ return void 0;
236
+ }
237
+ function findClientDir() {
238
+ const here = dirname(fileURLToPath(import.meta.url));
239
+ const candidates = [
240
+ // Bundled: dist/vite.js -> dist/client.
241
+ resolve(here, "client"),
242
+ // Source layout: packages/adapter-vite/src/vite.ts -> packages/adapter-vite/src/client
243
+ resolve(here, "..", "client")
244
+ ];
245
+ for (const candidate of candidates) {
246
+ if (existsSync(join(candidate, "index.html"))) {
247
+ return candidate;
248
+ }
249
+ }
250
+ return candidates[0] ?? resolve(here, "client");
251
+ }
252
+ function shouldRefresh(file, cwd) {
253
+ const relativePath = relative(cwd, file).split(sep).join("/");
254
+ if (!relativePath || relativePath.startsWith("../")) {
255
+ return false;
256
+ }
257
+ if (/(^|\/)(node_modules|dist|build|coverage|\.git|\.snoop)(\/|$)/.test(relativePath)) {
258
+ return false;
259
+ }
260
+ if (basename(relativePath).startsWith("snoop.config.")) {
261
+ return true;
262
+ }
263
+ if (basename(relativePath).startsWith("tailwind.config.")) {
264
+ return true;
265
+ }
266
+ return /\.(jsx?|tsx?|mjs|cjs|mts|cts|css)$/.test(relativePath);
267
+ }
268
+ function warnIfExposed(server) {
269
+ const host = server.config.server.host;
270
+ const exposed = host === true || typeof host === "string" && host.length > 0 && !LOCAL_HOSTS.has(host);
271
+ if (exposed) {
272
+ server.config.logger.warn(
273
+ "[snoop] The Vite dev server is exposed beyond localhost. The dashboard is read-only, but keep it on a trusted network."
274
+ );
275
+ }
276
+ }
277
+ export {
278
+ parseCssModuleExports,
279
+ snoop
280
+ };
281
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Plugin, ViteDevServer } from 'vite';\nimport { loadConfig } from '@snoop-ui/core/config.js';\nimport { createSnoopMiddleware, normalizeBasePath } from '@snoop-ui/core/server/http.js';\nimport { createScanService } from '@snoop-ui/core/server/service.js';\nimport { createSnoopWebSocket } from '@snoop-ui/core/server/ws.js';\nimport { shouldTagFile, tagSource } from '@snoop-ui/core/runtime/tagging.js';\nimport { toPosixPath } from '@snoop-ui/core/utils/paths.js';\n\nexport interface SnoopPluginOptions {\n /**\n * Where the dashboard is served. Defaults to the config value (`/ui`).\n */\n basePath?: string;\n /**\n * The app URL loaded inside the dashboard preview iframe. Defaults to `/`.\n */\n appUrl?: string;\n /** Override content-hash caching. Enabled by default. */\n cache?: boolean;\n /**\n * Inject the runtime agent into the app. Enabled by default.\n */\n runtime?: boolean;\n /**\n * Override the built dashboard directory. Used by tests and unusual installs.\n * @internal\n */\n clientDir?: string;\n}\n\nconst LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);\nconst VIRTUAL_AGENT_ID = 'virtual:snoop-agent';\nconst RESOLVED_AGENT_ID = `\\0${VIRTUAL_AGENT_ID}`;\nconst MISSING_AGENT_MODULE = `console.warn('[snoop] The runtime agent bundle is missing. Run \\`pnpm build\\` in the Snoop package.');`;\n\nexport function snoop(options: SnoopPluginOptions = {}): Plugin {\n let dispose: (() => void) | undefined;\n let root = process.cwd();\n\n return {\n name: 'snoop-ui',\n apply: 'serve',\n // The tagging transform has to see the original JSX, before the React\n // plugin compiles it away.\n enforce: 'pre',\n\n configResolved(config) {\n root = config.root;\n },\n\n resolveId(id) {\n if (id === VIRTUAL_AGENT_ID || id === RESOLVED_AGENT_ID) {\n return RESOLVED_AGENT_ID;\n }\n\n return undefined;\n },\n\n load(id) {\n if (id === RESOLVED_AGENT_ID) {\n return loadAgentModule();\n }\n\n return undefined;\n },\n\n transform: {\n order: 'pre',\n handler(code, id) {\n return transformSource(code, id, root);\n },\n },\n\n // The agent script has to be injected before Vite's own HTML transform\n // runs. Only then does Vite rewrite the inline module into a proxied\n // module request, which is what makes `virtual:snoop-agent` resolve.\n transformIndexHtml: {\n order: 'pre',\n handler() {\n if (options.runtime === false) {\n return [];\n }\n\n return [\n {\n tag: 'script',\n attrs: { type: 'module' },\n children: `import '${VIRTUAL_AGENT_ID}';`,\n injectTo: 'body' as const,\n },\n ];\n },\n },\n\n async configureServer(server) {\n const cwd = server.config.root;\n const config = await loadConfig({ cwd });\n const basePath = normalizeBasePath(options.basePath ?? config.basePath);\n const appUrl = options.appUrl ?? config.appUrl;\n const clientDir = options.clientDir ?? findClientDir();\n\n const service = createScanService({\n cwd,\n include: config.include,\n exclude: config.exclude,\n tokens: config.tokens,\n ...(options.cache !== undefined ? { cache: options.cache } : {}),\n resolveCssModules: (files) => resolveCssModules(server, files),\n onError(error) {\n server.config.logger.error(\n `[snoop] scan failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n },\n });\n\n const bridge = createSnoopWebSocket({ service });\n\n server.middlewares.use(\n createSnoopMiddleware({\n service,\n clientDir,\n basePath,\n appUrl,\n ...(config.editor ? { editor: config.editor } : {}),\n agents: bridge,\n }),\n );\n\n const onUpgrade = (\n req: Parameters<typeof bridge.handleUpgrade>[0],\n socket: Parameters<typeof bridge.handleUpgrade>[1],\n head: Buffer,\n ) => {\n bridge.handleUpgrade(req, socket, head);\n };\n\n server.httpServer?.on('upgrade', onUpgrade);\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const scheduleRefresh = () => {\n if (timer) {\n clearTimeout(timer);\n }\n\n timer = setTimeout(() => {\n timer = undefined;\n void service.refresh();\n }, 120);\n };\n\n const onFileChange = (_event: string, file: string) => {\n if (shouldRefresh(file, cwd)) {\n scheduleRefresh();\n }\n };\n\n server.watcher.on('all', onFileChange);\n\n dispose = () => {\n if (timer) {\n clearTimeout(timer);\n timer = undefined;\n }\n\n server.watcher.off('all', onFileChange);\n server.httpServer?.off('upgrade', onUpgrade);\n bridge.close();\n service.dispose();\n };\n\n server.httpServer?.once('close', () => {\n dispose?.();\n dispose = undefined;\n });\n\n warnIfExposed(server);\n\n // `transformRequest` (used for CSS Modules) needs the plugin container to\n // be fully ready. Defer the first scan until the server is listening.\n if (server.httpServer?.listening) {\n void service.refresh();\n } else if (server.httpServer) {\n server.httpServer.once('listening', () => void service.refresh());\n } else {\n void service.refresh();\n }\n },\n\n closeBundle() {\n dispose?.();\n dispose = undefined;\n },\n };\n}\n\nfunction transformSource(code: string, id: string, root: string) {\n const [filePath] = id.split('?');\n\n if (!filePath || !filePath.startsWith('/') || filePath.includes('/node_modules/')) {\n return undefined;\n }\n\n if (!shouldTagFile(filePath)) {\n return undefined;\n }\n\n const relativePath = relative(root, filePath);\n if (!relativePath || relativePath.startsWith('..')) {\n return undefined;\n }\n\n const file = toPosixPath(relativePath);\n const result = tagSource(code, { file, extension: extname(filePath) });\n\n if (result.tags.length === 0) {\n return undefined;\n }\n\n return { code: result.code, map: null };\n}\n\n/**\n * Ask Vite for each CSS Module's generated class mapping. The dev server's own\n * transform owns the naming scheme, so Snoop never reimplements it.\n */\nasync function resolveCssModules(\n server: ViteDevServer,\n files: string[],\n): Promise<Record<string, Record<string, string>>> {\n const maps: Record<string, Record<string, string>> = {};\n\n await Promise.all(\n files.map(async (file) => {\n try {\n const result = await server.transformRequest(`/${file}`);\n const map = parseCssModuleExports(result?.code ?? '');\n\n if (map) {\n maps[file] = map;\n }\n } catch {\n // A module that Vite cannot transform simply keeps its original\n // selectors. The resolver labels those matches INFERRED.\n }\n }),\n );\n\n return maps;\n}\n\n/** Read original -> generated class names from Vite's CSS Modules JS output. */\nexport function parseCssModuleExports(code: string): Record<string, string> | undefined {\n const named = new Map<string, string>();\n const namedPattern = /export const (\\w+) = (\"(?:[^\"\\\\]|\\\\.)*\");/g;\n\n for (const match of code.matchAll(namedPattern)) {\n const name = match[1];\n const value = match[2];\n if (name && value) {\n named.set(name, decodeStringLiteral(value));\n }\n }\n\n const defaultMatch = /export default\\s*(\\{[\\s\\S]*?\\});?\\s*(?:import\\.meta|$)/.exec(code);\n\n if (!defaultMatch?.[1]) {\n return named.size > 0 ? Object.fromEntries(named) : undefined;\n }\n\n const body = defaultMatch[1];\n const map: Record<string, string> = {};\n const entryPattern =\n /(?:\"((?:[^\"\\\\]|\\\\.)*)\"|'((?:[^'\\\\]|\\\\.)*)'|(\\w+))\\s*:\\s*(?:\"((?:[^\"\\\\]|\\\\.)*)\"|'((?:[^'\\\\]|\\\\.)*)'|(\\w+))/g;\n\n for (const match of body.matchAll(entryPattern)) {\n const key = match[1] ?? match[2] ?? match[3];\n const quotedValue = match[4] ?? match[5];\n const reference = match[6];\n\n if (!key) {\n continue;\n }\n\n const value =\n quotedValue !== undefined ? decodeStringLiteral(quotedValue) : named.get(reference ?? '');\n if (value) {\n map[key] = value;\n }\n }\n\n return Object.keys(map).length > 0 ? map : named.size > 0 ? Object.fromEntries(named) : undefined;\n}\n\nfunction decodeStringLiteral(value: string): string {\n const unquoted = /^(['\"]).*\\1$/s.test(value) ? value.slice(1, -1) : value;\n return unquoted.replace(/\\\\(.)/g, '$1');\n}\n\nfunction loadAgentModule(): string {\n const file = findAgentFile();\n\n if (!file) {\n return MISSING_AGENT_MODULE;\n }\n\n return readFileSync(file, 'utf8');\n}\n\nfunction findAgentFile(): string | undefined {\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n // Bundled: dist/vite.js -> dist/runtime/agent.js.\n resolve(here, 'runtime', 'agent.js'),\n // Source layout: packages/adapter-vite/src/vite.ts -> packages/runtime/src/agent.ts\n resolve(here, '..', '..', 'runtime', 'src', 'agent.ts'),\n // Monorepo root: packages/runtime/dist/agent.js\n resolve(here, '..', '..', '..', 'runtime', 'dist', 'agent.js'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return undefined;\n}\n\nfunction findClientDir(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n // Bundled: dist/vite.js -> dist/client.\n resolve(here, 'client'),\n // Source layout: packages/adapter-vite/src/vite.ts -> packages/adapter-vite/src/client\n resolve(here, '..', 'client'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(join(candidate, 'index.html'))) {\n return candidate;\n }\n }\n\n return candidates[0] ?? resolve(here, 'client');\n}\n\nfunction shouldRefresh(file: string, cwd: string): boolean {\n const relativePath = relative(cwd, file).split(sep).join('/');\n\n if (!relativePath || relativePath.startsWith('../')) {\n return false;\n }\n\n if (/(^|\\/)(node_modules|dist|build|coverage|\\.git|\\.snoop)(\\/|$)/.test(relativePath)) {\n return false;\n }\n\n if (basename(relativePath).startsWith('snoop.config.')) {\n return true;\n }\n\n if (basename(relativePath).startsWith('tailwind.config.')) {\n return true;\n }\n\n return /\\.(jsx?|tsx?|mjs|cjs|mts|cts|css)$/.test(relativePath);\n}\n\nfunction warnIfExposed(server: ViteDevServer): void {\n const host = server.config.server.host;\n const exposed =\n host === true || (typeof host === 'string' && host.length > 0 && !LOCAL_HOSTS.has(host));\n\n if (exposed) {\n server.config.logger.warn(\n '[snoop] The Vite dev server is exposed beyond localhost. The dashboard is read-only, but keep it on a trusted network.',\n );\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { resolve as resolve3 } from "path";
5
+ import { cac } from "cac";
6
+ import { readFileSync } from "fs";
7
+ import { relativePosix } from "@snoop-ui/core/utils/paths.js";
8
+
9
+ // src/cli/init.ts
10
+ import { access, writeFile } from "fs/promises";
11
+ import { resolve } from "path";
12
+ var CONFIG_TEMPLATE = `import { defineConfig } from 'snoop-ui';
13
+
14
+ export default defineConfig({
15
+ // include: ['src/**/*.{jsx,tsx}'],
16
+ // exclude: ['**/*.stories.{js,jsx,ts,tsx}'],
17
+ // tokens: ['src/styles/tokens.css'],
18
+ });
19
+ `;
20
+ async function initConfig(options = {}) {
21
+ const cwd = resolve(options.cwd ?? process.cwd());
22
+ const path = resolve(cwd, "snoop.config.ts");
23
+ if (!options.force && await exists(path)) {
24
+ throw new Error(`A Snoop config already exists at ${path}. Use --force to overwrite it.`);
25
+ }
26
+ try {
27
+ await writeFile(path, CONFIG_TEMPLATE, {
28
+ encoding: "utf8",
29
+ flag: options.force ? "w" : "wx"
30
+ });
31
+ } catch (error) {
32
+ if (!options.force && isAlreadyExists(error)) {
33
+ throw new Error(`A Snoop config already exists at ${path}. Use --force to overwrite it.`, {
34
+ cause: error
35
+ });
36
+ }
37
+ throw error;
38
+ }
39
+ return { path };
40
+ }
41
+ async function exists(path) {
42
+ try {
43
+ await access(path);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ function isAlreadyExists(error) {
50
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
51
+ }
52
+
53
+ // src/cli/scan.ts
54
+ import { resolve as resolve2 } from "path";
55
+ import { loadConfig } from "@snoop-ui/core/config.js";
56
+ import { scanProject, writeManifest } from "@snoop-ui/core/scanner/index.js";
57
+ var DEFAULT_MANIFEST_PATH = ".snoop/manifest.json";
58
+ async function runScan(options = {}) {
59
+ const cwd = resolve2(options.cwd ?? process.cwd());
60
+ const config = await loadConfig({
61
+ cwd,
62
+ ...options.configFile ? { configFile: options.configFile } : {}
63
+ });
64
+ const result = await scanProject({
65
+ cwd,
66
+ include: config.include,
67
+ exclude: config.exclude,
68
+ tokens: config.tokens,
69
+ cache: options.cache
70
+ });
71
+ const outPath = resolve2(cwd, options.out ?? DEFAULT_MANIFEST_PATH);
72
+ await writeManifest(outPath, result.manifest);
73
+ return { result, outPath };
74
+ }
75
+
76
+ // src/cli/index.ts
77
+ var pkg = JSON.parse(readFileSync(resolve3(import.meta.dirname, "package.json"), "utf8"));
78
+ var cli = cac("snoop");
79
+ cli.command("init", "Create a snoop.config.ts file").option("--force", "Overwrite an existing config").option("--cwd <path>", "Project directory").action(async (options) => {
80
+ const cwd = resolve3(options.cwd ?? process.cwd());
81
+ const result = await initConfig({ cwd, force: options.force });
82
+ console.log(`Created ${displayPath(cwd, result.path)}.`);
83
+ });
84
+ cli.command("scan", "Scan the project and write .snoop/manifest.json").option("--out <path>", "Output path", { default: DEFAULT_MANIFEST_PATH }).option("--cwd <path>", "Project directory").option("--config <path>", "Config file to load").option("--no-cache", "Disable the content-hash scan cache").action(async (options) => {
85
+ const cwd = resolve3(options.cwd ?? process.cwd());
86
+ const { result, outPath } = await runScan({
87
+ cwd,
88
+ out: options.out,
89
+ ...options.config ? { configFile: options.config } : {},
90
+ cache: options.cache
91
+ });
92
+ const { stats } = result;
93
+ const output = displayPath(cwd, outPath);
94
+ if (stats.components === 0) {
95
+ console.log(`No components found in ${stats.files} file(s).`);
96
+ } else {
97
+ console.log(`Scanned ${stats.components} component(s) in ${stats.files} file(s).`);
98
+ }
99
+ if (stats.cachedFiles > 0) {
100
+ console.log(`Reused ${stats.cachedFiles} cached file(s).`);
101
+ }
102
+ if (stats.errors > 0) {
103
+ console.log(`${stats.errors} file(s) had warnings; see ${output}.`);
104
+ }
105
+ console.log(`Wrote ${output}.`);
106
+ });
107
+ cli.help();
108
+ cli.version(pkg.version);
109
+ async function main() {
110
+ if (process.argv.slice(2).length === 0) {
111
+ cli.outputHelp();
112
+ return;
113
+ }
114
+ const wantsHelp = process.argv.includes("--help") || process.argv.includes("-h");
115
+ const wantsVersion = process.argv.includes("--version") || process.argv.includes("-v");
116
+ cli.parse(process.argv, { run: false });
117
+ if (!cli.matchedCommand) {
118
+ if (!wantsHelp && !wantsVersion && cli.args[0]) {
119
+ console.error(`Unknown command: ${cli.args[0]}`);
120
+ cli.outputHelp();
121
+ process.exitCode = 1;
122
+ }
123
+ return;
124
+ }
125
+ try {
126
+ await cli.runMatchedCommand();
127
+ } catch (error) {
128
+ console.error(error instanceof Error ? error.message : String(error));
129
+ process.exitCode = 1;
130
+ }
131
+ }
132
+ function displayPath(cwd, path) {
133
+ return relativePosix(cwd, path) || ".";
134
+ }
135
+ await main();
136
+ //# sourceMappingURL=cli.js.map