@ui-annotate/react-vite 0.1.0

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.
Files changed (100) hide show
  1. package/dist/code-open.d.ts +17 -0
  2. package/dist/code-open.js +82 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +4 -0
  5. package/dist/inspector-transform.d.ts +59 -0
  6. package/dist/inspector-transform.js +218 -0
  7. package/dist/protocol/constants.d.ts +7 -0
  8. package/dist/protocol/constants.js +31 -0
  9. package/dist/protocol/ids.d.ts +3 -0
  10. package/dist/protocol/ids.js +15 -0
  11. package/dist/protocol/index.d.ts +4 -0
  12. package/dist/protocol/index.js +4 -0
  13. package/dist/protocol/paths.d.ts +3 -0
  14. package/dist/protocol/paths.js +36 -0
  15. package/dist/protocol/task-model.d.ts +35 -0
  16. package/dist/protocol/task-model.js +68 -0
  17. package/dist/runtime/app/AnnotateToolbar.d.ts +9 -0
  18. package/dist/runtime/app/AnnotateToolbar.js +14 -0
  19. package/dist/runtime/app/AnnotateWindows.d.ts +28 -0
  20. package/dist/runtime/app/AnnotateWindows.js +8 -0
  21. package/dist/runtime/app/UiAnnotate.d.ts +5 -0
  22. package/dist/runtime/app/UiAnnotate.js +540 -0
  23. package/dist/runtime/index.d.ts +5 -0
  24. package/dist/runtime/index.js +4 -0
  25. package/dist/runtime/inspector/target-inspection.d.ts +10 -0
  26. package/dist/runtime/inspector/target-inspection.js +337 -0
  27. package/dist/runtime/layout/annotate-storage.d.ts +9 -0
  28. package/dist/runtime/layout/annotate-storage.js +134 -0
  29. package/dist/runtime/layout/use-annotate-layout.d.ts +17 -0
  30. package/dist/runtime/layout/use-annotate-layout.js +147 -0
  31. package/dist/runtime/overlay/SelectionOverlay.d.ts +7 -0
  32. package/dist/runtime/overlay/SelectionOverlay.js +95 -0
  33. package/dist/runtime/shared/annotate-constants.d.ts +13 -0
  34. package/dist/runtime/shared/annotate-constants.js +13 -0
  35. package/dist/runtime/shared/annotate-types.d.ts +36 -0
  36. package/dist/runtime/shared/annotate-types.js +1 -0
  37. package/dist/runtime/shared/clipboard.d.ts +1 -0
  38. package/dist/runtime/shared/clipboard.js +33 -0
  39. package/dist/runtime/style.css +206 -0
  40. package/dist/runtime/task/annotate-task.d.ts +16 -0
  41. package/dist/runtime/task/annotate-task.js +85 -0
  42. package/dist/runtime/task/use-annotate-task.d.ts +16 -0
  43. package/dist/runtime/task/use-annotate-task.js +115 -0
  44. package/dist/runtime/windows/AnnotateSettingsWindow.d.ts +6 -0
  45. package/dist/runtime/windows/AnnotateSettingsWindow.js +5 -0
  46. package/dist/runtime/windows/AnnotateWindow.d.ts +21 -0
  47. package/dist/runtime/windows/AnnotateWindow.js +83 -0
  48. package/dist/runtime/windows/AnnotateWindowFrame.d.ts +26 -0
  49. package/dist/runtime/windows/AnnotateWindowFrame.js +56 -0
  50. package/dist/runtime/windows/TargetTraceTree.d.ts +12 -0
  51. package/dist/runtime/windows/TargetTraceTree.js +163 -0
  52. package/dist/runtime/windows/window-shared.d.ts +14 -0
  53. package/dist/runtime/windows/window-shared.js +41 -0
  54. package/dist/task-api.d.ts +15 -0
  55. package/dist/task-api.js +239 -0
  56. package/dist/ui/components/accordion.d.ts +7 -0
  57. package/dist/ui/components/accordion.js +18 -0
  58. package/dist/ui/components/alert-dialog.d.ts +18 -0
  59. package/dist/ui/components/alert-dialog.js +41 -0
  60. package/dist/ui/components/alert.d.ts +9 -0
  61. package/dist/ui/components/alert.js +24 -0
  62. package/dist/ui/components/badge.d.ts +9 -0
  63. package/dist/ui/components/badge.js +24 -0
  64. package/dist/ui/components/breadcrumb.d.ts +11 -0
  65. package/dist/ui/components/breadcrumb.js +27 -0
  66. package/dist/ui/components/button.d.ts +10 -0
  67. package/dist/ui/components/button.js +31 -0
  68. package/dist/ui/components/card.d.ts +9 -0
  69. package/dist/ui/components/card.js +24 -0
  70. package/dist/ui/components/dropdown-menu.d.ts +11 -0
  71. package/dist/ui/components/dropdown-menu.js +21 -0
  72. package/dist/ui/components/input.d.ts +3 -0
  73. package/dist/ui/components/input.js +6 -0
  74. package/dist/ui/components/scroll-area.d.ts +5 -0
  75. package/dist/ui/components/scroll-area.js +12 -0
  76. package/dist/ui/components/separator.d.ts +4 -0
  77. package/dist/ui/components/separator.js +8 -0
  78. package/dist/ui/components/switch.d.ts +6 -0
  79. package/dist/ui/components/switch.js +7 -0
  80. package/dist/ui/components/table.d.ts +10 -0
  81. package/dist/ui/components/table.js +27 -0
  82. package/dist/ui/components/tabs.d.ts +11 -0
  83. package/dist/ui/components/tabs.js +28 -0
  84. package/dist/ui/components/textarea.d.ts +3 -0
  85. package/dist/ui/components/textarea.js +6 -0
  86. package/dist/ui/components/toggle-group.d.ts +9 -0
  87. package/dist/ui/components/toggle-group.js +22 -0
  88. package/dist/ui/components/toggle.d.ts +9 -0
  89. package/dist/ui/components/toggle.js +25 -0
  90. package/dist/ui/components/tooltip.d.ts +7 -0
  91. package/dist/ui/components/tooltip.js +18 -0
  92. package/dist/ui/index.d.ts +2 -0
  93. package/dist/ui/index.js +2 -0
  94. package/dist/ui/lib/utils.d.ts +2 -0
  95. package/dist/ui/lib/utils.js +5 -0
  96. package/dist/ui/portal/portal-container.d.ts +13 -0
  97. package/dist/ui/portal/portal-container.js +12 -0
  98. package/dist/ui-annotate-plugin.d.ts +28 -0
  99. package/dist/ui-annotate-plugin.js +227 -0
  100. package/package.json +55 -0
