@ui-annotate/react-vite 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -79,7 +79,7 @@ uiAnnotate({
79
79
 
80
80
  ## Runtime Boundaries
81
81
 
82
- - Supported host apps: React 18 or 19 with Vite 8.
82
+ - Supported host apps: React 18 or 19 with Vite 7 or 8.
83
83
  - The runtime is development-only and is injected by the Vite dev server.
84
84
  - The runtime creates `#__ui_annotate_root__` under `document.body`; your app does not need to provide a mount point.
85
85
  - The UI is isolated in a shadow root.
@@ -30,30 +30,6 @@ type ViteDevServerLike = {
30
30
  use(handler: (request: IncomingMessage, response: ServerResponse, next: ConnectNext) => void): void;
31
31
  };
32
32
  };
33
- /**
34
- * 创建 Vite 插件,核心职责有两个:
35
- *
36
- * 1. 拦截 UI Annotate runtime 的 /__ui-annotate/* 请求
37
- * - UI Annotate runtime 通过这些 API 读写 ui.annotate.json 并打开源码位置
38
- *
39
- * 2. 在 transform 阶段为项目内的每个 JSX 元素注入 data-ui-annotate-inspector 属性
40
- * - 这样 UI Annotate runtime 就能通过 DOM 属性拿到源码定位
41
- * - 注入只在内存 bundle 中进行,不修改磁盘上的源文件
42
- * - 只在 dev server (apply: "serve") 中生效,生产构建完全不执行
43
- */
44
33
  export declare function createUiAnnotateInspectorPlugin(options?: UiAnnotateInspectorPluginOptions): VitePluginLike;
45
- /**
46
- * 核心转换逻辑:用 TypeScript AST 遍历源码中的所有 JSX 元素,
47
- * 为每个元素计算一个形如 "src/components/Button.tsx:12:5" 的定位字符串,
48
- * 然后作为 data-ui-annotate-inspector 属性注入到元素的标签上。
49
- *
50
- * 整体流程:
51
- * 1. 判断文件是否属于项目可写源码(排除 node_modules 等)
52
- * 2. 解析为 TypeScript AST
53
- * 3. 递归访问所有 JSX 元素,收集需要插入属性的位置
54
- * 4. 从后往前执行字符串插入,生成最终的代码
55
- *
56
- * 注意:这里只修改 Vite 内存中的 bundle 输出,不会写入磁盘文件。
57
- */
58
34
  export declare function transformUiAnnotateInspectorSource(code: string, id: string, options?: UiAnnotateInspectorTransformOptions): UiAnnotateInspectorTransformResult | null;
59
35
  export {};
@@ -2,33 +2,17 @@ import path from "node:path";
2
2
  import ts from "typescript";
3
3
  import { isWritableProjectSource, normalizeProjectRelativePath } from "./protocol/index.js";
4
4
  import { handleUiAnnotateRequest } from "./task-api.js";
5
- // 注入到每个 JSX 元素上的 HTML 属性名,值为 "相对路径:行号:列号" 格式的源码定位信息。
6
- // UI Annotate runtime 在用户 hover/click 时读取这个属性,就能知道当前元素对应哪一行源码。
7
- // 这个属性只在 dev server 的内存 bundle 中存在,不会写入真实文件,也不会进入生产构建。
8
5
  export const UI_ANNOTATE_INSPECTOR_ATTRIBUTE = "data-ui-annotate-inspector";
9
- /**
10
- * 创建 Vite 插件,核心职责有两个:
11
- *
12
- * 1. 拦截 UI Annotate runtime 的 /__ui-annotate/* 请求
13
- * - UI Annotate runtime 通过这些 API 读写 ui.annotate.json 并打开源码位置
14
- *
15
- * 2. 在 transform 阶段为项目内的每个 JSX 元素注入 data-ui-annotate-inspector 属性
16
- * - 这样 UI Annotate runtime 就能通过 DOM 属性拿到源码定位
17
- * - 注入只在内存 bundle 中进行,不修改磁盘上的源文件
18
- * - 只在 dev server (apply: "serve") 中生效,生产构建完全不执行
19
- */
20
6
  export function createUiAnnotateInspectorPlugin(options = {}) {
21
7
  let root = path.resolve(options.root ?? process.cwd());
22
8
  return {
23
9
  name: "ui-annotate-inspector-transform",
24
- enforce: "pre", // 在其他插件(如 @vitejs/plugin-react)之前执行,确保注入的是原始源码位置
25
- apply: "serve", // 只在 dev server 生效,生产构建不加载这个插件
10
+ enforce: "pre",
11
+ apply: "serve",
26
12
  configResolved(config) {
27
- // 优先使用插件选项中的 root,其次使用 Vite 解析出的项目根目录
28
13
  root = path.resolve(options.root ?? config.root);
29
14
  },
30
15
  configureServer(server) {
31
- // 挂载中间件:拦截所有 /__ui-annotate/* 路径的 UI Annotate runtime 请求。
32
16
  server.middlewares.use(async (request, response, next) => {
33
17
  const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
34
18
  if (!pathname.startsWith("/__ui-annotate/")) {
@@ -54,19 +38,6 @@ export function createUiAnnotateInspectorPlugin(options = {}) {
54
38
  }
55
39
  };
56
40
  }
57
- /**
58
- * 核心转换逻辑:用 TypeScript AST 遍历源码中的所有 JSX 元素,
59
- * 为每个元素计算一个形如 "src/components/Button.tsx:12:5" 的定位字符串,
60
- * 然后作为 data-ui-annotate-inspector 属性注入到元素的标签上。
61
- *
62
- * 整体流程:
63
- * 1. 判断文件是否属于项目可写源码(排除 node_modules 等)
64
- * 2. 解析为 TypeScript AST
65
- * 3. 递归访问所有 JSX 元素,收集需要插入属性的位置
66
- * 4. 从后往前执行字符串插入,生成最终的代码
67
- *
68
- * 注意:这里只修改 Vite 内存中的 bundle 输出,不会写入磁盘文件。
69
- */
70
41
  export function transformUiAnnotateInspectorSource(code, id, options = {}) {
71
42
  if (options.enabled === false) {
72
43
  return null;
@@ -76,11 +47,8 @@ export function transformUiAnnotateInspectorSource(code, id, options = {}) {
76
47
  return null;
77
48
  }
78
49
  const inspectorRelativePath = relativePath;
79
- // 用 TypeScript 编译器解析源码为 AST,开启 parent 节点引用以便后续遍历
80
50
  const sourceFile = ts.createSourceFile(stripViteQuery(id), code, ts.ScriptTarget.Latest, true, getScriptKind(id));
81
- // 收集所有需要插入 inspector 属性的位置
82
51
  const insertions = [];
83
- // 递归遍历 AST,找到所有 JSX 开标签和自闭合标签
84
52
  function visit(node) {
85
53
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
86
54
  collectInspectorInsertion(sourceFile, code, inspectorRelativePath, node, insertions);
@@ -96,16 +64,6 @@ export function transformUiAnnotateInspectorSource(code, id, options = {}) {
96
64
  map: null
97
65
  };
98
66
  }
99
- /**
100
- * 为单个 JSX 元素收集 inspector 属性的插入计划。
101
- *
102
- * 跳过两种情况:
103
- * - Fragment 标签(它不产生真实 DOM,注入属性没有意义)
104
- * - 已经有 data-ui-annotate-inspector 的标签(避免重复注入)
105
- *
106
- * 对于其他标签,计算它在源码中的 "文件路径:行号:列号" 定位,
107
- * 然后记录一个 Insertion,表示在标签的合适位置插入属性文本。
108
- */
109
67
  function collectInspectorInsertion(sourceFile, code, relativePath, node, insertions) {
110
68
  if (isFragmentTag(sourceFile, node.tagName)) {
111
69
  return;
@@ -117,8 +75,6 @@ function collectInspectorInsertion(sourceFile, code, relativePath, node, inserti
117
75
  if (insertAt === null) {
118
76
  return;
119
77
  }
120
- // 将 AST 节点的起始位置转换为 "行号:列号" 格式(均为 1-based)
121
- // 最终生成形如 "src/components/Button.tsx:12:5" 的定位字符串
122
78
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
123
79
  const inspector = `${relativePath}:${line + 1}:${character + 1}`;
124
80
  insertions.push({
@@ -126,50 +82,26 @@ function collectInspectorInsertion(sourceFile, code, relativePath, node, inserti
126
82
  text: ` ${UI_ANNOTATE_INSPECTOR_ATTRIBUTE}="${escapeJsxAttribute(inspector)}"`
127
83
  });
128
84
  }
129
- /**
130
- * 计算在标签的哪个位置插入新属性最合适。
131
- *
132
- * 对于自闭合标签 <Foo />:在 "/>" 之前插入
133
- * 例如 <Foo bar /> → <Foo bar data-ui-annotate-inspector="..." />
134
- *
135
- * 对于开标签 <Foo>:在 ">" 之前插入
136
- * 例如 <Foo bar> → <Foo bar data-ui-annotate-inspector="...">
137
- *
138
- * 如果 "/" 或 ">" 不在标签范围内,返回 null 表示无法安全插入。
139
- */
140
85
  function getAttributeInsertionIndex(code, node) {
141
86
  if (ts.isJsxSelfClosingElement(node)) {
142
- // 自闭合标签:从标签末尾往前找 "/>",属性插在它前面
143
87
  const closeIndex = code.lastIndexOf("/>", node.end);
144
88
  if (closeIndex < node.getStart()) {
145
89
  return null;
146
90
  }
147
- // 如果 "/>" 前面已经有空格(如 <Foo bar />),直接插在 "/" 前面;
148
- // 如果没有空格(如 <Foo/>),也插在 "/" 前面(Insertion text 自带前导空格)。
149
91
  return /\s/.test(code[closeIndex - 1] ?? "") ? closeIndex - 1 : closeIndex;
150
92
  }
151
- // 开标签(非自闭合):从标签末尾往前找 ">",属性插在它前面
152
93
  const closeIndex = code.lastIndexOf(">", node.end);
153
94
  return closeIndex >= node.getStart() ? closeIndex : null;
154
95
  }
155
- // 检查 JSX 元素是否已经有指定名称的属性,用于避免重复注入
156
96
  function hasAttribute(sourceFile, attributes, name) {
157
97
  return attributes.properties.some((property) => {
158
98
  return ts.isJsxAttribute(property) && property.name.getText(sourceFile) === name;
159
99
  });
160
100
  }
161
- // Fragment 不产生真实 DOM 节点,注入属性没有意义,需要跳过
162
101
  function isFragmentTag(sourceFile, tagName) {
163
102
  const text = tagName.getText(sourceFile);
164
103
  return text === "Fragment" || text === "React.Fragment";
165
104
  }
166
- /**
167
- * 将收集到的所有 Insertion 按位置从后往前依次应用到源码上。
168
- *
169
- * 为什么要从后往前?
170
- * 因为每次插入都会让后面的字符位置向后偏移。从后往前插入,
171
- * 前面已记录的位置不会受到影响,避免了重新计算偏移量的问题。
172
- */
173
105
  function applyInsertions(code, insertions) {
174
106
  let nextCode = code;
175
107
  for (const insertion of [...insertions].sort((a, b) => b.index - a.index)) {
@@ -177,14 +109,6 @@ function applyInsertions(code, insertions) {
177
109
  }
178
110
  return nextCode;
179
111
  }
180
- /**
181
- * 判断一个文件是否需要注入 inspector 信息。
182
- *
183
- * 只对"项目内可写源码"注入,排除 node_modules、dist、generated 等不可写路径。
184
- * 这样做的原因:
185
- * - 第三方库的 DOM 元素没有项目源码可以定位,注入了也没用
186
- * - UI Annotate runtime 需要根据这个定位信息找到对应的源码
187
- */
188
112
  function resolveInspectableRelativePath(id, options) {
189
113
  try {
190
114
  const relativePath = options.projectRelativePath ?? toProjectRelativePath(stripViteQuery(id), options.root);
@@ -195,24 +119,20 @@ function resolveInspectableRelativePath(id, options) {
195
119
  return null;
196
120
  }
197
121
  }
198
- // 将绝对路径转为相对于项目根目录的路径,已经是相对路径则直接返回
199
122
  function toProjectRelativePath(id, root = process.cwd()) {
200
123
  if (!path.isAbsolute(id)) {
201
124
  return id;
202
125
  }
203
126
  return path.relative(path.resolve(root), id);
204
127
  }
205
- // 去掉 Vite 在模块 ID 后面追加的查询参数,例如 "/src/App.tsx?vue&type=script" → "/src/App.tsx"
206
128
  function stripViteQuery(id) {
207
129
  const queryIndex = id.indexOf("?");
208
130
  return queryIndex === -1 ? id : id.slice(0, queryIndex);
209
131
  }
210
- // 根据文件扩展名判断 TypeScript 解析模式(.jsx 用 JSX 模式,其余默认用 TSX 模式)
211
132
  function getScriptKind(id) {
212
133
  const cleanId = stripViteQuery(id);
213
134
  return cleanId.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.TSX;
214
135
  }
215
- // 转义 JSX 属性值中的特殊字符,防止注入的属性值破坏 JSX 语法
216
136
  function escapeJsxAttribute(value) {
217
137
  return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
218
138
  }
@@ -1,11 +1,9 @@
1
- // 辅助逻辑:解析以 # 开头的目标 ID,例如 "#1" -> 1。
2
1
  export function parseTargetIndex(id) {
3
2
  if (!/^#[1-9]\d*$/.test(id)) {
4
3
  return null;
5
4
  }
6
5
  return Number(id.slice(1));
7
6
  }
8
- // 交互执行流程:当用户圈选一个新目标时,遍历现有 target 集合,提取最大序号并 +1。
9
7
  export function getNextTargetId(targets) {
10
8
  const maxIndex = targets.reduce((max, target) => {
11
9
  const index = parseTargetIndex(target.id);
@@ -1,6 +1,5 @@
1
1
  import path from "node:path";
2
2
  import { BLOCKED_WRITE_PATH_SEGMENTS } from "./constants.js";
3
- // 关键算法逻辑:将任意路径标准化为基于项目根目录的相对路径,并阻止跨目录逃逸。
4
3
  export function normalizeProjectRelativePath(input) {
5
4
  if (path.isAbsolute(input)) {
6
5
  throw new Error(`Expected a project-relative path, received absolute path: ${input}`);
@@ -41,8 +41,6 @@ export function findElementForTraceNode(previewRoot, target, node, annotateFile,
41
41
  if (!location) {
42
42
  return null;
43
43
  }
44
- // First locate the selected target as context, so matching names elsewhere
45
- // in the Annotate UI do not steal the trace hover highlight.
46
44
  const targetElement = findElementForTarget(previewRoot, target, annotateFile, showNativeTraceNodes) ??
47
45
  findElementForTarget(previewRoot, target, annotateFile, !showNativeTraceNodes);
48
46
  if (targetElement) {
@@ -5,7 +5,6 @@ export async function writeClipboardText(text) {
5
5
  return true;
6
6
  }
7
7
  catch {
8
- // Fall through to the textarea fallback for older or restricted contexts.
9
8
  }
10
9
  }
11
10
  return writeClipboardTextWithTextarea(text);
@@ -79,7 +79,6 @@ async function readErrorMessage(response) {
79
79
  }
80
80
  }
81
81
  catch {
82
- // JSON parse failed; fall back to the generic status message.
83
82
  }
84
83
  return `UI Annotate task request failed with ${response.status}.`;
85
84
  }
@@ -49,8 +49,7 @@ export function AnnotateWindowFrame({ children, floating = true, icon: Icon, id,
49
49
  width: state.width,
50
50
  height: state.height
51
51
  };
52
- return (_jsxs("section", { className: cn("pointer-events-auto flex flex-col overflow-hidden rounded-xl border bg-card/95 text-card-foreground shadow-xl backdrop-blur", floating ? "fixed z-20" : "relative max-w-full"), style: style, "aria-label": label, children: [_jsxs("header", { className: "flex min-h-9 cursor-move select-none items-center justify-between gap-2 border-b bg-muted/30 py-0.5 pl-2.5 pr-1.5", onPointerDown: (event) => onBeginMove(id, event), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs font-semibold text-foreground", children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "min-w-0 truncate", children: title })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-6" // 覆盖为 size-6(24px),不再被 size="icon" 默认的 size-9(36px)撑开
53
- , "aria-label": `Close ${label}`, onPointerDown: (event) => event.stopPropagation(), onClick: () => onClose(id), children: _jsx(XIcon, { "data-icon": "inline-start", "aria-hidden": "true" }) })] }), _jsx("div", { className: "min-h-0 flex-1 p-3", children: children }), floating
52
+ return (_jsxs("section", { className: cn("pointer-events-auto flex flex-col overflow-hidden rounded-xl border bg-card/95 text-card-foreground shadow-xl backdrop-blur", floating ? "fixed z-20" : "relative max-w-full"), style: style, "aria-label": label, children: [_jsxs("header", { className: "flex min-h-9 cursor-move select-none items-center justify-between gap-2 border-b bg-muted/30 py-0.5 pl-2.5 pr-1.5", onPointerDown: (event) => onBeginMove(id, event), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs font-semibold text-foreground", children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "min-w-0 truncate", children: title })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-6", "aria-label": `Close ${label}`, onPointerDown: (event) => event.stopPropagation(), onClick: () => onClose(id), children: _jsx(XIcon, { "data-icon": "inline-start", "aria-hidden": "true" }) })] }), _jsx("div", { className: "min-h-0 flex-1 p-3", children: children }), floating
54
53
  ? resizeHandles.map((handle) => (_jsx("div", { className: cn("absolute touch-none", handle.className), "aria-hidden": "true", onPointerDown: (event) => onBeginResize(id, handle.direction, event) }, `${handle.direction.horizontal}:${handle.direction.vertical}`)))
55
54
  : null] }));
56
55
  }
@@ -37,8 +37,6 @@ function createUiAnnotateRuntimePlugin() {
37
37
  appendAnnotateTaskWatchIgnores(config);
38
38
  return {
39
39
  resolve: {
40
- // Linked host apps can otherwise load one React from the app and another from
41
- // the embedded UI Annotate runtime, which breaks hooks inside the runtime.
42
40
  dedupe: ["react", "react-dom"],
43
41
  alias: {
44
42
  [UI_ANNOTATE_RUNTIME_STYLE_IMPORT_ID]: runtimeStylePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ui-annotate/react-vite",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Annotate React UI issues in Vite apps and hand them off to coding agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",
42
42
  "react-dom": "^18.2.0 || ^19.0.0",
43
- "vite": "^8.0.13"
43
+ "vite": "^7.0.0 || ^8.0.13"
44
44
  },
45
45
  "devDependencies": {
46
46
  "vite": "^8.0.13"