@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 +1 -1
- package/dist/inspector-transform.d.ts +0 -24
- package/dist/inspector-transform.js +2 -82
- package/dist/protocol/ids.js +0 -2
- package/dist/protocol/paths.js +0 -1
- package/dist/runtime/inspector/target-inspection.js +0 -2
- package/dist/runtime/shared/clipboard.js +0 -1
- package/dist/runtime/task/annotate-task.js +0 -1
- package/dist/runtime/windows/AnnotateWindowFrame.js +1 -2
- package/dist/ui-annotate-plugin.js +0 -2
- package/package.json +2 -2
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",
|
|
25
|
-
apply: "serve",
|
|
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("&", "&").replaceAll("\"", """);
|
|
218
138
|
}
|
package/dist/protocol/ids.js
CHANGED
|
@@ -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);
|
package/dist/protocol/paths.js
CHANGED
|
@@ -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) {
|
|
@@ -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"
|
|
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.
|
|
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"
|