@llui/vite-plugin 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,10 +7,14 @@ export interface LluiPluginOptions {
7
7
  * to `ws://127.0.0.1:<port>` so an external `llui-mcp` server can forward
8
8
  * tool calls into the running app.
9
9
  *
10
- * Defaults to `false` (opt-in). Pass a number (typically `5200`) to
11
- * enable. When enabled but the MCP server isn't running, the plugin
12
- * returns 404 from its discovery endpoint and the browser silently
13
- * skips the connection no retry noise.
10
+ * When omitted, the plugin checks whether `@llui/mcp` is resolvable from
11
+ * the Vite project root. If yes defaults to `5200`. If no stays
12
+ * disabled. This means installing `@llui/mcp` (+ starting its server)
13
+ * Just Works without an explicit config entry. Pass an explicit `false`
14
+ * to opt out even when `@llui/mcp` is installed; pass a number to use
15
+ * a non-default port. When enabled but the MCP server isn't running,
16
+ * the plugin returns 404 from its discovery endpoint and the browser
17
+ * silently skips the connection — no retry noise.
14
18
  */
15
19
  mcpPort?: number | false;
16
20
  /**
@@ -35,6 +39,14 @@ export interface LluiPluginOptions {
35
39
  * re-exported from this module. Unknown rule names are ignored.
36
40
  */
37
41
  disabledWarnings?: readonly DiagnosticRule[];
42
+ /**
43
+ * Emit `[llui]`-prefixed `console.info` logs for every transformed
44
+ * component file — state-path bit assignments, mask injections, and
45
+ * helper compile/bail counts. Useful when diagnosing why a binding
46
+ * isn't gated the way you expect, or why a call fell back from
47
+ * template-clone to `elSplit`. Off by default.
48
+ */
49
+ verbose?: boolean;
38
50
  }
39
51
  export default function llui(options?: LluiPluginOptions): Plugin;
40
52
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAKjD,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEhE,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAqBtD,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAExB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CAC7C;AAED,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAuNpE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAOjD,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEhE,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAqBtD,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAExB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IAE5C;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAsCD,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CA2RpE"}
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import MagicString from 'magic-string';
2
2
  import { existsSync, readFileSync, writeFileSync, watch as fsWatch } from 'node:fs';
3
3
  import { dirname, relative, resolve } from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { spawn } from 'node:child_process';
4
6
  import { transformLlui } from './transform.js';
5
7
  import { diagnose } from './diagnostics.js';