@@ -0,0 +1,17 @@
1
+ export type OpenCodeRequest = {
2
+ path: string;
3
+ };
4
+ export type OpenCodeResponse = {
5
+ path: string;
6
+ absolutePath: string;
7
+ };
8
+ export type CodeGotoLocation = {
9
+ relativePath: string;
10
+ absolutePath: string;
11
+ line: number;
12
+ column: number;
13
+ gotoArg: string;
14
+ };
15
+ export declare function openCodeLocation(root: string, request: unknown): Promise<OpenCodeResponse>;
16
+ export declare function validateOpenCodeRequest(input: unknown): OpenCodeRequest;
17
+ export declare function resolveCodeGotoLocation(root: string, sourcePath: string): CodeGotoLocation;
@@ -0,0 +1,82 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { normalizeProjectRelativePath } from "./protocol/index.js";
4
+ export async function openCodeLocation(root, request) {
5
+ const body = validateOpenCodeRequest(request);
6
+ const location = resolveCodeGotoLocation(root, body.path);
7
+ await runCodeGoto(location.gotoArg);
8
+ return {
9
+ path: `${location.relativePath}:${location.line}:${location.column}`,
10
+ absolutePath: location.absolutePath
11
+ };
12
+ }
13
+ export function validateOpenCodeRequest(input) {
14
+ if (!isRecord(input)) {
15
+ throw new Error("Code open body must be a JSON object.");
16
+ }
17
+ return {
18
+ path: requireString(input.path, "path")
19
+ };
20
+ }
21
+ export function resolveCodeGotoLocation(root, sourcePath) {
22
+ const match = /^(.*):(\d+):(\d+)$/.exec(sourcePath);
23
+ if (!match) {
24
+ throw new Error('path must use "relative/path.tsx:line:column" format.');
25
+ }
26
+ const relativePath = normalizeProjectRelativePath(match[1] ?? "");
27
+ const line = requirePositiveInteger(Number.parseInt(match[2] ?? "0", 10), "path line");
28
+ const column = requirePositiveInteger(Number.parseInt(match[3] ?? "0", 10), "path column");
29
+ const absoluteRoot = path.resolve(root);
30
+ const absolutePath = path.resolve(absoluteRoot, relativePath);
31
+ const relativeFromRoot = path.relative(absoluteRoot, absolutePath);
32
+ if (relativeFromRoot.startsWith("..") || path.isAbsolute(relativeFromRoot)) {
33
+ throw new Error(`Source file must stay inside the project: ${sourcePath}`);
34
+ }
35
+ return {
36
+ relativePath,
37
+ absolutePath,
38
+ line,
39
+ column,
40
+ gotoArg: `${absolutePath}:${line}:${column}`
41
+ };
42
+ }
43
+ function runCodeGoto(gotoArg) {
44
+ return new Promise((resolve, reject) => {
45
+ const child = spawn("code", ["--goto", gotoArg], {
46
+ stdio: "ignore",
47
+ shell: false
48
+ });
49
+ child.on("error", (error) => {
50
+ if (isNodeError(error) && error.code === "ENOENT") {
51
+ reject(new Error('Unable to open VS Code. Make sure the "code" command is installed in PATH.'));
52
+ return;
53
+ }
54
+ reject(error);
55
+ });
56
+ child.on("exit", (code) => {
57
+ if (code === 0) {
58
+ resolve();
59
+ return;
60
+ }
61
+ reject(new Error(`VS Code exited with status ${code ?? "unknown"}.`));
62
+ });
63
+ });
64
+ }
65
+ function requireString(input, label) {
66
+ if (typeof input !== "string") {
67
+ throw new Error(`${label} must be a string.`);
68
+ }
69
+ return input;
70
+ }
71
+ function requirePositiveInteger(input, label) {
72
+ if (!Number.isInteger(input) || input < 1) {
73
+ throw new Error(`${label} must be a positive integer.`);
74
+ }
75
+ return input;
76
+ }
77
+ function isRecord(input) {
78
+ return typeof input === "object" && input !== null && !Array.isArray(input);
79
+ }
80
+ function isNodeError(error) {
81
+ return error instanceof Error && "code" in error;
82
+ }
@@ -0,0 +1,8 @@
1
+ export { UI_ANNOTATE_INSPECTOR_ATTRIBUTE, createUiAnnotateInspectorPlugin, transformUiAnnotateInspectorSource } from "./inspector-transform.js";
2
+ export type { UiAnnotateInspectorPluginOptions, UiAnnotateInspectorTransformOptions, UiAnnotateInspectorTransformResult } from "./inspector-transform.js";
3
+ export { createUiAnnotatePlugin, uiAnnotate } from "./ui-annotate-plugin.js";
4
+ export type { UiAnnotatePluginOptions } from "./ui-annotate-plugin.js";
5
+ export { openCodeLocation, resolveCodeGotoLocation, validateOpenCodeRequest } from "./code-open.js";
6
+ export type { CodeGotoLocation, OpenCodeRequest, OpenCodeResponse } from "./code-open.js";
7
+ export { createAnnotateApiServer, handleUiAnnotateRequest, readAnnotateTask, resolveAnnotateTaskPath, validateAnnotateTaskFile, writeAnnotateTask } from "./task-api.js";
8
+ export type { AnnotateApiServerOptions, AnnotateApiServerResult } from "./task-api.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { UI_ANNOTATE_INSPECTOR_ATTRIBUTE, createUiAnnotateInspectorPlugin, transformUiAnnotateInspectorSource } from "./inspector-transform.js";
2
+ export { createUiAnnotatePlugin, uiAnnotate } from "./ui-annotate-plugin.js";
3
+ export { openCodeLocation, resolveCodeGotoLocation, validateOpenCodeRequest } from "./code-open.js";
4
+ export { createAnnotateApiServer, handleUiAnnotateRequest, readAnnotateTask, resolveAnnotateTaskPath, validateAnnotateTaskFile, writeAnnotateTask } from "./task-api.js";
@@ -0,0 +1,59 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare const UI_ANNOTATE_INSPECTOR_ATTRIBUTE = "data-ui-annotate-inspector";
3
+ export type UiAnnotateInspectorTransformOptions = {
4
+ enabled?: boolean;
5
+ projectRelativePath?: string;
6
+ root?: string;
7
+ };
8
+ export type UiAnnotateInspectorTransformResult = {
9
+ code: string;
10
+ map: null;
11
+ };
12
+ export type UiAnnotateInspectorPluginOptions = {
13
+ enabled?: boolean;
14
+ root?: string;
15
+ };
16
+ type ViteConfig = {
17
+ root: string;
18
+ };
19
+ type VitePluginLike = {
20
+ name: string;
21
+ enforce: "pre";
22
+ apply: "serve";
23
+ configResolved(config: ViteConfig): void;
24
+ configureServer(server: ViteDevServerLike): void;
25
+ transform(code: string, id: string): UiAnnotateInspectorTransformResult | null;
26
+ };
27
+ type ConnectNext = (error?: unknown) => void;
28
+ type ViteDevServerLike = {
29
+ middlewares: {
30
+ use(handler: (request: IncomingMessage, response: ServerResponse, next: ConnectNext) => void): void;
31
+ };
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
+ 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
+ export declare function transformUiAnnotateInspectorSource(code: string, id: string, options?: UiAnnotateInspectorTransformOptions): UiAnnotateInspectorTransformResult | null;
59
+ export {};
@@ -0,0 +1,218 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ import { isWritableProjectSource, normalizeProjectRelativePath } from "./protocol/index.js";
4
+ import { handleUiAnnotateRequest } from "./task-api.js";
5
+ // 注入到每个 JSX 元素上的 HTML 属性名,值为 "相对路径:行号:列号" 格式的源码定位信息。
6
+ // UI Annotate runtime 在用户 hover/click 时读取这个属性,就能知道当前元素对应哪一行源码。
7
+ // 这个属性只在 dev server 的内存 bundle 中存在,不会写入真实文件,也不会进入生产构建。
8
+ 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
+ export function createUiAnnotateInspectorPlugin(options = {}) {
21
+ let root = path.resolve(options.root ?? process.cwd());
22
+ return {
23
+ name: "ui-annotate-inspector-transform",
24
+ enforce: "pre", // 在其他插件(如 @vitejs/plugin-react)之前执行,确保注入的是原始源码位置
25
+ apply: "serve", // 只在 dev server 生效,生产构建不加载这个插件
26
+ configResolved(config) {
27
+ // 优先使用插件选项中的 root,其次使用 Vite 解析出的项目根目录
28
+ root = path.resolve(options.root ?? config.root);
29
+ },
30
+ configureServer(server) {
31
+ // 挂载中间件:拦截所有 /__ui-annotate/* 路径的 UI Annotate runtime 请求。
32
+ server.middlewares.use(async (request, response, next) => {
33
+ const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
34
+ if (!pathname.startsWith("/__ui-annotate/")) {
35
+ next();
36
+ return;
37
+ }
38
+ try {
39
+ const handled = await handleUiAnnotateRequest(root, request, response);
40
+ if (!handled) {
41
+ next();
42
+ }
43
+ }
44
+ catch (error) {
45
+ next(error);
46
+ }
47
+ });
48
+ },
49
+ transform(code, id) {
50
+ return transformUiAnnotateInspectorSource(code, id, {
51
+ enabled: options.enabled,
52
+ root
53
+ });
54
+ }
55
+ };
56
+ }
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
+ export function transformUiAnnotateInspectorSource(code, id, options = {}) {
71
+ if (options.enabled === false) {
72
+ return null;
73
+ }
74
+ const relativePath = resolveInspectableRelativePath(id, options);
75
+ if (!relativePath) {
76
+ return null;
77
+ }
78
+ const inspectorRelativePath = relativePath;
79
+ // 用 TypeScript 编译器解析源码为 AST,开启 parent 节点引用以便后续遍历
80
+ const sourceFile = ts.createSourceFile(stripViteQuery(id), code, ts.ScriptTarget.Latest, true, getScriptKind(id));
81
+ // 收集所有需要插入 inspector 属性的位置
82
+ const insertions = [];
83
+ // 递归遍历 AST,找到所有 JSX 开标签和自闭合标签
84
+ function visit(node) {
85
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
86
+ collectInspectorInsertion(sourceFile, code, inspectorRelativePath, node, insertions);
87
+ }
88
+ ts.forEachChild(node, visit);
89
+ }
90
+ visit(sourceFile);
91
+ if (insertions.length === 0) {
92
+ return null;
93
+ }
94
+ return {
95
+ code: applyInsertions(code, insertions),
96
+ map: null
97
+ };
98
+ }
99
+ /**
100
+ * 为单个 JSX 元素收集 inspector 属性的插入计划。
101
+ *
102
+ * 跳过两种情况:
103
+ * - Fragment 标签(它不产生真实 DOM,注入属性没有意义)
104
+ * - 已经有 data-ui-annotate-inspector 的标签(避免重复注入)
105
+ *
106
+ * 对于其他标签,计算它在源码中的 "文件路径:行号:列号" 定位,
107
+ * 然后记录一个 Insertion,表示在标签的合适位置插入属性文本。
108
+ */
109
+ function collectInspectorInsertion(sourceFile, code, relativePath, node, insertions) {
110
+ if (isFragmentTag(sourceFile, node.tagName)) {
111
+ return;
112
+ }
113
+ if (hasAttribute(sourceFile, node.attributes, UI_ANNOTATE_INSPECTOR_ATTRIBUTE)) {
114
+ return;
115
+ }
116
+ const insertAt = getAttributeInsertionIndex(code, node);
117
+ if (insertAt === null) {
118
+ return;
119
+ }
120
+ // 将 AST 节点的起始位置转换为 "行号:列号" 格式(均为 1-based)
121
+ // 最终生成形如 "src/components/Button.tsx:12:5" 的定位字符串
122
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
123
+ const inspector = `${relativePath}:${line + 1}:${character + 1}`;
124
+ insertions.push({
125
+ index: insertAt,
126
+ text: ` ${UI_ANNOTATE_INSPECTOR_ATTRIBUTE}="${escapeJsxAttribute(inspector)}"`
127
+ });
128
+ }
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
+ function getAttributeInsertionIndex(code, node) {
141
+ if (ts.isJsxSelfClosingElement(node)) {
142
+ // 自闭合标签:从标签末尾往前找 "/>",属性插在它前面
143
+ const closeIndex = code.lastIndexOf("/>", node.end);
144
+ if (closeIndex < node.getStart()) {
145
+ return null;
146
+ }
147
+ // 如果 "/>" 前面已经有空格(如 <Foo bar />),直接插在 "/" 前面;
148
+ // 如果没有空格(如 <Foo/>),也插在 "/" 前面(Insertion text 自带前导空格)。
149
+ return /\s/.test(code[closeIndex - 1] ?? "") ? closeIndex - 1 : closeIndex;
150
+ }
151
+ // 开标签(非自闭合):从标签末尾往前找 ">",属性插在它前面
152
+ const closeIndex = code.lastIndexOf(">", node.end);
153
+ return closeIndex >= node.getStart() ? closeIndex : null;
154
+ }
155
+ // 检查 JSX 元素是否已经有指定名称的属性,用于避免重复注入
156
+ function hasAttribute(sourceFile, attributes, name) {
157
+ return attributes.properties.some((property) => {
158
+ return ts.isJsxAttribute(property) && property.name.getText(sourceFile) === name;
159
+ });
160
+ }
161
+ // Fragment 不产生真实 DOM 节点,注入属性没有意义,需要跳过
162
+ function isFragmentTag(sourceFile, tagName) {
163
+ const text = tagName.getText(sourceFile);
164
+ return text === "Fragment" || text === "React.Fragment";
165
+ }
166
+ /**
167
+ * 将收集到的所有 Insertion 按位置从后往前依次应用到源码上。
168
+ *
169
+ * 为什么要从后往前?
170
+ * 因为每次插入都会让后面的字符位置向后偏移。从后往前插入,
171
+ * 前面已记录的位置不会受到影响,避免了重新计算偏移量的问题。
172
+ */
173
+ function applyInsertions(code, insertions) {
174
+ let nextCode = code;
175
+ for (const insertion of [...insertions].sort((a, b) => b.index - a.index)) {
176
+ nextCode = `${nextCode.slice(0, insertion.index)}${insertion.text}${nextCode.slice(insertion.index)}`;
177
+ }
178
+ return nextCode;
179
+ }
180
+ /**
181
+ * 判断一个文件是否需要注入 inspector 信息。
182
+ *
183
+ * 只对"项目内可写源码"注入,排除 node_modules、dist、generated 等不可写路径。
184
+ * 这样做的原因:
185
+ * - 第三方库的 DOM 元素没有项目源码可以定位,注入了也没用
186
+ * - UI Annotate runtime 需要根据这个定位信息找到对应的源码
187
+ */
188
+ function resolveInspectableRelativePath(id, options) {
189
+ try {
190
+ const relativePath = options.projectRelativePath ?? toProjectRelativePath(stripViteQuery(id), options.root);
191
+ const normalized = normalizeProjectRelativePath(relativePath);
192
+ return isWritableProjectSource(normalized) ? normalized : null;
193
+ }
194
+ catch {
195
+ return null;
196
+ }
197
+ }
198
+ // 将绝对路径转为相对于项目根目录的路径,已经是相对路径则直接返回
199
+ function toProjectRelativePath(id, root = process.cwd()) {
200
+ if (!path.isAbsolute(id)) {
201
+ return id;
202
+ }
203
+ return path.relative(path.resolve(root), id);
204
+ }
205
+ // 去掉 Vite 在模块 ID 后面追加的查询参数,例如 "/src/App.tsx?vue&type=script" → "/src/App.tsx"
206
+ function stripViteQuery(id) {
207
+ const queryIndex = id.indexOf("?");
208
+ return queryIndex === -1 ? id : id.slice(0, queryIndex);
209
+ }
210
+ // 根据文件扩展名判断 TypeScript 解析模式(.jsx 用 JSX 模式,其余默认用 TSX 模式)
211
+ function getScriptKind(id) {
212
+ const cleanId = stripViteQuery(id);
213
+ return cleanId.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.TSX;
214
+ }
215
+ // 转义 JSX 属性值中的特殊字符,防止注入的属性值破坏 JSX 语法
216
+ function escapeJsxAttribute(value) {
217
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
218
+ }
@@ -0,0 +1,7 @@
1
+ export declare const ANNOTATE_TASK_FILE_NAME = "ui.annotate.json";
2
+ export declare const ANNOTATE_TASK_AGENT_INSTRUCTIONS: readonly ["This task file was created from UI Annotate runtime on host pages.", "Read targets first.", "Each target contains its own comments. Use target.comments as the implementation requirements attached to that target.", "Each item in target.comments is an independent requirement for the same target.", "Use target.trace as the primary navigation path from the host page to the selected target.", "target.trace is ordered from the farthest entry component to the nearest selected target.", "Each trace frame represents one continuous segment of calls within a source file.", "The same file may appear multiple times in trace if the navigation path enters it multiple times.", "Within each trace frame, calls are ordered from file entry to file exit.", "The last trace frame contains the selected target file, and its last call item is the selected target.", "If target.trace contains a single call item, that call is both the entry and the selected target.", "A call item line is a 1-based source line number.", "Use line numbers as navigation hints, not exact edit ranges.", "Line numbers may become stale after code edits or formatting changes.", "Treat each target as a location hint, not always the exact edit range; comments may refer to content inside the target or nearby UI.", "Decide whether to edit the selected element, parent layout, or referenced project component."];
3
+ export declare const EMPTY_ANNOTATE_TASK_FILE: {
4
+ readonly agentInstructions: readonly ["This task file was created from UI Annotate runtime on host pages.", "Read targets first.", "Each target contains its own comments. Use target.comments as the implementation requirements attached to that target.", "Each item in target.comments is an independent requirement for the same target.", "Use target.trace as the primary navigation path from the host page to the selected target.", "target.trace is ordered from the farthest entry component to the nearest selected target.", "Each trace frame represents one continuous segment of calls within a source file.", "The same file may appear multiple times in trace if the navigation path enters it multiple times.", "Within each trace frame, calls are ordered from file entry to file exit.", "The last trace frame contains the selected target file, and its last call item is the selected target.", "If target.trace contains a single call item, that call is both the entry and the selected target.", "A call item line is a 1-based source line number.", "Use line numbers as navigation hints, not exact edit ranges.", "Line numbers may become stale after code edits or formatting changes.", "Treat each target as a location hint, not always the exact edit range; comments may refer to content inside the target or nearby UI.", "Decide whether to edit the selected element, parent layout, or referenced project component."];
5
+ readonly targets: readonly [];
6
+ };
7
+ export declare const BLOCKED_WRITE_PATH_SEGMENTS: readonly ["node_modules", "dist", "build", ".next", "generated", "vendor"];
@@ -0,0 +1,31 @@
1
+ export const ANNOTATE_TASK_FILE_NAME = "ui.annotate.json";
2
+ export const ANNOTATE_TASK_AGENT_INSTRUCTIONS = [
3
+ "This task file was created from UI Annotate runtime on host pages.",
4
+ "Read targets first.",
5
+ "Each target contains its own comments. Use target.comments as the implementation requirements attached to that target.",
6
+ "Each item in target.comments is an independent requirement for the same target.",
7
+ "Use target.trace as the primary navigation path from the host page to the selected target.",
8
+ "target.trace is ordered from the farthest entry component to the nearest selected target.",
9
+ "Each trace frame represents one continuous segment of calls within a source file.",
10
+ "The same file may appear multiple times in trace if the navigation path enters it multiple times.",
11
+ "Within each trace frame, calls are ordered from file entry to file exit.",
12
+ "The last trace frame contains the selected target file, and its last call item is the selected target.",
13
+ "If target.trace contains a single call item, that call is both the entry and the selected target.",
14
+ "A call item line is a 1-based source line number.",
15
+ "Use line numbers as navigation hints, not exact edit ranges.",
16
+ "Line numbers may become stale after code edits or formatting changes.",
17
+ "Treat each target as a location hint, not always the exact edit range; comments may refer to content inside the target or nearby UI.",
18
+ "Decide whether to edit the selected element, parent layout, or referenced project component."
19
+ ];
20
+ export const EMPTY_ANNOTATE_TASK_FILE = {
21
+ agentInstructions: ANNOTATE_TASK_AGENT_INSTRUCTIONS,
22
+ targets: []
23
+ };
24
+ export const BLOCKED_WRITE_PATH_SEGMENTS = [
25
+ "node_modules",
26
+ "dist",
27
+ "build",
28
+ ".next",
29
+ "generated",
30
+ "vendor"
31
+ ];
@@ -0,0 +1,3 @@
1
+ import type { AnnotateTarget } from "./task-model.js";
2
+ export declare function parseTargetIndex(id: string): number | null;
3
+ export declare function getNextTargetId(targets: Pick<AnnotateTarget, "id">[]): string;
@@ -0,0 +1,15 @@
1
+ // 辅助逻辑:解析以 # 开头的目标 ID,例如 "#1" -> 1。
2
+ export function parseTargetIndex(id) {
3
+ if (!/^#[1-9]\d*$/.test(id)) {
4
+ return null;
5
+ }
6
+ return Number(id.slice(1));
7
+ }
8
+ // 交互执行流程:当用户圈选一个新目标时,遍历现有 target 集合,提取最大序号并 +1。
9
+ export function getNextTargetId(targets) {
10
+ const maxIndex = targets.reduce((max, target) => {
11
+ const index = parseTargetIndex(target.id);
12
+ return index === null ? max : Math.max(max, index);
13
+ }, 0);
14
+ return `#${maxIndex + 1}`;
15
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./constants.js";
2
+ export * from "./ids.js";
3
+ export * from "./paths.js";
4
+ export * from "./task-model.js";
@@ -0,0 +1,4 @@
1
+ export * from "./constants.js";
2
+ export * from "./ids.js";
3
+ export * from "./paths.js";
4
+ export * from "./task-model.js";
@@ -0,0 +1,3 @@
1
+ export declare function normalizeProjectRelativePath(input: string): string;
2
+ export declare function isBlockedWritePath(relativePath: string): boolean;
3
+ export declare function isWritableProjectSource(relativePath: string): boolean;
@@ -0,0 +1,36 @@
1
+ import path from "node:path";
2
+ import { BLOCKED_WRITE_PATH_SEGMENTS } from "./constants.js";
3
+ // 关键算法逻辑:将任意路径标准化为基于项目根目录的相对路径,并阻止跨目录逃逸。
4
+ export function normalizeProjectRelativePath(input) {
5
+ if (path.isAbsolute(input)) {
6
+ throw new Error(`Expected a project-relative path, received absolute path: ${input}`);
7
+ }
8
+ const normalized = input.replaceAll("\\", "/");
9
+ const collapsed = path.posix.normalize(normalized);
10
+ if (collapsed === "." || collapsed.startsWith("../") || collapsed === "..") {
11
+ throw new Error(`Path must stay inside the project: ${input}`);
12
+ }
13
+ return collapsed.replace(/^\.\/+/, "");
14
+ }
15
+ export function isBlockedWritePath(relativePath) {
16
+ try {
17
+ const normalized = normalizeProjectRelativePath(relativePath);
18
+ const segments = normalized.split("/");
19
+ return BLOCKED_WRITE_PATH_SEGMENTS.some((blocked) => segments.includes(blocked));
20
+ }
21
+ catch {
22
+ return true;
23
+ }
24
+ }
25
+ export function isWritableProjectSource(relativePath) {
26
+ try {
27
+ const normalized = normalizeProjectRelativePath(relativePath);
28
+ if (isBlockedWritePath(normalized)) {
29
+ return false;
30
+ }
31
+ return /\.(tsx|jsx)$/.test(normalized);
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
@@ -0,0 +1,35 @@
1
+ export type AnnotateTaskFile = {
2
+ agentInstructions: string[];
3
+ targets: AnnotateTarget[];
4
+ };
5
+ export type AnnotateTarget = {
6
+ id: string;
7
+ comments: string[];
8
+ trace: TraceFrame[];
9
+ };
10
+ export type TraceFrame = {
11
+ file: string;
12
+ calls: TraceCall[];
13
+ };
14
+ export type TraceCall = {
15
+ line: number;
16
+ component: string;
17
+ role?: "entry" | "target";
18
+ };
19
+ export type TraceNodeRef = {
20
+ kind: "file";
21
+ frameIndex: number;
22
+ } | {
23
+ kind: "call";
24
+ frameIndex: number;
25
+ callIndex: number;
26
+ };
27
+ export type ParsedSourceLocation = {
28
+ relativePath: string;
29
+ line: number;
30
+ column: number;
31
+ };
32
+ export declare function parseInspectorPath(path: string): ParsedSourceLocation;
33
+ export declare function getTargetSourceLocation(target: Pick<AnnotateTarget, "id" | "trace">): Omit<ParsedSourceLocation, "column">;
34
+ export declare function isSameTargetTrace(left: TraceFrame[], right: TraceFrame[]): boolean;
35
+ export declare function normalizeTraceRoles(trace: TraceFrame[]): TraceFrame[];
@@ -0,0 +1,68 @@
1
+ export function parseInspectorPath(path) {
2
+ const match = /^(.*):(\d+):(\d+)$/.exec(path);
3
+ if (!match) {
4
+ throw new Error(`Invalid source path: ${path}`);
5
+ }
6
+ return {
7
+ relativePath: match[1] ?? "",
8
+ line: Number.parseInt(match[2] ?? "0", 10),
9
+ column: Number.parseInt(match[3] ?? "0", 10)
10
+ };
11
+ }
12
+ export function getTargetSourceLocation(target) {
13
+ const lastFrame = target.trace[target.trace.length - 1];
14
+ const lastCall = lastFrame?.calls[lastFrame.calls.length - 1];
15
+ if (!lastFrame || !lastCall) {
16
+ throw new Error(`Target ${target.id} does not include a trace target.`);
17
+ }
18
+ return {
19
+ relativePath: lastFrame.file,
20
+ line: lastCall.line
21
+ };
22
+ }
23
+ export function isSameTargetTrace(left, right) {
24
+ if (left.length !== right.length) {
25
+ return false;
26
+ }
27
+ for (let index = 0; index < left.length; index += 1) {
28
+ if (left[index]?.file !== right[index]?.file) {
29
+ return false;
30
+ }
31
+ }
32
+ for (let frameIndex = 0; frameIndex < left.length; frameIndex += 1) {
33
+ const leftCalls = left[frameIndex]?.calls ?? [];
34
+ const rightCalls = right[frameIndex]?.calls ?? [];
35
+ if (leftCalls.length !== rightCalls.length) {
36
+ return false;
37
+ }
38
+ for (let callIndex = 0; callIndex < leftCalls.length; callIndex += 1) {
39
+ const leftCall = leftCalls[callIndex];
40
+ const rightCall = rightCalls[callIndex];
41
+ if (leftCall?.line !== rightCall?.line ||
42
+ leftCall?.component !== rightCall?.component ||
43
+ leftCall?.role !== rightCall?.role) {
44
+ return false;
45
+ }
46
+ }
47
+ }
48
+ return true;
49
+ }
50
+ export function normalizeTraceRoles(trace) {
51
+ const normalized = trace.map((frame) => ({
52
+ ...frame,
53
+ calls: frame.calls.map((call) => {
54
+ const { role: _role, ...rest } = call;
55
+ return rest;
56
+ })
57
+ }));
58
+ const calls = normalized.flatMap((frame) => frame.calls);
59
+ const firstCall = calls[0];
60
+ const lastCall = calls[calls.length - 1];
61
+ if (firstCall) {
62
+ firstCall.role = "entry";
63
+ }
64
+ if (lastCall) {
65
+ lastCall.role = "target";
66
+ }
67
+ return normalized;
68
+ }
@@ -0,0 +1,9 @@
1
+ import { type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
2
+ import type { AnnotateWindowId, ToolbarPosition } from "../shared/annotate-types.js";
3
+ export declare function AnnotateToolbar({ annotateMode, position, onBeginDrag, onOpenWindow, onToggleAnnotateMode }: {
4
+ annotateMode: boolean;
5
+ position: ToolbarPosition;
6
+ onBeginDrag(event: ReactPointerEvent<HTMLDivElement>): void;
7
+ onOpenWindow(id: AnnotateWindowId): void;
8
+ onToggleAnnotateMode(): void;
9
+ }): ReactNode;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { BugIcon, GripHorizontalIcon, SettingsIcon } from "lucide-react";
3
+ import { Button } from "../../ui/components/button.js";
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/components/tooltip.js";
5
+ export function AnnotateToolbar({ annotateMode, position, onBeginDrag, onOpenWindow, onToggleAnnotateMode }) {
6
+ const style = {
7
+ left: position.x,
8
+ top: position.y
9
+ };
10
+ return (_jsxs("div", { className: "pointer-events-auto fixed z-[10000] flex min-h-12 w-28 items-center gap-1.5 rounded-lg border bg-background/95 p-1.5 text-foreground shadow-xl backdrop-blur", style: style, onPointerDown: onBeginDrag, "aria-label": "UI Annotate toolbar", children: [_jsx(ToolbarButton, { active: annotateMode, icon: BugIcon, label: "Annotate mode", onClick: onToggleAnnotateMode }), _jsx(ToolbarButton, { icon: SettingsIcon, label: "Settings", onClick: () => onOpenWindow("settings") }), _jsx(GripHorizontalIcon, { "aria-hidden": "true", className: "ml-auto size-4 text-muted-foreground" })] }));
11
+ }
12
+ function ToolbarButton({ active = false, icon: Icon, label, onClick }) {
13
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: active ? "default" : "ghost", size: "icon", "aria-label": label, "aria-pressed": active, onClick: onClick, children: _jsx(Icon, { "data-icon": "inline-start", "aria-hidden": "true" }) }) }), _jsx(TooltipContent, { side: "top", children: label })] }));
14
+ }