npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vite-plugin-visual-selector

v0.1.3

Published

面向 React/JSX/TSX 的 Vite 插件,实现"源码同源元素联动高亮"能力。

Downloads

437

Readme

vite-plugin-visual-selector

面向 React/JSX/TSX 的 Vite 插件,实现"源码同源元素联动高亮"能力。

编译时给 DOM 打上源码坐标(data-source-location),运行时根据坐标将同源元素聚合高亮,支持行内编辑和远程 DOM 修改。


一、稳定公开 API

安装

npm install vite-plugin-visual-selector

编译时插件

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualSelectorPlugin } from 'vite-plugin-visual-selector'

export default defineConfig({
  plugins: [react(), visualSelectorPlugin()],
})

VisualSelectorPluginOptions

interface VisualSelectorPluginOptions {
  /** 需要处理的文件匹配规则,默认 [/\.[jt]sx$/] */
  include?: RegExp[]
  /** 需要排除的文件匹配规则,默认 [/node_modules/] */
  exclude?: RegExp[]
  /** 注入属性名,默认 "data-source-location" */
  attributeName?: string
  /** 项目根目录,用于计算相对路径,默认 process.cwd() */
  root?: string
}

运行时 Agent

import { setupVisualEditAgent } from 'vite-plugin-visual-selector/runtime'

const agent = setupVisualEditAgent()

VisualEditAgentOptions

interface VisualEditAgentOptions {
  /** 属性名,默认 "data-source-location" */
  attributeName?: string
  /** postMessage 的目标 origin,默认 "*" */
  targetOrigin?: string
}

VisualEditAgentInstance

setupVisualEditAgent() 返回控制实例:

interface VisualEditAgentInstance {
  /** 销毁 agent,移除所有监听器和 overlay */
  destroy(): void
  /** 开启/关闭编辑模式 */
  enableEditMode(enabled: boolean): void
  /** 手动选中元素 */
  selectElement(element: Element): void
  /** 清除所有选中和高亮 */
  clearSelection(): void
  /** 获取当前选中的 source id */
  getSelectedId(): string | null
}

npm 导出

// 主入口 "vite-plugin-visual-selector"
export { visualSelectorPlugin, setupVisualEditAgent }

// 运行时入口 "vite-plugin-visual-selector/runtime"
export { setupVisualEditAgent }

完整类型导出见 src/index.ts


二、高级集成能力

父编辑器控制

父窗口通过 postMessage 控制 iframe 内的 agent:

// 开启编辑模式
iframe.contentWindow.postMessage(
  { type: 'toggle-visual-edit-mode', data: { enabled: true } },
  '*'
)

// 修改元素 class
iframe.contentWindow.postMessage(
  { type: 'update-classes', data: { visualSelectorId: 'src/App.tsx:12:6', classes: 'text-lg font-bold' } },
  '*'
)

// 修改元素文本
iframe.contentWindow.postMessage(
  { type: 'update-content', data: { visualSelectorId: 'src/App.tsx:12:6', content: '新文本' } },
  '*'
)

// 修改元素属性
iframe.contentWindow.postMessage(
  { type: 'update-attribute', data: { visualSelectorId: 'src/App.tsx:12:6', attribute: 'src', value: '/new-image.png' } },
  '*'
)

// 取消选中
iframe.contentWindow.postMessage({ type: 'unselect-element' }, '*')

// 切换行内编辑
iframe.contentWindow.postMessage(
  { type: 'toggle-inline-edit-mode', data: { dataSourceLocation: 'src/App.tsx:12:6', inlineEditingMode: true } },
  '*'
)

// 修改 CSS 变量
iframe.contentWindow.postMessage(
  { type: 'update-theme-variables', data: { variables: { '--primary': '#e11d48' } } },
  '*'
)

// 注入字体
iframe.contentWindow.postMessage(
  { type: 'inject-font-import', data: { fontUrl: 'https://fonts.googleapis.com/css2?family=Inter' } },
  '*'
)

// 刷新页面
iframe.contentWindow.postMessage({ type: 'refresh-page' }, '*')