6
8
  /**
@@ -25,14 +27,56 @@ function findWorkspaceRoot(start = process.cwd()) {
25
27
  dir = parent;
26
28
  }
27
29
  }
30
+ /**
31
+ * Does `@llui/mcp` resolve from `root`'s node_modules? Uses
32
+ * `require.resolve` so monorepo workspaces and hoisted installs both
33
+ * work. Catches failures silently — the only consequence is that we
34
+ * leave `mcpPort` disabled, which is the safe default.
35
+ */
36
+ function hasMcpPackage(root) {
37
+ try {
38
+ const req = createRequire(resolve(root, 'package.json'));
39
+ req.resolve('@llui/mcp/package.json');
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Resolve the path to the llui-mcp CLI entry. Reads `bin.llui-mcp`
48
+ * from @llui/mcp's package.json and joins it against the package
49
+ * directory. Returns null if @llui/mcp isn't resolvable.
50
+ */
51
+ function resolveMcpCliPath(root) {
52
+ try {
53
+ const req = createRequire(resolve(root, 'package.json'));
54
+ const pkgJsonPath = req.resolve('@llui/mcp/package.json');
55
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
56
+ const binEntry = typeof pkgJson.bin === 'string' ? pkgJson.bin : pkgJson.bin?.['llui-mcp'];
57
+ if (!binEntry)
58
+ return null;
59
+ return resolve(dirname(pkgJsonPath), binEntry);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
28
65
  export default function llui(options = {}) {
29
66
  let devMode = false;
30
- // MCP is opt-in: developers who want interactive debugging pass an
31
- // explicit port. Leaving it off avoids 404 polling in projects that
32
- // don't use the companion server.
33
- const mcpPort = options.mcpPort === false || options.mcpPort === undefined ? null : options.mcpPort;
67
+ // `mcpPort` + `mcpMode` are resolved lazily in `configResolved` so we
68
+ // can check for @llui/mcp in the consuming project's node_modules.
69
+ // - `options.mcpPort === false` → disabled
70
+ // - explicit number → wire-only (user manages the server)
71
+ // - undefined + @llui/mcp found → spawn (plugin starts llui-mcp --http)
72
+ // - undefined + no @llui/mcp → disabled
73
+ let mcpPort = null;
74
+ let mcpMode = 'disabled';
75
+ let mcpCliPath = null;
76
+ let mcpChild = null;
34
77
  const failOnWarning = options.failOnWarning === true;
35
78
  const disabledWarnings = new Set(options.disabledWarnings ?? []);
79
+ const verbose = options.verbose === true;
36
80
  // File-based handshake with @llui/mcp. The MCP server writes a marker
37
81
  // file when its bridge starts; we watch it and send a Vite HMR custom
38
82
  // event so the browser can call __lluiConnect() automatically — without
@@ -95,10 +139,73 @@ export default function llui(options = {}) {
95
139
  enforce: 'pre',
96
140
  configResolved(config) {
97
141
  devMode = config.command === 'serve' || config.mode === 'development';
142
+ if (options.mcpPort === false) {
143
+ mcpMode = 'disabled';
144
+ mcpPort = null;
145
+ }
146
+ else if (typeof options.mcpPort === 'number') {
147
+ mcpMode = 'wire';
148
+ mcpPort = options.mcpPort;
149
+ }
150
+ else if (hasMcpPackage(config.root)) {
151
+ mcpCliPath = resolveMcpCliPath(config.root);
152
+ if (mcpCliPath) {
153
+ mcpMode = 'spawn';
154
+ mcpPort = 5200;
155
+ }
156
+ else {
157
+ mcpMode = 'wire';
158
+ mcpPort = 5200;
159
+ }
160
+ }
161
+ else {
162
+ mcpMode = 'disabled';
163
+ mcpPort = null;
164
+ }
98
165
  },
99
166
  configureServer(server) {
100
- if (mcpPort === null)
167
+ if (mcpPort === null) {
168
+ // #3 diagnostic: MCP server is running but the plugin is opted
169
+ // out. Users in this state usually don't realize the mismatch —
170
+ // loud-and-early log saves the "why isn't my MCP attached" hunt.
171
+ if (existsSync(activeFilePath)) {
172
+ console.warn(`[llui] @llui/mcp server is running (marker at ${activeFilePath}) ` +
173
+ `but the Vite plugin is opted out (mcpPort: false, or @llui/mcp ` +
174
+ `isn't a dep of this project). Add \`llui({ mcpPort: 5200 })\` ` +
175
+ `to vite.config to wire them up, or remove the marker file and ` +
176
+ `stop the MCP server if the mismatch was unintended.`);
177
+ }
101
178
  return;
179
+ }
180
+ // Spawn mode: plugin launches llui-mcp as a child process so
181
+ // `pnpm dev` handles the whole stack. Skip spawning when a marker
182
+ // already exists — something (usually a separate llui-mcp process
183
+ // started before Vite) is already listening. The existing wire
184
+ // behavior takes over from there.
185
+ if (mcpMode === 'spawn' && mcpCliPath !== null && !existsSync(activeFilePath)) {
186
+ mcpChild = spawn(process.execPath, [mcpCliPath, '--http', String(mcpPort)], {
187
+ stdio: ['ignore', 'pipe', 'pipe'],
188
+ env: { ...process.env, LLUI_MCP_PORT: String(mcpPort) },
189
+ });
190
+ mcpChild.stdout?.on('data', (buf) => {
191
+ process.stdout.write(`[mcp] ${buf.toString()}`);
192
+ });
193
+ mcpChild.stderr?.on('data', (buf) => {
194
+ process.stderr.write(`[mcp] ${buf.toString()}`);
195
+ });
196
+ mcpChild.on('exit', (code) => {
197
+ if (code !== 0 && code !== null) {
198
+ console.warn(`[llui] @llui/mcp child exited with code ${code}`);
199
+ }
200
+ mcpChild = null;
201
+ });
202
+ const killChild = () => {
203
+ if (mcpChild && !mcpChild.killed)
204
+ mcpChild.kill('SIGTERM');
205
+ };
206
+ server.httpServer?.on('close', killChild);
207
+ process.once('exit', killChild);
208
+ }
102
209
  // HTTP endpoint: the browser fetches this on load to discover the
103
210
  // current MCP port. Avoids the race where HMR events sent before
104
211
  // the import.meta.hot listener registers get dropped — and lets
@@ -216,7 +323,7 @@ export default function llui(options = {}) {
216
323
  }
217
324
  }
218
325
  }
219
- const result = transformLlui(code, id, devMode, mcpPort);
326
+ const result = transformLlui(code, id, devMode, mcpPort, verbose);
220
327
  if (!result)
221
328
  return undefined;
222
329
  // Apply per-statement edits via MagicString for accurate source maps.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Plugin, ViteDevServer } from 'vite'\nimport MagicString from 'magic-string'\nimport { existsSync, readFileSync, writeFileSync, watch as fsWatch, type FSWatcher } from 'node:fs'\nimport { dirname, relative, resolve } from 'node:path'\nimport { transformLlui } from './transform.js'\nimport { diagnose, type DiagnosticRule } from './diagnostics.js'\n\nexport type { DiagnosticRule } from './diagnostics.js'\n\n/**\n * Locate the workspace root so we share the MCP active marker file\n * with @llui/mcp regardless of which subdirectory the dev server runs in.\n * Mirrors `findWorkspaceRoot` from @llui/mcp — duplicated to avoid a\n * vite-plugin → mcp dependency cycle. The contract must stay in sync.\n */\nfunction findWorkspaceRoot(start: string = process.cwd()): string {\n let dir = resolve(start)\n let lastPackageJson: string | null = null\n while (true) {\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir\n if (existsSync(resolve(dir, '.git'))) return dir\n if (existsSync(resolve(dir, 'package.json'))) lastPackageJson = dir\n const parent = dirname(dir)\n if (parent === dir) return lastPackageJson ?? start\n dir = parent\n }\n}\n\nexport interface LluiPluginOptions {\n /**\n * Port for the MCP debug bridge. In dev mode, the runtime relay connects\n * to `ws://127.0.0.1:<port>` so an external `llui-mcp` server can forward\n * tool calls into the running app.\n *\n * Defaults to `false` (opt-in). Pass a number (typically `5200`) to\n * enable. When enabled but the MCP server isn't running, the plugin\n * returns 404 from its discovery endpoint and the browser silently\n * skips the connection — no retry noise.\n */\n mcpPort?: number | false\n\n /**\n * Treat every compiler diagnostic as a build error.\n *\n * Default `false` — diagnostics are emitted via rollup's `this.warn` and\n * can be ignored. Set to `true` in CI so lint-style regressions (namespace\n * imports, bitmask overflow, spread-in-children, `.map()` on state, etc.)\n * fail the build without requiring a custom `build.rollupOptions.onwarn`\n * handler.\n */\n failOnWarning?: boolean\n\n /**\n * Silence specific diagnostic rules without disabling the whole lint\n * pass. Each message is tagged with a rule name (shown in brackets at\n * the start of every warning, e.g. `[spread-in-children]`). Listing\n * a rule here drops all diagnostics with that tag before rollup sees\n * them — so they don't fire via `this.warn` and don't fail the build\n * even when `failOnWarning` is enabled.\n *\n * The valid rule names are enumerated by the `DiagnosticRule` type\n * re-exported from this module. Unknown rule names are ignored.\n */\n disabledWarnings?: readonly DiagnosticRule[]\n}\n\nexport default function llui(options: LluiPluginOptions = {}): Plugin {\n let devMode = false\n // MCP is opt-in: developers who want interactive debugging pass an\n // explicit port. Leaving it off avoids 404 polling in projects that\n // don't use the companion server.\n const mcpPort =\n options.mcpPort === false || options.mcpPort === undefined ? null : options.mcpPort\n const failOnWarning = options.failOnWarning === true\n const disabledWarnings = new Set<string>(options.disabledWarnings ?? [])\n\n // File-based handshake with @llui/mcp. The MCP server writes a marker\n // file when its bridge starts; we watch it and send a Vite HMR custom\n // event so the browser can call __lluiConnect() automatically — without\n // retry spam, regardless of whether MCP or Vite started first.\n const activeFilePath = resolve(findWorkspaceRoot(), 'node_modules/.cache/llui-mcp/active.json')\n let mcpWatcher: FSWatcher | null = null\n let dirWatcher: FSWatcher | null = null\n // Cached once Vite's HTTP server emits `listening`. `stampDevUrl()`\n // uses this to write the URL into the marker file — either immediately\n // (if MCP already started and wrote one) or later when the marker\n // appears via the directory watcher (MCP-starts-after-Vite path).\n let cachedDevUrl: string | null = null\n\n function readMcpMarker(): { port: number; devUrl?: string } | null {\n try {\n if (!existsSync(activeFilePath)) return null\n const data = JSON.parse(readFileSync(activeFilePath, 'utf8')) as {\n port?: number\n devUrl?: string\n }\n if (typeof data.port !== 'number') return null\n return { port: data.port, ...(data.devUrl ? { devUrl: data.devUrl } : {}) }\n } catch {\n return null\n }\n }\n\n /**\n * Idempotently write `cachedDevUrl` into the marker file. No-op if the\n * URL hasn't been captured yet (Vite hasn't emitted `listening`) or if\n * the marker file doesn't exist (MCP hasn't started yet). Covers both\n * orderings — the listening hook calls this after caching, and the\n * directory watcher calls it when the marker appears later.\n */\n function stampDevUrl(): void {\n if (cachedDevUrl === null) return\n if (!existsSync(activeFilePath)) return\n try {\n const marker = JSON.parse(readFileSync(activeFilePath, 'utf8')) as Record<string, unknown>\n if (marker.devUrl === cachedDevUrl) return\n marker.devUrl = cachedDevUrl\n writeFileSync(activeFilePath, JSON.stringify(marker))\n } catch {\n // Best-effort — failure to update the marker should not crash Vite\n }\n }\n\n function notifyMcpReady(server: ViteDevServer): void {\n const marker = readMcpMarker()\n if (marker === null) return\n server.ws.send({ type: 'custom', event: 'llui:mcp-ready', data: marker })\n }\n\n function notifyMcpOffline(server: ViteDevServer): void {\n server.ws.send({ type: 'custom', event: 'llui:mcp-offline', data: {} })\n }\n\n return {\n name: 'llui',\n enforce: 'pre',\n\n configResolved(config) {\n devMode = config.command === 'serve' || config.mode === 'development'\n },\n\n configureServer(server) {\n if (mcpPort === null) return\n\n // HTTP endpoint: the browser fetches this on load to discover the\n // current MCP port. Avoids the race where HMR events sent before\n // the import.meta.hot listener registers get dropped — and lets\n // the browser connect to the actual port (which may differ from\n // the compile-time default if MCP was started with LLUI_MCP_PORT).\n server.middlewares.use('/__llui_mcp_status', (_req, res) => {\n const marker = readMcpMarker()\n if (marker === null) {\n res.statusCode = 404\n res.end()\n return\n }\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ port: marker.port }))\n })\n\n // Watch the marker file for create/delete. fs.watch on the parent\n // directory catches both events; the file itself may not exist\n // when we start watching.\n const dir = dirname(activeFilePath)\n try {\n // Watch the parent directory for the marker file appearing/disappearing\n const watchDir = (): void => {\n if (!existsSync(dir)) return\n dirWatcher = fsWatch(dir, (_event, filename) => {\n if (filename !== 'active.json') return\n if (existsSync(activeFilePath)) {\n // Stamp BEFORE notifying so the `llui:mcp-ready` payload\n // carries the cached devUrl. This is the MCP-after-Vite\n // path: listening already fired and cached the URL; the\n // marker is only now appearing.\n stampDevUrl()\n notifyMcpReady(server)\n } else {\n notifyMcpOffline(server)\n }\n })\n }\n if (existsSync(dir)) {\n watchDir()\n } else {\n // Parent directory doesn't exist yet — poll for it briefly\n const poll = setInterval(() => {\n if (existsSync(dir)) {\n clearInterval(poll)\n watchDir()\n }\n }, 1000)\n // Clean up the poller if vite shuts down before MCP starts\n server.httpServer?.on('close', () => clearInterval(poll))\n }\n } catch {\n // fs.watch can fail on some filesystems — degrade silently\n }\n\n // Re-send the ready event when a new HMR client connects, in case\n // the page loads while MCP is already running.\n server.ws.on('connection', () => {\n if (existsSync(activeFilePath)) notifyMcpReady(server)\n })\n\n server.httpServer?.on('close', () => {\n mcpWatcher?.close()\n dirWatcher?.close()\n mcpWatcher = null\n dirWatcher = null\n })\n\n // Once Vite's HTTP server is listening, cache our dev URL and stamp\n // it into the marker file. Two orderings are possible:\n // (a) MCP started FIRST → marker exists now → stampDevUrl() writes\n // it, and we broadcast llui:mcp-ready so the browser picks up\n // the devUrl without relying on an incidental fs.watch tick\n // (which can miss on NFS/SMB).\n // (b) MCP will start LATER → marker doesn't exist yet → stamp is a\n // no-op. When MCP eventually writes the marker, the directory\n // watcher fires, calls stampDevUrl(), and notifies.\n server.httpServer?.once('listening', () => {\n const address = server.httpServer?.address()\n if (!address || typeof address !== 'object') return\n const host =\n address.address === '::' || address.address === '0.0.0.0' ? 'localhost' : address.address\n cachedDevUrl = `http://${host}:${address.port}`\n stampDevUrl()\n // Broadcast after stamping so the payload carries devUrl. Only\n // fires in case (a) — notifyMcpReady no-ops when the marker is\n // absent.\n notifyMcpReady(server)\n })\n },\n\n transform(code, id) {\n if (!id.endsWith('.ts') && !id.endsWith('.tsx')) return\n\n const diagnostics = diagnose(code)\n if (diagnostics.length > 0) {\n // Prefix every diagnostic with `<file>:<line>:<col>` plus the\n // `[rule-name]` tag so consumers logging `warning.message` in a\n // custom onwarn handler see both the location and the rule they\n // could silence via `disabledWarnings`.\n const cwd = process.cwd()\n const rel = relative(cwd, id)\n const display = rel.startsWith('..') ? id : rel\n for (const d of diagnostics) {\n if (disabledWarnings.has(d.rule)) continue\n const message = `${display}:${d.line}:${d.column}: [${d.rule}] ${d.message}`\n if (failOnWarning) {\n this.error({ message, loc: { line: d.line, column: d.column, file: id } })\n } else {\n this.warn(message, { line: d.line, column: d.column })\n }\n }\n }\n\n const result = transformLlui(code, id, devMode, mcpPort)\n if (!result) return undefined\n\n // Apply per-statement edits via MagicString for accurate source maps.\n // Untouched statements keep their original positions.\n const s = new MagicString(code)\n for (const edit of result.edits) {\n if (edit.start === edit.end) {\n // Insert at position — appendRight for middle, append for end-of-file\n if (edit.start === code.length) s.append(edit.replacement)\n else s.appendRight(edit.start, edit.replacement)\n } else {\n s.overwrite(edit.start, edit.end, edit.replacement)\n }\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ source: id, includeContent: true, hires: true }),\n }\n },\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Plugin, ViteDevServer } from 'vite'\nimport MagicString from 'magic-string'\nimport { existsSync, readFileSync, writeFileSync, watch as fsWatch, type FSWatcher } from 'node:fs'\nimport { dirname, relative, resolve } from 'node:path'\nimport { createRequire } from 'node:module'\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport { transformLlui } from './transform.js'\nimport { diagnose, type DiagnosticRule } from './diagnostics.js'\n\nexport type { DiagnosticRule } from './diagnostics.js'\n\n/**\n * Locate the workspace root so we share the MCP active marker file\n * with @llui/mcp regardless of which subdirectory the dev server runs in.\n * Mirrors `findWorkspaceRoot` from @llui/mcp — duplicated to avoid a\n * vite-plugin → mcp dependency cycle. The contract must stay in sync.\n */\nfunction findWorkspaceRoot(start: string = process.cwd()): string {\n let dir = resolve(start)\n let lastPackageJson: string | null = null\n while (true) {\n if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return dir\n if (existsSync(resolve(dir, '.git'))) return dir\n if (existsSync(resolve(dir, 'package.json'))) lastPackageJson = dir\n const parent = dirname(dir)\n if (parent === dir) return lastPackageJson ?? start\n dir = parent\n }\n}\n\nexport interface LluiPluginOptions {\n /**\n * Port for the MCP debug bridge. In dev mode, the runtime relay connects\n * to `ws://127.0.0.1:<port>` so an external `llui-mcp` server can forward\n * tool calls into the running app.\n *\n * When omitted, the plugin checks whether `@llui/mcp` is resolvable from\n * the Vite project root. If yes → defaults to `5200`. If no → stays\n * disabled. This means installing `@llui/mcp` (+ starting its server)\n * Just Works without an explicit config entry. Pass an explicit `false`\n * to opt out even when `@llui/mcp` is installed; pass a number to use\n * a non-default port. When enabled but the MCP server isn't running,\n * the plugin returns 404 from its discovery endpoint and the browser\n * silently skips the connection — no retry noise.\n */\n mcpPort?: number | false\n\n /**\n * Treat every compiler diagnostic as a build error.\n *\n * Default `false` — diagnostics are emitted via rollup's `this.warn` and\n * can be ignored. Set to `true` in CI so lint-style regressions (namespace\n * imports, bitmask overflow, spread-in-children, `.map()` on state, etc.)\n * fail the build without requiring a custom `build.rollupOptions.onwarn`\n * handler.\n */\n failOnWarning?: boolean\n\n /**\n * Silence specific diagnostic rules without disabling the whole lint\n * pass. Each message is tagged with a rule name (shown in brackets at\n * the start of every warning, e.g. `[spread-in-children]`). Listing\n * a rule here drops all diagnostics with that tag before rollup sees\n * them — so they don't fire via `this.warn` and don't fail the build\n * even when `failOnWarning` is enabled.\n *\n * The valid rule names are enumerated by the `DiagnosticRule` type\n * re-exported from this module. Unknown rule names are ignored.\n */\n disabledWarnings?: readonly DiagnosticRule[]\n\n /**\n * Emit `[llui]`-prefixed `console.info` logs for every transformed\n * component file — state-path bit assignments, mask injections, and\n * helper compile/bail counts. Useful when diagnosing why a binding\n * isn't gated the way you expect, or why a call fell back from\n * template-clone to `elSplit`. Off by default.\n */\n verbose?: boolean\n}\n\n/**\n * Does `@llui/mcp` resolve from `root`'s node_modules? Uses\n * `require.resolve` so monorepo workspaces and hoisted installs both\n * work. Catches failures silently — the only consequence is that we\n * leave `mcpPort` disabled, which is the safe default.\n */\nfunction hasMcpPackage(root: string): boolean {\n try {\n const req = createRequire(resolve(root, 'package.json'))\n req.resolve('@llui/mcp/package.json')\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Resolve the path to the llui-mcp CLI entry. Reads `bin.llui-mcp`\n * from @llui/mcp's package.json and joins it against the package\n * directory. Returns null if @llui/mcp isn't resolvable.\n */\nfunction resolveMcpCliPath(root: string): string | null {\n try {\n const req = createRequire(resolve(root, 'package.json'))\n const pkgJsonPath = req.resolve('@llui/mcp/package.json')\n const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as {\n bin?: string | Record<string, string>\n }\n const binEntry = typeof pkgJson.bin === 'string' ? pkgJson.bin : pkgJson.bin?.['llui-mcp']\n if (!binEntry) return null\n return resolve(dirname(pkgJsonPath), binEntry)\n } catch {\n return null\n }\n}\n\nexport default function llui(options: LluiPluginOptions = {}): Plugin {\n let devMode = false\n // `mcpPort` + `mcpMode` are resolved lazily in `configResolved` so we\n // can check for @llui/mcp in the consuming project's node_modules.\n // - `options.mcpPort === false` → disabled\n // - explicit number → wire-only (user manages the server)\n // - undefined + @llui/mcp found → spawn (plugin starts llui-mcp --http)\n // - undefined + no @llui/mcp → disabled\n let mcpPort: number | null = null\n let mcpMode: 'disabled' | 'wire' | 'spawn' = 'disabled'\n let mcpCliPath: string | null = null\n let mcpChild: ChildProcess | null = null\n const failOnWarning = options.failOnWarning === true\n const disabledWarnings = new Set<string>(options.disabledWarnings ?? [])\n const verbose = options.verbose === true\n\n // File-based handshake with @llui/mcp. The MCP server writes a marker\n // file when its bridge starts; we watch it and send a Vite HMR custom\n // event so the browser can call __lluiConnect() automatically — without\n // retry spam, regardless of whether MCP or Vite started first.\n const activeFilePath = resolve(findWorkspaceRoot(), 'node_modules/.cache/llui-mcp/active.json')\n let mcpWatcher: FSWatcher | null = null\n let dirWatcher: FSWatcher | null = null\n // Cached once Vite's HTTP server emits `listening`. `stampDevUrl()`\n // uses this to write the URL into the marker file — either immediately\n // (if MCP already started and wrote one) or later when the marker\n // appears via the directory watcher (MCP-starts-after-Vite path).\n let cachedDevUrl: string | null = null\n\n function readMcpMarker(): { port: number; devUrl?: string } | null {\n try {\n if (!existsSync(activeFilePath)) return null\n const data = JSON.parse(readFileSync(activeFilePath, 'utf8')) as {\n port?: number\n devUrl?: string\n }\n if (typeof data.port !== 'number') return null\n return { port: data.port, ...(data.devUrl ? { devUrl: data.devUrl } : {}) }\n } catch {\n return null\n }\n }\n\n /**\n * Idempotently write `cachedDevUrl` into the marker file. No-op if the\n * URL hasn't been captured yet (Vite hasn't emitted `listening`) or if\n * the marker file doesn't exist (MCP hasn't started yet). Covers both\n * orderings — the listening hook calls this after caching, and the\n * directory watcher calls it when the marker appears later.\n */\n function stampDevUrl(): void {\n if (cachedDevUrl === null) return\n if (!existsSync(activeFilePath)) return\n try {\n const marker = JSON.parse(readFileSync(activeFilePath, 'utf8')) as Record<string, unknown>\n if (marker.devUrl === cachedDevUrl) return\n marker.devUrl = cachedDevUrl\n writeFileSync(activeFilePath, JSON.stringify(marker))\n } catch {\n // Best-effort — failure to update the marker should not crash Vite\n }\n }\n\n function notifyMcpReady(server: ViteDevServer): void {\n const marker = readMcpMarker()\n if (marker === null) return\n server.ws.send({ type: 'custom', event: 'llui:mcp-ready', data: marker })\n }\n\n function notifyMcpOffline(server: ViteDevServer): void {\n server.ws.send({ type: 'custom', event: 'llui:mcp-offline', data: {} })\n }\n\n return {\n name: 'llui',\n enforce: 'pre',\n\n configResolved(config) {\n devMode = config.command === 'serve' || config.mode === 'development'\n if (options.mcpPort === false) {\n mcpMode = 'disabled'\n mcpPort = null\n } else if (typeof options.mcpPort === 'number') {\n mcpMode = 'wire'\n mcpPort = options.mcpPort\n } else if (hasMcpPackage(config.root)) {\n mcpCliPath = resolveMcpCliPath(config.root)\n if (mcpCliPath) {\n mcpMode = 'spawn'\n mcpPort = 5200\n } else {\n mcpMode = 'wire'\n mcpPort = 5200\n }\n } else {\n mcpMode = 'disabled'\n mcpPort = null\n }\n },\n\n configureServer(server) {\n if (mcpPort === null) {\n // #3 diagnostic: MCP server is running but the plugin is opted\n // out. Users in this state usually don't realize the mismatch —\n // loud-and-early log saves the \"why isn't my MCP attached\" hunt.\n if (existsSync(activeFilePath)) {\n console.warn(\n `[llui] @llui/mcp server is running (marker at ${activeFilePath}) ` +\n `but the Vite plugin is opted out (mcpPort: false, or @llui/mcp ` +\n `isn't a dep of this project). Add \\`llui({ mcpPort: 5200 })\\` ` +\n `to vite.config to wire them up, or remove the marker file and ` +\n `stop the MCP server if the mismatch was unintended.`,\n )\n }\n return\n }\n\n // Spawn mode: plugin launches llui-mcp as a child process so\n // `pnpm dev` handles the whole stack. Skip spawning when a marker\n // already exists — something (usually a separate llui-mcp process\n // started before Vite) is already listening. The existing wire\n // behavior takes over from there.\n if (mcpMode === 'spawn' && mcpCliPath !== null && !existsSync(activeFilePath)) {\n mcpChild = spawn(process.execPath, [mcpCliPath, '--http', String(mcpPort)], {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: { ...process.env, LLUI_MCP_PORT: String(mcpPort) },\n })\n mcpChild.stdout?.on('data', (buf: Buffer) => {\n process.stdout.write(`[mcp] ${buf.toString()}`)\n })\n mcpChild.stderr?.on('data', (buf: Buffer) => {\n process.stderr.write(`[mcp] ${buf.toString()}`)\n })\n mcpChild.on('exit', (code) => {\n if (code !== 0 && code !== null) {\n console.warn(`[llui] @llui/mcp child exited with code ${code}`)\n }\n mcpChild = null\n })\n const killChild = (): void => {\n if (mcpChild && !mcpChild.killed) mcpChild.kill('SIGTERM')\n }\n server.httpServer?.on('close', killChild)\n process.once('exit', killChild)\n }\n\n // HTTP endpoint: the browser fetches this on load to discover the\n // current MCP port. Avoids the race where HMR events sent before\n // the import.meta.hot listener registers get dropped — and lets\n // the browser connect to the actual port (which may differ from\n // the compile-time default if MCP was started with LLUI_MCP_PORT).\n server.middlewares.use('/__llui_mcp_status', (_req, res) => {\n const marker = readMcpMarker()\n if (marker === null) {\n res.statusCode = 404\n res.end()\n return\n }\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ port: marker.port }))\n })\n\n // Watch the marker file for create/delete. fs.watch on the parent\n // directory catches both events; the file itself may not exist\n // when we start watching.\n const dir = dirname(activeFilePath)\n try {\n // Watch the parent directory for the marker file appearing/disappearing\n const watchDir = (): void => {\n if (!existsSync(dir)) return\n dirWatcher = fsWatch(dir, (_event, filename) => {\n if (filename !== 'active.json') return\n if (existsSync(activeFilePath)) {\n // Stamp BEFORE notifying so the `llui:mcp-ready` payload\n // carries the cached devUrl. This is the MCP-after-Vite\n // path: listening already fired and cached the URL; the\n // marker is only now appearing.\n stampDevUrl()\n notifyMcpReady(server)\n } else {\n notifyMcpOffline(server)\n }\n })\n }\n if (existsSync(dir)) {\n watchDir()\n } else {\n // Parent directory doesn't exist yet — poll for it briefly\n const poll = setInterval(() => {\n if (existsSync(dir)) {\n clearInterval(poll)\n watchDir()\n }\n }, 1000)\n // Clean up the poller if vite shuts down before MCP starts\n server.httpServer?.on('close', () => clearInterval(poll))\n }\n } catch {\n // fs.watch can fail on some filesystems — degrade silently\n }\n\n // Re-send the ready event when a new HMR client connects, in case\n // the page loads while MCP is already running.\n server.ws.on('connection', () => {\n if (existsSync(activeFilePath)) notifyMcpReady(server)\n })\n\n server.httpServer?.on('close', () => {\n mcpWatcher?.close()\n dirWatcher?.close()\n mcpWatcher = null\n dirWatcher = null\n })\n\n // Once Vite's HTTP server is listening, cache our dev URL and stamp\n // it into the marker file. Two orderings are possible:\n // (a) MCP started FIRST → marker exists now → stampDevUrl() writes\n // it, and we broadcast llui:mcp-ready so the browser picks up\n // the devUrl without relying on an incidental fs.watch tick\n // (which can miss on NFS/SMB).\n // (b) MCP will start LATER → marker doesn't exist yet → stamp is a\n // no-op. When MCP eventually writes the marker, the directory\n // watcher fires, calls stampDevUrl(), and notifies.\n server.httpServer?.once('listening', () => {\n const address = server.httpServer?.address()\n if (!address || typeof address !== 'object') return\n const host =\n address.address === '::' || address.address === '0.0.0.0' ? 'localhost' : address.address\n cachedDevUrl = `http://${host}:${address.port}`\n stampDevUrl()\n // Broadcast after stamping so the payload carries devUrl. Only\n // fires in case (a) — notifyMcpReady no-ops when the marker is\n // absent.\n notifyMcpReady(server)\n })\n },\n\n transform(code, id) {\n if (!id.endsWith('.ts') && !id.endsWith('.tsx')) return\n\n const diagnostics = diagnose(code)\n if (diagnostics.length > 0) {\n // Prefix every diagnostic with `<file>:<line>:<col>` plus the\n // `[rule-name]` tag so consumers logging `warning.message` in a\n // custom onwarn handler see both the location and the rule they\n // could silence via `disabledWarnings`.\n const cwd = process.cwd()\n const rel = relative(cwd, id)\n const display = rel.startsWith('..') ? id : rel\n for (const d of diagnostics) {\n if (disabledWarnings.has(d.rule)) continue\n const message = `${display}:${d.line}:${d.column}: [${d.rule}] ${d.message}`\n if (failOnWarning) {\n this.error({ message, loc: { line: d.line, column: d.column, file: id } })\n } else {\n this.warn(message, { line: d.line, column: d.column })\n }\n }\n }\n\n const result = transformLlui(code, id, devMode, mcpPort, verbose)\n if (!result) return undefined\n\n // Apply per-statement edits via MagicString for accurate source maps.\n // Untouched statements keep their original positions.\n const s = new MagicString(code)\n for (const edit of result.edits) {\n if (edit.start === edit.end) {\n // Insert at position — appendRight for middle, append for end-of-file\n if (edit.start === code.length) s.append(edit.replacement)\n else s.appendRight(edit.start, edit.replacement)\n } else {\n s.overwrite(edit.start, edit.end, edit.replacement)\n }\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ source: id, includeContent: true, hires: true }),\n }\n },\n }\n}\n"]}
@@ -7,7 +7,7 @@ export interface TransformEdit {
7
7
  end: number;
8
8
  replacement: string;
9
9
  }
10
- export declare function transformLlui(source: string, _filename: string, devMode?: boolean, mcpPort?: number | null): {
10
+ export declare function transformLlui(source: string, _filename: string, devMode?: boolean, mcpPort?: number | null, verbose?: boolean): {
11
11
  output: string;
12
12
  edits: TransformEdit[];
13
13
  } | null;
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AA6KA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,UAAQ,EACf,OAAO,GAAE,MAAM,GAAG,IAAW,GAC5B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAyPnD"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AA6KA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,UAAQ,EACf,OAAO,GAAE,MAAM,GAAG,IAAW,EAC7B,OAAO,UAAQ,GACd;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAmQnD"}
package/dist/transform.js CHANGED
@@ -172,7 +172,7 @@ function resolveKey(key, kind) {
172
172
  return 'class';
173
173
  return key;
174
174
  }
175
- export function transformLlui(source, _filename, devMode = false, mcpPort = 5200) {
175
+ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200, verbose = false) {
176
176
  const sourceFile = ts.createSourceFile('input.ts', source, ts.ScriptTarget.Latest, true);
177
177
  // Find the @llui/dom import
178
178
  const imp = findLluiImport(sourceFile);
@@ -189,6 +189,13 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
189
189
  // won't match. Files without component() get FULL_MASK on all bindings.
190
190
  const fileHasComponent = hasComponentDef(sourceFile, lluiImport);
191
191
  const fieldBits = fileHasComponent ? collectDeps(source) : new Map();
192
+ if (verbose && fileHasComponent) {
193
+ const pairs = [...fieldBits.entries()]
194
+ .map(([path, bit]) => `${path}=${bit === -1 ? 'FULL' : bit}`)
195
+ .join(', ');
196
+ console.info(`[llui] ${_filename}: ${fieldBits.size} reactive path${fieldBits.size === 1 ? '' : 's'}` +
197
+ (pairs.length > 0 ? ` — ${pairs}` : ''));
198
+ }
192
199
  // Identifier names bound to the View<S,M> helpers parameter of a `view` callback.
193
200
  // When the user writes `h.text(...)` / `h.show(...)` / `h.each(...)`, the
194
201
  // compiler treats the call as if it were a bare import call.