@runner-ai/vite-uuid-tagger 0.0.9 → 0.0.11

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
@@ -1,10 +1,23 @@
1
- import { Plugin } from "vite";
1
+ import * as _$magic_string0 from "magic-string";
2
2
 
3
3
  //#region src/plugin.d.ts
4
4
  interface UuidTaggerOptions {
5
5
  projectRoot: string;
6
6
  }
7
- declare function uuidTagger(options: UuidTaggerOptions): Plugin;
7
+ interface WatchedFileChange {
8
+ readonly event: 'create' | 'update' | 'delete';
9
+ }
10
+ declare function uuidTagger(options: UuidTaggerOptions): {
11
+ name: string;
12
+ enforce: "pre";
13
+ transform(code: string, id: string): Promise<{
14
+ code: string;
15
+ map: _$magic_string0.SourceMap;
16
+ } | null>;
17
+ watchChange(id: string, {
18
+ event
19
+ }: WatchedFileChange): void;
20
+ };
8
21
  //#endregion
9
22
  export { type UuidTaggerOptions, uuidTagger };
10
23
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import path, { dirname, join } from "node:path";
2
2
  import { parse } from "@babel/parser";
3
3
  import MagicString from "magic-string";
4
+ import { z } from "zod";
4
5
  import { createHash } from "node:crypto";
5
6
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
6
7
  //#region src/ast-helpers.ts
@@ -35,6 +36,21 @@ function getElementName(node) {
35
36
  if (node.name.type === "JSXMemberExpression") return `${node.name.object.name}.${node.name.property.name}`;
36
37
  return null;
37
38
  }
39
+ const savedUuidAttributeSchema = z.object({
40
+ type: z.literal("JSXAttribute"),
41
+ name: z.object({ name: z.literal("data-uuid-loc") }),
42
+ value: z.object({
43
+ type: z.literal("StringLiteral"),
44
+ value: z.string()
45
+ })
46
+ });
47
+ const savedUuidSchema = z.object({ attributes: z.array(z.unknown()) }).transform(({ attributes }) => {
48
+ for (const attribute of attributes) {
49
+ const saved = savedUuidAttributeSchema.safeParse(attribute);
50
+ if (saved.success) return saved.data.value.value;
51
+ }
52
+ return null;
53
+ });
38
54
  /**
39
55
  * Determines if a JSXElement node should be tagged with a UUID.
40
56
  * Skips non-JSXElement nodes and Fragment elements.
@@ -120,28 +136,92 @@ function isInlineEditable(jsxElement) {
120
136
  }
121
137
  //#endregion
122
138
  //#region src/uuid-mapping-manager.ts
139
+ const savedMapSchema = z.record(z.string(), z.unknown());
140
+ const savedMappingSchema = z.object({
141
+ filePath: z.string(),
142
+ line: z.number(),
143
+ column: z.number(),
144
+ elementName: z.string()
145
+ });
146
+ function elementUUID(filePath, { line, column, elementName }) {
147
+ const hash = createHash("sha256").update(`${filePath}:${line}:${column}:${elementName}`).digest("hex");
148
+ return [
149
+ hash.slice(0, 8),
150
+ hash.slice(8, 12),
151
+ hash.slice(12, 16),
152
+ hash.slice(16, 20),
153
+ hash.slice(20, 32)
154
+ ].join("-");
155
+ }
156
+ function parseSavedMappings(text) {
157
+ try {
158
+ const saved = savedMapSchema.safeParse(JSON.parse(text));
159
+ if (!saved.success) return [];
160
+ return Object.entries(saved.data).flatMap(([uuid, value]) => {
161
+ const mapping = savedMappingSchema.safeParse(value);
162
+ return mapping.success ? [[uuid, mapping.data]] : [];
163
+ });
164
+ } catch {
165
+ return [];
166
+ }
167
+ }
168
+ function sameMapping(previous, next) {
169
+ return previous !== void 0 && previous.filePath === next.filePath && previous.line === next.line && previous.column === next.column && previous.elementName === next.elementName;
170
+ }
171
+ function sameEntries(previous, next) {
172
+ return previous.size === next.size && [...next].every(([uuid, mapping]) => sameMapping(previous.get(uuid), mapping));
173
+ }
123
174
  var UUIDMappingManager = class {
124
175
  mappingFile;
125
- mappings;
176
+ projectRoot;
177
+ files = /* @__PURE__ */ new Map();
126
178
  constructor(projectRoot) {
179
+ this.projectRoot = projectRoot;
127
180
  this.mappingFile = join(projectRoot, "public", "uuid-mappings.json");
128
- this.mappings = this.loadMappings();
181
+ const saved = this.readSavedText();
182
+ if (saved === null) return;
183
+ this.loadEntriesForExistingFiles(saved);
184
+ if (this.serialize() !== saved) this.saveMappings();
185
+ }
186
+ replaceFile(filePath, elements) {
187
+ const next = new Map(elements.map(({ id, line, column, elementName }) => [id, {
188
+ filePath,
189
+ line,
190
+ column,
191
+ elementName
192
+ }]));
193
+ if (sameEntries(this.files.get(filePath) ?? /* @__PURE__ */ new Map(), next)) return;
194
+ if (next.size === 0) this.files.delete(filePath);
195
+ else this.files.set(filePath, next);
196
+ this.saveMappings();
129
197
  }