// 请求当前选中元素位置
iframe.contentWindow.postMessage({ type: 'request-element-position' }, '*')

监听 iframe 上报消息

window.addEventListener('message', (e) => {
  switch (e.data?.type) {
    case 'visual-edit-agent-ready':
      // agent 初始化完成,可以开始发送指令
      break

    case 'element-selected':
      // 元素被选中
      // e.data: { tagName, classes, visualSelectorId, content?, position, isTextElement, computedStyles, ... }
      break

    case 'element-position-update':
      // 选中元素位置更新(滚动/resize 时)
      // e.data: { position, isInViewport, visualSelectorId }
      break

    case 'inline-edit':
      // 行内编辑内容变更
      // e.data: { elementInfo, originalContent, newContent }
      break

    case 'content-editing-started':
    case 'content-editing-ended':
      // 行内编辑状态变更
      // e.data: { visualSelectorId }
      break

    case 'sandbox:onMounted':
    case 'sandbox:onUnmounted':
      // 页面中带标记元素的挂载/卸载状态
      break
  }
})

旧版兼容

以下消息类型保留向后兼容,建议迁移到新版 API:

| 旧版 | 新版替代 | |------|---------| | visual-selector:set-active | toggle-visual-edit-mode | | update-element-style | update-classes + CSS class | | update-element-text | update-content | | update-element-class | update-classes |

完整消息协议类型

所有消息类型均从主入口导出,可用于父编辑器的 TypeScript 类型检查:

import type {
  // iframe → 主页面
  ElementSelectedMessage,
  ElementPositionUpdateMessage,
  InlineEditMessage,
  ContentEditingMessage,
  AgentReadyMessage,
  SandboxMountMessage,
  // 主页面 → iframe
  ToggleVisualEditModeMessage,
  UpdateClassesMessage,
  UpdateAttributeMessage,
  UpdateContentMessage,
  UpdateThemeVariablesMessage,
  ToggleInlineEditModeMessage,
  InjectFontImportMessage,
  UnselectElementMessage,
  RefreshPageMessage,
  RequestElementPositionMessage,
} from 'vite-plugin-visual-selector'

三、实现原理

整体架构

编译时(Vite transform)         运行时(iframe 内)
┌─────────────────────┐     ┌──────────────────────────────┐
│ .jsx/.tsx 文件        │     │ setupVisualEditAgent()       │
│ ↓ @babel/parser      │     │ ↓ 绑定 click/mousemove 监听   │
│ ↓ @babel/traverse    │     │ ↓ 查找 [data-source-location] │
│ ↓ 只处理小写标签       │     │ ↓ querySelectorAll 同源元素    │
│ ↓ magic-string 注入   │     │ ↓ 创建 overlay 高亮           │
│ ↓ 输出 sourcemap     │     │ ↓ postMessage 上报            │
└─────────────────────┘     └──────────────────────────────┘

编译时

  • Vite transform 钩子(enforce: 'pre'),在 React 插件之前运行
  • @babel/parser 解析 JSX AST,@babel/traverse 遍历 JSXOpeningElement
  • 只处理原生 DOM 元素(小写标签),跳过 React 组件(大写标签)
  • magic-string 在标签名末尾注入 data-source-location="相对路径:行:列"
  • 已有该属性的元素不重复注入
  • 生成高精度 sourcemap

编译前后对比:

// 编译前
<div className="cell">{item.label}</div>

// 编译后
<div data-source-location="src/App.tsx:12:6" className="cell">{item.label}</div>

运行时模块结构

src/runtime/
├── index.ts              # setupVisualEditAgent 组装入口
├── state.ts              # AgentState 状态管理
├── utils.ts              # 纯工具函数(getSourceId, findAllElementsById 等)
├── overlay.ts            # overlay 创建/定位 + 动画冻结
├── messages.ts           # 消息收发协议
├── inline-edit.ts        # 行内编辑
└── layer-navigation.ts   # 层级导航下拉菜单