130
- loadMappings() {
198
+ readSavedText() {
131
199
  try {
132
- if (existsSync(this.mappingFile)) {
133
- const content = readFileSync(this.mappingFile, "utf8");
134
- return JSON.parse(content);
135
- }
136
- } catch {}
137
- return {};
200
+ return existsSync(this.mappingFile) ? readFileSync(this.mappingFile, "utf8") : null;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ loadEntriesForExistingFiles(saved) {
206
+ const grouped = /* @__PURE__ */ new Map();
207
+ for (const [uuid, mapping] of parseSavedMappings(saved)) {
208
+ if (!existsSync(join(this.projectRoot, mapping.filePath))) continue;
209
+ const entries = grouped.get(mapping.filePath) ?? /* @__PURE__ */ new Map();
210
+ entries.set(uuid, mapping);
211
+ grouped.set(mapping.filePath, entries);
212
+ }
213
+ for (const [filePath, entries] of grouped) this.files.set(filePath, entries);
214
+ }
215
+ serialize() {
216
+ const lines = [...this.files.values()].flatMap((entries) => [...entries].map(([uuid, mapping]) => `${JSON.stringify(uuid)}:${JSON.stringify(mapping)}`));
217
+ return lines.length === 0 ? "{}" : `{\n${lines.join(",\n")}\n}`;
138
218
  }
139
219
  saveMappings() {
140
220
  const tmpFile = `${this.mappingFile}.${process.pid}.tmp`;
141
221
  try {
142
222
  const dir = dirname(this.mappingFile);
143
223
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
144
- writeFileSync(tmpFile, JSON.stringify(this.mappings, null, 2));
224
+ writeFileSync(tmpFile, this.serialize());
145
225
  renameSync(tmpFile, this.mappingFile);
146
226
  } catch {
147
227
  try {
@@ -149,34 +229,6 @@ var UUIDMappingManager = class {
149
229
  } catch {}
150
230
  }
151
231
  }
152
- generateStableUUID(seed) {
153
- const hash = createHash("sha256").update(seed).digest("hex");
154
- return [
155
- hash.slice(0, 8),
156
- hash.slice(8, 12),
157
- hash.slice(12, 16),
158
- hash.slice(16, 20),
159
- hash.slice(20, 32)
160
- ].join("-");
161
- }
162
- getOrCreateUUID(filePath, line, column, elementName) {
163
- const existingUUID = Object.keys(this.mappings).find((uuid) => {
164
- const m = this.mappings[uuid];
165
- return m.filePath === filePath && m.line === line && m.column === column && m.elementName === elementName;
166
- });
167
- if (existingUUID) return existingUUID;
168
- const seed = `${filePath}:${line}:${column}:${elementName}`;
169
- const uuid = this.generateStableUUID(seed);
170
- this.mappings[uuid] = {
171
- filePath,
172
- line,
173
- column,
174
- elementName,
175
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
176
- };
177
- this.saveMappings();
178
- return uuid;
179
- }
180
232
  };
181
233
  //#endregion
182
234
  //#region src/plugin.ts
@@ -198,7 +250,7 @@ function uuidTagger(options) {
198
250
  plugins: ["jsx", "typescript"]
199
251
  });
200
252
  const magic = new MagicString(code);
201
- let changed = 0;
253
+ const elements = [];
202
254
  walk(ast, { enter(node) {
203
255
  const n = node;
204
256
  if (!shouldTagElement(n)) return;
@@ -206,13 +258,24 @@ function uuidTagger(options) {
206
258
  const elementName = getElementName(openingElement);
207
259
  if (!elementName) return;
208
260
  const { line = 0, column = 0 } = openingElement.loc?.start ?? {};
209
- const uuid = uuidManager.getOrCreateUUID(relativePath, line, column, elementName);
210
- magic.appendLeft(openingElement.name.end ?? 0, ` data-uuid-loc="${uuid}"`);
261
+ const position = {
262
+ line,
263
+ column,
264
+ elementName
265
+ };
266
+ const saved = savedUuidSchema.safeParse(n.openingElement);
267
+ const savedUuid = saved.success ? saved.data : null;
268
+ const elementId = savedUuid ?? elementUUID(relativePath, position);
269
+ elements.push({
270
+ id: elementId,
271
+ ...position
272
+ });
273
+ if (savedUuid === null) magic.appendLeft(openingElement.name.end ?? 0, ` data-uuid-loc="${elementId}"`);
211
274
  if (supportsContentEditable(n)) magic.appendLeft(openingElement.name.end ?? 0, " data-content-editable");
212
275
  if (isInlineEditable(n)) magic.appendLeft(openingElement.name.end ?? 0, " data-inline-editable");
213
- changed++;
214
276
  } });
215
- if (changed > 0) return {
277
+ uuidManager.replaceFile(relativePath, elements);
278
+ if (elements.length > 0) return {
216
279
  code: magic.toString(),
217
280
  map: magic.generateMap({ hires: true })
218
281
  };
@@ -220,6 +283,9 @@ function uuidTagger(options) {
220
283
  } catch {
221
284
  return null;
222
285
  }
286
+ },
287
+ watchChange(id, { event }) {
288
+ if (event === "delete") uuidManager.replaceFile(path.relative(projectRoot, id), []);
223
289
  }
224
290
  };
225
291
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/ast-helpers.ts","../src/uuid-mapping-manager.ts","../src/plugin.ts"],"sourcesContent":["import path from 'node:path';\n\n/**\n * Elements that cannot have attributes added to them:\n * - Fragment & React.Fragment: virtual containers that don't render\n * as actual DOM elements, so they cannot have data attributes\n */\nconst NON_TAGGABLE_ELEMENTS = ['Fragment', 'React.Fragment'];\n\n/**\n * File extensions that should be processed by this plugin\n */\nconst VALID_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts']);\n\n// --- Babel AST node interfaces ---\n\ninterface JSXIdentifier {\n type: 'JSXIdentifier';\n name: string;\n}\n\ninterface JSXMemberExpression {\n type: 'JSXMemberExpression';\n object: JSXIdentifier;\n property: JSXIdentifier;\n}\n\ninterface JSXOpeningElement {\n type: 'JSXOpeningElement';\n name: JSXIdentifier | JSXMemberExpression;\n attributes: Array<JSXAttribute | JSXSpreadAttribute>;\n}\n\ninterface JSXAttribute {\n type: 'JSXAttribute';\n name: JSXIdentifier;\n value: StringLiteral | JSXExpressionContainer | null;\n}\n\ninterface JSXSpreadAttribute {\n type: 'JSXSpreadAttribute';\n argument: ASTExpression;\n}\n\ninterface JSXText {\n type: 'JSXText';\n value: string;\n}\n\ninterface JSXExpressionContainer {\n type: 'JSXExpressionContainer';\n expression: ASTExpression;\n}\n\ninterface JSXElement {\n type: 'JSXElement';\n openingElement: JSXOpeningElement;\n children: Array<JSXText | JSXExpressionContainer | JSXElement>;\n}\n\ninterface JSXEmptyExpression {\n type: 'JSXEmptyExpression';\n}\n\ninterface NumericLiteral {\n type: 'NumericLiteral';\n}\n\ninterface BooleanLiteral {\n type: 'BooleanLiteral';\n}\n\ninterface StringLiteral {\n type: 'StringLiteral';\n}\n\ninterface NullLiteral {\n type: 'NullLiteral';\n}\n\ninterface ObjectProperty {\n type: 'ObjectProperty';\n computed: boolean;\n value: ASTExpression;\n}\n\ninterface SpreadElement {\n type: 'SpreadElement';\n}\n\ninterface ObjectExpression {\n type: 'ObjectExpression';\n properties: Array<ObjectProperty | SpreadElement>;\n}\n\ninterface ArrayExpression {\n type: 'ArrayExpression';\n elements: Array<ASTExpression | SpreadElement | null>;\n}\n\ntype ASTExpression =\n | JSXEmptyExpression\n | NumericLiteral\n | BooleanLiteral\n | StringLiteral\n | NullLiteral\n | ObjectExpression\n | ArrayExpression\n | SpreadElement\n | { type: string };\n\n// --- Helper functions ---\n\n/**\n * Determines if a file should be skipped by the plugin.\n * Skips node_modules and files without valid JSX/TS extensions.\n */\nexport function shouldSkipFile(filename: string): boolean {\n return filename.includes('node_modules') || !VALID_EXTENSIONS.has(path.extname(filename));\n}\n\n/**\n * Extracts element name from a JSX opening element node.\n * Returns 'div' for `<div>`, 'motion.div' for `<motion.div>`, etc.\n */\nexport function getElementName(node: JSXOpeningElement): string | null {\n // Handle simple JSX elements: <div>, <MyComponent>, etc.\n if (node.name.type === 'JSXIdentifier') {\n return node.name.name;\n }\n\n // Handle member expressions: <Foo.Bar>, <motion.div>, etc.\n if (node.name.type === 'JSXMemberExpression') {\n return `${node.name.object.name}.${node.name.property.name}`;\n }\n\n // Unknown JSX element type - skip it\n return null;\n}\n\n/**\n * Determines if a JSXElement node should be tagged with a UUID.\n * Skips non-JSXElement nodes and Fragment elements.\n */\nexport function shouldTagElement(node: { type: string; openingElement?: JSXOpeningElement }): boolean {\n if (node.type !== 'JSXElement') {\n return false;\n }\n\n const openingElement = node.openingElement;\n if (!openingElement) {\n return false;\n }\n\n const elementName = getElementName(openingElement);\n if (!elementName || NON_TAGGABLE_ELEMENTS.includes(elementName)) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Determines if an expression is static (contains no dynamic references).\n * Static means: literals, static objects, static arrays, empty expressions (comments).\n */\nexport function isStaticExpression(expression: ASTExpression | null | undefined): boolean {\n if (!expression) {\n return false;\n }\n\n // Comments compile to JSXEmptyExpression\n if (expression.type === 'JSXEmptyExpression') {\n return true;\n }\n\n // Static literals\n if (['NumericLiteral', 'BooleanLiteral', 'StringLiteral', 'NullLiteral'].includes(expression.type)) {\n return true;\n }\n\n // Object literals: check all properties are static\n if (expression.type === 'ObjectExpression') {\n const obj = expression as ObjectExpression;\n return obj.properties.every((prop) => {\n if (prop.type === 'SpreadElement') {\n return false;\n }\n if (prop.type === 'ObjectProperty') {\n if (prop.computed) {\n return false;\n }\n return isStaticExpression(prop.value);\n }\n return false;\n });\n }\n\n // Array literals: check all elements are static\n if (expression.type === 'ArrayExpression') {\n const arr = expression as ArrayExpression;\n return arr.elements.every((elem) => {\n if (elem === null) {\n return true; // Holes in arrays\n }\n if (elem.type === 'SpreadElement') {\n return false;\n }\n return isStaticExpression(elem);\n });\n }\n\n // Everything else is dynamic (Identifier, CallExpression, MemberExpression, etc.)\n return false;\n}\n\n/**\n * Determines if an element can use contenteditable.\n * True when the element has only static text children (no nested elements).\n */\nexport function supportsContentEditable(jsxElement: JSXElement): boolean {\n if (!jsxElement.children || jsxElement.children.length === 0) {\n return false;\n }\n\n const hasValidChildren = jsxElement.children.every((child) => {\n if (child.type === 'JSXText') {\n return true;\n }\n if (child.type === 'JSXExpressionContainer') {\n return isStaticExpression((child as JSXExpressionContainer).expression);\n }\n return false;\n });\n\n if (!hasValidChildren) {\n return false;\n }\n\n // At least one child must have non-whitespace content\n return jsxElement.children.some((child) => {\n if (child.type === 'JSXText') {\n return (child as JSXText).value.trim().length > 0;\n }\n if (child.type === 'JSXExpressionContainer') {\n return (child as JSXExpressionContainer).expression.type !== 'JSXEmptyExpression';\n }\n return false;\n });\n}\n\n/**\n * Determines if an element's content/attributes can be edited inline.\n * True when the element has static text content or static string attributes.\n */\nexport function isInlineEditable(jsxElement: JSXElement): boolean {\n // Check children for static expressions\n const hasStaticChildren =\n jsxElement.children &&\n jsxElement.children.length > 0 &&\n jsxElement.children.every((child) => {\n if (child.type === 'JSXText') {\n return true;\n }\n if (child.type === 'JSXExpressionContainer') {\n return isStaticExpression((child as JSXExpressionContainer).expression);\n }\n return false;\n });\n\n // Check if there's actual editable content (not just comments)\n const hasEditableContent = jsxElement.children?.some((child) => {\n if (child.type === 'JSXText') {\n return (child as JSXText).value.trim().length > 0;\n }\n if (child.type === 'JSXExpressionContainer') {\n return (child as JSXExpressionContainer).expression.type !== 'JSXEmptyExpression';\n }\n return false;\n });\n\n if (hasStaticChildren && hasEditableContent) {\n return true;\n }\n\n // Check attributes for static expressions\n if (jsxElement.openingElement.attributes && jsxElement.openingElement.attributes.length > 0) {\n const hasStaticAttributes = jsxElement.openingElement.attributes.some((attr) => {\n if (attr.type === 'JSXSpreadAttribute') {\n return false;\n }\n\n if (attr.value && attr.value.type === 'JSXExpressionContainer') {\n return isStaticExpression((attr.value as JSXExpressionContainer).expression);\n }\n\n if (attr.value && attr.value.type === 'StringLiteral') {\n return true;\n }\n\n return false;\n });\n\n if (hasStaticAttributes) {\n return true;\n }\n }\n\n return false;\n}\n","import { createHash } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nexport interface UUIDMapping {\n column: number;\n elementName: string;\n filePath: string;\n lastUpdated?: string;\n line: number;\n}\n\nexport class UUIDMappingManager {\n private readonly mappingFile: string;\n private readonly mappings: Record<string, UUIDMapping>;\n\n constructor(projectRoot: string) {\n this.mappingFile = join(projectRoot, 'public', 'uuid-mappings.json');\n this.mappings = this.loadMappings();\n }\n\n private loadMappings(): Record<string, UUIDMapping> {\n try {\n if (existsSync(this.mappingFile)) {\n const content = readFileSync(this.mappingFile, 'utf8');\n return JSON.parse(content);\n }\n } catch {\n // Start fresh on error\n }\n return {};\n }\n\n saveMappings(): void {\n // Per-process temp path next to the destination (same filesystem) so the\n // rename below is atomic.\n const tmpFile = `${this.mappingFile}.${process.pid}.tmp`;\n try {\n const dir = dirname(this.mappingFile);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n // Write to a temp file then atomically rename it into place. A direct\n // writeFileSync truncates the destination first, so a reader that loads\n // the file mid-write sees partial/invalid JSON (RUN-2217); the rename\n // guarantees a reader only ever sees a complete file.\n writeFileSync(tmpFile, JSON.stringify(this.mappings, null, 2));\n renameSync(tmpFile, this.mappingFile);\n } catch {\n // Silent fail — non-critical. Best-effort cleanup so a failed write\n // can't leave a stray partial temp file behind.\n try {\n if (existsSync(tmpFile)) {\n unlinkSync(tmpFile);\n }\n } catch {\n // ignore cleanup failure\n }\n }\n }\n\n private generateStableUUID(seed: string): string {\n const hash = createHash('sha256').update(seed).digest('hex');\n return [hash.slice(0, 8), hash.slice(8, 12), hash.slice(12, 16), hash.slice(16, 20), hash.slice(20, 32)].join('-');\n }\n\n getOrCreateUUID(filePath: string, line: number, column: number, elementName: string): string {\n const existingUUID = Object.keys(this.mappings).find((uuid) => {\n const m = this.mappings[uuid];\n return m.filePath === filePath && m.line === line && m.column === column && m.elementName === elementName;\n });\n\n if (existingUUID) {\n return existingUUID;\n }\n\n const seed = `${filePath}:${line}:${column}:${elementName}`;\n const uuid = this.generateStableUUID(seed);\n\n this.mappings[uuid] = {\n filePath,\n line,\n column,\n elementName,\n lastUpdated: new Date().toISOString(),\n };\n\n this.saveMappings();\n return uuid;\n }\n}\n","import path from 'node:path';\nimport { parse } from '@babel/parser';\nimport MagicString from 'magic-string';\nimport type { Plugin } from 'vite';\nimport {\n getElementName,\n isInlineEditable,\n shouldSkipFile,\n shouldTagElement,\n supportsContentEditable,\n} from './ast-helpers';\nimport { UUIDMappingManager } from './uuid-mapping-manager';\n\nexport interface UuidTaggerOptions {\n projectRoot: string;\n}\n\nexport function uuidTagger(options: UuidTaggerOptions): Plugin {\n const { projectRoot } = options;\n const uuidManager = new UUIDMappingManager(projectRoot);\n\n return {\n name: 'vite-uuid-tagger',\n enforce: 'pre',\n\n async transform(code: string, id: string) {\n if (shouldSkipFile(id)) {\n return null;\n }\n\n const ext = path.extname(id);\n if (ext !== '.tsx' && ext !== '.jsx') {\n return null;\n }\n\n const relativePath = path.relative(projectRoot, id);\n const { walk } = await import('estree-walker');\n\n try {\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['jsx', 'typescript'],\n });\n\n const magic = new MagicString(code);\n let changed = 0;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Babel AST is not estree-compatible\n walk(ast as any, {\n enter(node) {\n // Babel AST nodes are not estree-compatible, so we cast through unknown\n const n = node as unknown as Record<string, unknown>;\n if (!shouldTagElement(n as never)) {\n return;\n }\n\n const openingElement = n.openingElement as {\n loc?: { start?: { line: number; column: number } };\n name: { end?: number };\n };\n const elementName = getElementName(openingElement as never);\n if (!elementName) {\n return;\n }\n\n const { line = 0, column = 0 } = openingElement.loc?.start ?? {};\n const uuid = uuidManager.getOrCreateUUID(relativePath, line, column, elementName);\n\n magic.appendLeft(openingElement.name.end ?? 0, ` data-uuid-loc=\"${uuid}\"`);\n\n if (supportsContentEditable(n as never)) {\n magic.appendLeft(openingElement.name.end ?? 0, ' data-content-editable');\n }\n\n if (isInlineEditable(n as never)) {\n magic.appendLeft(openingElement.name.end ?? 0, ' data-inline-editable');\n }\n\n changed++;\n },\n });\n\n if (changed > 0) {\n return {\n code: magic.toString(),\n map: magic.generateMap({ hires: true }),\n };\n }\n return null;\n } catch {\n return null;\n }\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/ast-helpers.ts","../src/uuid-mapping-manager.ts","../src/plugin.ts"],"sourcesContent":["import path from 'node:path';\nimport { z } from 'zod';\n\n/**\n * Elements that cannot have attributes added to them:\n * - Fragment & React.Fragment: virtual containers that don't render\n * as actual DOM elements, so they cannot have data attributes\n */\nconst NON_TAGGABLE_ELEMENTS = ['Fragment', 'React.Fragment'];\n\n/**\n * File extensions that should be processed by this plugin\n */\nconst VALID_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts']);\n\n// --- Babel AST node interfaces ---\n\ninterface JSXIdentifier {\n type: 'JSXIdentifier';\n name: string;\n}\n\ninterface JSXMemberExpression {\n type: 'JSXMemberExpression';\n object: JSXIdentifier;\n property: JSXIdentifier;\n}\n\ninterface JSXOpeningElement {\n type: 'JSXOpeningElement';\n name: JSXIdentifier | JSXMemberExpression;\n attributes: Array<JSXAttribute | JSXSpreadAttribute>;\n}\n\ninterface JSXAttribute {\n type: 'JSXAttribute';\n name: JSXIdentifier;\n value: StringLiteral | JSXExpressionContainer | null;\n}\n\ninterface JSXSpreadAttribute {\n type: 'JSXSpreadAttribute';\n argument: ASTExpression;\n}\n\ninterface JSXText {\n type: 'JSXText';\n value: string;\n}\n\ninterface JSXExpressionContainer {\n type: 'JSXExpressionContainer';\n expression: ASTExpression;\n}\n\ninterface JSXElement {\n type: 'JSXElement';\n openingElement: JSXOpeningElement;\n children: Array<JSXText | JSXExpressionContainer | JSXElement>;\n}\n\ninterface JSXEmptyExpression {\n type: 'JSXEmptyExpression';\n}\n\ninterface NumericLiteral {\n type: 'NumericLiteral';\n}\n\ninterface BooleanLiteral {\n type: 'BooleanLiteral';\n}\n\ninterface StringLiteral {\n type: 'StringLiteral';\n}\n\ninterface NullLiteral {\n type: 'NullLiteral';\n}\n\ninterface ObjectProperty {\n type: 'ObjectProperty';\n computed: boolean;\n value: ASTExpression;\n}\n\ninterface SpreadElement {\n type: 'SpreadElement';\n}\n\ninterface ObjectExpression {\n type: 'ObjectExpression';\n properties: Array<ObjectProperty | SpreadElement>;\n}\n\ninterface ArrayExpression {\n type: 'ArrayExpression';\n elements: Array<ASTExpression | SpreadElement | null>;\n}\n\ntype ASTExpression =\n | JSXEmptyExpression\n | NumericLiteral\n | BooleanLiteral\n | StringLiteral\n | NullLiteral\n | ObjectExpression\n | ArrayExpression\n | SpreadElement\n | { type: string };\n\n// --- Helper functions ---\n\n/**\n * Determines if a file should be skipped by the plugin.\n * Skips node_modules and files without valid JSX/TS extensions.\n */\nexport function shouldSkipFile(filename: string): boolean {\n return filename.includes('node_modules') || !VALID_EXTENSIONS.has(path.extname(filename));\n}\n\n/**\n * Extracts element name from a JSX opening element node.\n * Returns 'div' for `<div>`, 'motion.div' for `<motion.div>`, etc.\n */\nexport function getElementName(node: JSXOpeningElement): string | null {\n // Handle simple JSX elements: <div>, <MyComponent>, etc.\n if (node.name.type === 'JSXIdentifier') {\n return node.name.name;\n }\n\n // Handle member expressions: <Foo.Bar>, <motion.div>, etc.\n if (node.name.type === 'JSXMemberExpression') {\n return `${node.name.object.name}.${node.name.property.name}`;\n }\n\n // Unknown JSX element type - skip it\n return null;\n}\n\nconst savedUuidAttributeSchema = z.object({\n type: z.literal('JSXAttribute'),\n name: z.object({ name: z.literal('data-uuid-loc') }),\n value: z.object({ type: z.literal('StringLiteral'), value: z.string() }),\n});\n\nexport const savedUuidSchema = z.object({ attributes: z.array(z.unknown()) }).transform(({ attributes }) => {\n for (const attribute of attributes) {\n const saved = savedUuidAttributeSchema.safeParse(attribute);\n if (saved.success) {\n return saved.data.value.value;\n }\n }\n return null;\n});\n\n/**\n * Determines if a JSXElement node should be tagged with a UUID.\n * Skips non-JSXElement nodes and Fragment elements.\n */\nexport function shouldTagElement(node: { type: string; openingElement?: JSXOpeningElement }): boolean {\n if (node.type !== 'JSXElement') {\n return false;\n }\n\n const openingElement = node.openingElement;\n if (!openingElement) {\n return false;\n }\n\n const elementName = getElementName(openingElement);\n if (!elementName || NON_TAGGABLE_ELEMENTS.includes(elementName)) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Determines if an expression is static (contains no dynamic references).\n * Static means: literals, static objects, static arrays, empty expressions (comments).\n */\nexport function isStaticExpression(expression: ASTExpression | null | undefined): boolean {\n if (!expression) {\n return false;\n }\n\n // Comments compile to JSXEmptyExpression\n if (expression.type === 'JSXEmptyExpression') {\n return true;\n }\n\n // Static literals\n if (['NumericLiteral', 'BooleanLiteral', 'StringLiteral', 'NullLiteral'].includes(expression.type)) {\n return true;\n }\n\n // Object literals: check all properties are static\n if (expression.type === 'ObjectExpression') {\n const obj = expression as ObjectExpression;\n return obj.properties.every((prop) => {\n if (prop.type === 'SpreadElement') {\n return false;\n }\n if (prop.type === 'ObjectProperty') {\n if (prop.computed) {\n return false;\n }\n return isStaticExpression(prop.value);\n }\n return false;\n });\n }\n\n // Array literals: check all elements are static\n if (expression.type === 'ArrayExpression') {\n const arr = expression as ArrayExpression;\n return arr.elements.every((elem) => {\n if (elem === null) {\n return true; // Holes in arrays\n }\n if (elem.type === 'SpreadElement') {\n return false;\n }\n return isStaticExpression(elem);\n });\n }\n\n // Everything else is dynamic (Identifier, CallExpression, MemberExpression, etc.)\n return false;\n}\n\n/**\n * Determines if an element can use contenteditable.\n * True when the element has only static text children (no nested elements).\n */\nexport function supportsContentEditable(jsxElement: JSXElement): boolean {\n if (!jsxElement.children || jsxElement.children.length === 0) {\n return false;\n }\n\n const hasValidChildren = jsxElement.children.every((child) => {\n if (child.type === 'JSXText') {\n return true;\n }\n if (child.type === 'JSXExpressionContainer') {\n return isStaticExpression((child as JSXExpressionContainer).expression);\n }\n return false;\n });\n\n if (!hasValidChildren) {\n return false;\n }\n\n // At least one child must have non-whitespace content\n return jsxElement.children.some((child) => {\n if (child.type === 'JSXText') {\n return (child as JSXText).value.trim().length > 0;\n }\n if (child.type === 'JSXExpressionContainer') {\n return (child as JSXExpressionContainer).expression.type !== 'JSXEmptyExpression';\n }\n return false;\n });\n}\n\n/**\n * Determines if an element's content/attributes can be edited inline.\n * True when the element has static text content or static string attributes.\n */\nexport function isInlineEditable(jsxElement: JSXElement): boolean {\n // Check children for static expressions\n const hasStaticChildren =\n jsxElement.children &&\n jsxElement.children.length > 0 &&\n jsxElement.children.every((child) => {\n if (child.type === 'JSXText') {\n return true;\n }\n if (child.type === 'JSXExpressionContainer') {\n return isStaticExpression((child as JSXExpressionContainer).expression);\n }\n return false;\n });\n\n // Check if there's actual editable content (not just comments)\n const hasEditableContent = jsxElement.children?.some((child) => {\n if (child.type === 'JSXText') {\n return (child as JSXText).value.trim().length > 0;\n }\n if (child.type === 'JSXExpressionContainer') {\n return (child as JSXExpressionContainer).expression.type !== 'JSXEmptyExpression';\n }\n return false;\n });\n\n if (hasStaticChildren && hasEditableContent) {\n return true;\n }\n\n // Check attributes for static expressions\n if (jsxElement.openingElement.attributes && jsxElement.openingElement.attributes.length > 0) {\n const hasStaticAttributes = jsxElement.openingElement.attributes.some((attr) => {\n if (attr.type === 'JSXSpreadAttribute') {\n return false;\n }\n\n if (attr.value && attr.value.type === 'JSXExpressionContainer') {\n return isStaticExpression((attr.value as JSXExpressionContainer).expression);\n }\n\n if (attr.value && attr.value.type === 'StringLiteral') {\n return true;\n }\n\n return false;\n });\n\n if (hasStaticAttributes) {\n return true;\n }\n }\n\n return false;\n}\n","import { createHash } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { z } from 'zod';\n\nexport interface ElementPosition {\n readonly column: number;\n readonly elementName: string;\n readonly line: number;\n}\n\nexport interface TaggedElement extends ElementPosition {\n readonly id: string;\n}\n\nexport interface UUIDMapping extends ElementPosition {\n readonly filePath: string;\n}\n\ntype FileMappings = ReadonlyMap<string, UUIDMapping>;\n\nconst savedMapSchema = z.record(z.string(), z.unknown());\n\nconst savedMappingSchema = z.object({\n filePath: z.string(),\n line: z.number(),\n column: z.number(),\n elementName: z.string(),\n});\n\nexport function elementUUID(filePath: string, { line, column, elementName }: ElementPosition): string {\n const hash = createHash('sha256').update(`${filePath}:${line}:${column}:${elementName}`).digest('hex');\n return [hash.slice(0, 8), hash.slice(8, 12), hash.slice(12, 16), hash.slice(16, 20), hash.slice(20, 32)].join('-');\n}\n\nfunction parseSavedMappings(text: string): [string, UUIDMapping][] {\n try {\n const saved = savedMapSchema.safeParse(JSON.parse(text));\n if (!saved.success) {\n return [];\n }\n return Object.entries(saved.data).flatMap(([uuid, value]): [string, UUIDMapping][] => {\n const mapping = savedMappingSchema.safeParse(value);\n return mapping.success ? [[uuid, mapping.data]] : [];\n });\n } catch {\n return [];\n }\n}\n\nfunction sameMapping(previous: UUIDMapping | undefined, next: UUIDMapping): boolean {\n return (\n previous !== undefined &&\n previous.filePath === next.filePath &&\n previous.line === next.line &&\n previous.column === next.column &&\n previous.elementName === next.elementName\n );\n}\n\nfunction sameEntries(previous: FileMappings, next: FileMappings): boolean {\n return previous.size === next.size && [...next].every(([uuid, mapping]) => sameMapping(previous.get(uuid), mapping));\n}\n\nexport class UUIDMappingManager {\n private readonly mappingFile: string;\n private readonly projectRoot: string;\n private readonly files = new Map<string, FileMappings>();\n\n constructor(projectRoot: string) {\n this.projectRoot = projectRoot;\n this.mappingFile = join(projectRoot, 'public', 'uuid-mappings.json');\n const saved = this.readSavedText();\n if (saved === null) {\n return;\n }\n this.loadEntriesForExistingFiles(saved);\n if (this.serialize() !== saved) {\n this.saveMappings();\n }\n }\n\n replaceFile(filePath: string, elements: readonly TaggedElement[]): void {\n const next: FileMappings = new Map(\n elements.map(({ id, line, column, elementName }): [string, UUIDMapping] => [\n id,\n { filePath, line, column, elementName },\n ]),\n );\n if (sameEntries(this.files.get(filePath) ?? new Map<string, UUIDMapping>(), next)) {\n return;\n }\n if (next.size === 0) {\n this.files.delete(filePath);\n } else {\n this.files.set(filePath, next);\n }\n this.saveMappings();\n }\n\n private readSavedText(): string | null {\n try {\n return existsSync(this.mappingFile) ? readFileSync(this.mappingFile, 'utf8') : null;\n } catch {\n return null;\n }\n }\n\n private loadEntriesForExistingFiles(saved: string): void {\n const grouped = new Map<string, Map<string, UUIDMapping>>();\n for (const [uuid, mapping] of parseSavedMappings(saved)) {\n if (!existsSync(join(this.projectRoot, mapping.filePath))) {\n continue;\n }\n const entries = grouped.get(mapping.filePath) ?? new Map<string, UUIDMapping>();\n entries.set(uuid, mapping);\n grouped.set(mapping.filePath, entries);\n }\n for (const [filePath, entries] of grouped) {\n this.files.set(filePath, entries);\n }\n }\n\n private serialize(): string {\n const lines = [...this.files.values()].flatMap((entries) =>\n [...entries].map(([uuid, mapping]) => `${JSON.stringify(uuid)}:${JSON.stringify(mapping)}`),\n );\n return lines.length === 0 ? '{}' : `{\\n${lines.join(',\\n')}\\n}`;\n }\n\n private saveMappings(): void {\n // Per-process temp path next to the destination (same filesystem) so the\n // rename below is atomic.\n const tmpFile = `${this.mappingFile}.${process.pid}.tmp`;\n try {\n const dir = dirname(this.mappingFile);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n // Write to a temp file then atomically rename it into place. A direct\n // writeFileSync truncates the destination first, so a reader that loads\n // the file mid-write sees partial/invalid JSON (RUN-2217); the rename\n // guarantees a reader only ever sees a complete file.\n writeFileSync(tmpFile, this.serialize());\n renameSync(tmpFile, this.mappingFile);\n } catch {\n // Silent fail — non-critical. Best-effort cleanup so a failed write\n // can't leave a stray partial temp file behind.\n try {\n if (existsSync(tmpFile)) {\n unlinkSync(tmpFile);\n }\n } catch {\n // ignore cleanup failure\n }\n }\n }\n}\n","import path from 'node:path';\nimport { parse } from '@babel/parser';\nimport MagicString from 'magic-string';\nimport type { Plugin } from 'vite';\nimport {\n getElementName,\n isInlineEditable,\n savedUuidSchema,\n shouldSkipFile,\n shouldTagElement,\n supportsContentEditable,\n} from './ast-helpers';\nimport { type ElementPosition, elementUUID, type TaggedElement, UUIDMappingManager } from './uuid-mapping-manager';\n\nexport interface UuidTaggerOptions {\n projectRoot: string;\n}\n\nexport interface WatchedFileChange {\n readonly event: 'create' | 'update' | 'delete';\n}\n\nexport function uuidTagger(options: UuidTaggerOptions) {\n const { projectRoot } = options;\n const uuidManager = new UUIDMappingManager(projectRoot);\n\n return {\n name: 'vite-uuid-tagger',\n enforce: 'pre',\n\n async transform(code: string, id: string) {\n if (shouldSkipFile(id)) {\n return null;\n }\n\n const ext = path.extname(id);\n if (ext !== '.tsx' && ext !== '.jsx') {\n return null;\n }\n\n const relativePath = path.relative(projectRoot, id);\n const { walk } = await import('estree-walker');\n\n try {\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['jsx', 'typescript'],\n });\n\n const magic = new MagicString(code);\n const elements: TaggedElement[] = [];\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Babel AST is not estree-compatible\n walk(ast as any, {\n enter(node) {\n // Babel AST nodes are not estree-compatible, so we cast through unknown\n const n = node as unknown as Record<string, unknown>;\n if (!shouldTagElement(n as never)) {\n return;\n }\n\n const openingElement = n.openingElement as {\n loc?: { start?: { line: number; column: number } };\n name: { end?: number };\n };\n const elementName = getElementName(openingElement as never);\n if (!elementName) {\n return;\n }\n\n const { line = 0, column = 0 } = openingElement.loc?.start ?? {};\n const position: ElementPosition = { line, column, elementName };\n const saved = savedUuidSchema.safeParse(n.openingElement);\n const savedUuid = saved.success ? saved.data : null;\n const elementId = savedUuid ?? elementUUID(relativePath, position);\n elements.push({ id: elementId, ...position });\n\n if (savedUuid === null) {\n magic.appendLeft(openingElement.name.end ?? 0, ` data-uuid-loc=\"${elementId}\"`);\n }\n\n if (supportsContentEditable(n as never)) {\n magic.appendLeft(openingElement.name.end ?? 0, ' data-content-editable');\n }\n\n if (isInlineEditable(n as never)) {\n magic.appendLeft(openingElement.name.end ?? 0, ' data-inline-editable');\n }\n },\n });\n\n uuidManager.replaceFile(relativePath, elements);\n\n if (elements.length > 0) {\n return {\n code: magic.toString(),\n map: magic.generateMap({ hires: true }),\n };\n }\n return null;\n } catch {\n return null;\n }\n },\n\n watchChange(id: string, { event }: WatchedFileChange) {\n if (event === 'delete') {\n uuidManager.replaceFile(path.relative(projectRoot, id), []);\n }\n },\n } satisfies Plugin;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@runner-ai/vite-uuid-tagger",
3
- "version": "0.0.9",
4
- "runnerSourceCommit": "37bd6bbd45550b799326f1eba973f59f95415b62",
5
- "runnerSourceDigest": "e64766ccee6ee42985ae91748a6c47cf08074f1a2880b861ad413aff56b109b5",
3
+ "version": "0.0.11",
4
+ "runnerSourceCommit": "fd192417d9138202a0acff106cd089de5c61b644",
5
+ "runnerSourceDigest": "8a8102e62952b245c0967f14aaa8849d4603ca7f241c97e1e38bcb328edf7067",
6
6
  "dependencies": {
7
7
  "@babel/parser": "7.29.2",
8
8
  "estree-walker": "^3.0.3",
9
- "magic-string": "^0.30.0"
9
+ "magic-string": "^0.30.0",
10
+ "zod": "^4.5.4"
10
11
  },
11
12
  "devDependencies": {
12
13
  "@repo/test-utils": "workspace:*",
@@ -34,9 +35,9 @@
34
35
  },
35
36
  "scripts": {
36
37
  "build": "tsdown",
37
- "test": "bun test --isolate",
38
+ "test": "bun --bun vitest run",
38
39
  "publish:prod": "bun run build && npm publish --access public",
39
- "test:coverage": "bun test --isolate --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage"
40
+ "test:coverage": "bun --bun vitest run --coverage"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "vite": ">=4.0.0"