运行时核心流程

  1. 初始化:绑定 message 监听器,通知父页面 visual-edit-agent-ready
  2. 激活编辑模式:冻结动画、禁用 pointer-events、设置 crosshair 光标、绑定鼠标事件
  3. Hover 高亮elementFromPoint 穿透 overlay 查找真实元素,querySelectorAll 找同源元素,创建半透明 overlay
  4. Click 选中:创建实线 overlay + 标签名 tag + 层级导航入口,postMessage 上报 element-selected
  5. 位置跟踪:scroll/resize 时通过 requestAnimationFrame 节流更新 overlay 位置,上报 element-position-update
  6. DOM 变化检测MutationObserver 监听 style/class/宽高变化,自动重定位 overlay
  7. 行内编辑:将文本元素设为 contentEditable,防抖上报 inline-edit
  8. 远程修改:接收父窗口的 update-classes/update-content/update-attribute 指令,直接操作 DOM

关键实现细节

  • 穿透 overlay 查找元素:临时禁用 freeze-pointer-events 样式表,调用 elementFromPoint,再恢复
  • 冻结动画:注入全局 CSS 规则 animation-play-state: paused + transition: none,并调用 getAnimations().forEach(anim => anim.finish())
  • pointer-events 管理:全局 pointer-events: none,agent 元素通过 data-vite-plugin-element 属性选择器恢复 pointer-events: auto
  • tag 定位:默认在元素上方 27px,上方空间不足时移到下方或内部,水平方向通过 requestAnimationFrame 后修正保证不超出视口

四、限制与副作用

支持范围

  • 只支持 React / JSX / TSX:插件通过 Babel 解析 JSX 语法,不支持 Vue SFC、Svelte 等其他模板语法
  • 只处理原生 DOM 元素:小写标签(divspanbutton 等)会被注入属性,大写标签(React 组件如 <Card />)不处理
  • Vite 5+ 环境:peer 依赖要求 vite >= 5

运行时侵入性

启用编辑模式后,agent 会对页面产生以下副作用:

  1. 冻结所有 CSS 动画和过渡:通过注入全局样式 animation-play-state: paused; transition: none
  2. 接管 pointer-events:全局设置 pointer-events: none,只有 agent 创建的元素可交互
  3. 修改 cursordocument.body.style.cursor = 'crosshair'
  4. 锁定 overflowoverflow-y: scroll; overflow-x: hidden 防止 tag 标签导致的滚动条闪烁
  5. 注入 DOM 元素:overlay、tag 标签、layer dropdown 挂在 document.body
  6. 拦截 click 事件:在捕获阶段阻止事件冒泡(stopImmediatePropagation
  7. 启动 MutationObserver:监听 document.body 的子树变化

关闭编辑模式后(enableEditMode(false)destroy()),以上副作用全部清除。

行内编辑限制

  • 只有叶子节点(无子元素)的文本类标签可行内编辑
  • 包含 img/video/canvas/svg 子元素的不可编辑
  • 空文本元素不可编辑
  • 编辑通过 contentEditable 实现,可能与某些 CSS 样式冲突

消息安全

  • postMessage 默认使用 targetOrigin: '*',生产环境建议设置具体 origin
  • agent 会校验 targetOrigin,只接受匹配 origin 的消息

开发

npm run build        # tsup 构建(ESM + dts + sourcemap)
npm test             # vitest run
npm run test:watch   # vitest watch
npm run typecheck    # tsc --noEmit
npm run demo         # 启动 demo

项目结构

src/
├── index.ts              # 主入口导出
├── plugin.ts             # 编译时 JSX 注入逻辑
├── runtime.ts            # 运行时入口(re-export)
├── runtime/
│   ├── index.ts          # setupVisualEditAgent 组装
│   ├── state.ts          # 状态管理
│   ├── utils.ts          # 工具函数
│   ├── overlay.ts        # overlay + 冻结动画
│   ├── messages.ts       # 消息协议
│   ├── inline-edit.ts    # 行内编辑
│   └── layer-navigation.ts # 层级导航
└── shared/
    ├── constants.ts      # 常量
    └── types.ts          # 公共类型
demo/                     # 示例项目
test/                     # Vitest 测试
docs/                     # 实现文档