@runner-ai/vite-uuid-tagger 0.0.12 → 0.0.14

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,23 +1,10 @@
1
- import * as _$magic_string0 from "magic-string";
1
+ import { Plugin } from "vite";
2
2
 
3
3
  //#region src/plugin.d.ts
4
4
  interface UuidTaggerOptions {
5
5
  projectRoot: string;
6
6
  }
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
- };
7
+ declare function uuidTagger(options: UuidTaggerOptions): Plugin;
21
8
  //#endregion
22
9
  export { type UuidTaggerOptions, uuidTagger };
23
10
  //# sourceMappingURL=index.d.ts.map
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';\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":";;;;;;;;;;;;AAQA,MAAM,wBAAwB,CAAC,YAAY,iBAAiB;;;;AAK5D,MAAM,mBAAmB,IAAI,IAAI;CAAC;CAAQ;CAAQ;CAAO;CAAM,CAAC;;;;;AAyGhE,SAAgB,eAAe,UAA2B;AACxD,QAAO,SAAS,SAAS,eAAe,IAAI,CAAC,iBAAiB,IAAI,KAAK,QAAQ,SAAS,CAAC;;;;;;AAO3F,SAAgB,eAAe,MAAwC;AAErE,KAAI,KAAK,KAAK,SAAS,gBACrB,QAAO,KAAK,KAAK;AAInB,KAAI,KAAK,KAAK,SAAS,sBACrB,QAAO,GAAG,KAAK,KAAK,OAAO,KAAK,GAAG,KAAK,KAAK,SAAS;AAIxD,QAAO;;AAGT,MAAM,2BAA2B,EAAE,OAAO;CACxC,MAAM,EAAE,QAAQ,eAAe;CAC/B,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,gBAAgB,EAAE,CAAC;CACpD,OAAO,EAAE,OAAO;EAAE,MAAM,EAAE,QAAQ,gBAAgB;EAAE,OAAO,EAAE,QAAQ;EAAE,CAAC;CACzE,CAAC;AAEF,MAAa,kBAAkB,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,iBAAiB;AAC1G,MAAK,MAAM,aAAa,YAAY;EAClC,MAAM,QAAQ,yBAAyB,UAAU,UAAU;AAC3D,MAAI,MAAM,QACR,QAAO,MAAM,KAAK,MAAM;;AAG5B,QAAO;EACP;;;;;AAMF,SAAgB,iBAAiB,MAAqE;AACpG,KAAI,KAAK,SAAS,aAChB,QAAO;CAGT,MAAM,iBAAiB,KAAK;AAC5B,KAAI,CAAC,eACH,QAAO;CAGT,MAAM,cAAc,eAAe,eAAe;AAClD,KAAI,CAAC,eAAe,sBAAsB,SAAS,YAAY,CAC7D,QAAO;AAGT,QAAO;;;;;;AAOT,SAAgB,mBAAmB,YAAuD;AACxF,KAAI,CAAC,WACH,QAAO;AAIT,KAAI,WAAW,SAAS,qBACtB,QAAO;AAIT,KAAI;EAAC;EAAkB;EAAkB;EAAiB;EAAc,CAAC,SAAS,WAAW,KAAK,CAChG,QAAO;AAIT,KAAI,WAAW,SAAS,mBAEtB,QADY,WACD,WAAW,OAAO,SAAS;AACpC,MAAI,KAAK,SAAS,gBAChB,QAAO;AAET,MAAI,KAAK,SAAS,kBAAkB;AAClC,OAAI,KAAK,SACP,QAAO;AAET,UAAO,mBAAmB,KAAK,MAAM;;AAEvC,SAAO;GACP;AAIJ,KAAI,WAAW,SAAS,kBAEtB,QADY,WACD,SAAS,OAAO,SAAS;AAClC,MAAI,SAAS,KACX,QAAO;AAET,MAAI,KAAK,SAAS,gBAChB,QAAO;AAET,SAAO,mBAAmB,KAAK;GAC/B;AAIJ,QAAO;;;;;;AAOT,SAAgB,wBAAwB,YAAiC;AACvE,KAAI,CAAC,WAAW,YAAY,WAAW,SAAS,WAAW,EACzD,QAAO;AAaT,KAAI,CAVqB,WAAW,SAAS,OAAO,UAAU;AAC5D,MAAI,MAAM,SAAS,UACjB,QAAO;AAET,MAAI,MAAM,SAAS,yBACjB,QAAO,mBAAoB,MAAiC,WAAW;AAEzE,SAAO;GACP,CAGA,QAAO;AAIT,QAAO,WAAW,SAAS,MAAM,UAAU;AACzC,MAAI,MAAM,SAAS,UACjB,QAAQ,MAAkB,MAAM,MAAM,CAAC,SAAS;AAElD,MAAI,MAAM,SAAS,yBACjB,QAAQ,MAAiC,WAAW,SAAS;AAE/D,SAAO;GACP;;;;;;AAOJ,SAAgB,iBAAiB,YAAiC;CAEhE,MAAM,oBACJ,WAAW,YACX,WAAW,SAAS,SAAS,KAC7B,WAAW,SAAS,OAAO,UAAU;AACnC,MAAI,MAAM,SAAS,UACjB,QAAO;AAET,MAAI,MAAM,SAAS,yBACjB,QAAO,mBAAoB,MAAiC,WAAW;AAEzE,SAAO;GACP;CAGJ,MAAM,qBAAqB,WAAW,UAAU,MAAM,UAAU;AAC9D,MAAI,MAAM,SAAS,UACjB,QAAQ,MAAkB,MAAM,MAAM,CAAC,SAAS;AAElD,MAAI,MAAM,SAAS,yBACjB,QAAQ,MAAiC,WAAW,SAAS;AAE/D,SAAO;GACP;AAEF,KAAI,qBAAqB,mBACvB,QAAO;AAIT,KAAI,WAAW,eAAe,cAAc,WAAW,eAAe,WAAW,SAAS;MAC5D,WAAW,eAAe,WAAW,MAAM,SAAS;AAC9E,OAAI,KAAK,SAAS,qBAChB,QAAO;AAGT,OAAI,KAAK,SAAS,KAAK,MAAM,SAAS,yBACpC,QAAO,mBAAoB,KAAK,MAAiC,WAAW;AAG9E,OAAI,KAAK,SAAS,KAAK,MAAM,SAAS,gBACpC,QAAO;AAGT,UAAO;IACP,CAGA,QAAO;;AAIX,QAAO;;;;AChTT,MAAM,iBAAiB,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,SAAS,CAAC;AAExD,MAAM,qBAAqB,EAAE,OAAO;CAClC,UAAU,EAAE,QAAQ;CACpB,MAAM,EAAE,QAAQ;CAChB,QAAQ,EAAE,QAAQ;CAClB,aAAa,EAAE,QAAQ;CACxB,CAAC;AAEF,SAAgB,YAAY,UAAkB,EAAE,MAAM,QAAQ,eAAwC;CACpG,MAAM,OAAO,WAAW,SAAS,CAAC,OAAO,GAAG,SAAS,GAAG,KAAK,GAAG,OAAO,GAAG,cAAc,CAAC,OAAO,MAAM;AACtG,QAAO;EAAC,KAAK,MAAM,GAAG,EAAE;EAAE,KAAK,MAAM,GAAG,GAAG;EAAE,KAAK,MAAM,IAAI,GAAG;EAAE,KAAK,MAAM,IAAI,GAAG;EAAE,KAAK,MAAM,IAAI,GAAG;EAAC,CAAC,KAAK,IAAI;;AAGpH,SAAS,mBAAmB,MAAuC;AACjE,KAAI;EACF,MAAM,QAAQ,eAAe,UAAU,KAAK,MAAM,KAAK,CAAC;AACxD,MAAI,CAAC,MAAM,QACT,QAAO,EAAE;AAEX,SAAO,OAAO,QAAQ,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,WAAoC;GACpF,MAAM,UAAU,mBAAmB,UAAU,MAAM;AACnD,UAAO,QAAQ,UAAU,CAAC,CAAC,MAAM,QAAQ,KAAK,CAAC,GAAG,EAAE;IACpD;SACI;AACN,SAAO,EAAE;;;AAIb,SAAS,YAAY,UAAmC,MAA4B;AAClF,QACE,aAAa,KAAA,KACb,SAAS,aAAa,KAAK,YAC3B,SAAS,SAAS,KAAK,QACvB,SAAS,WAAW,KAAK,UACzB,SAAS,gBAAgB,KAAK;;AAIlC,SAAS,YAAY,UAAwB,MAA6B;AACxE,QAAO,SAAS,SAAS,KAAK,QAAQ,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,aAAa,YAAY,SAAS,IAAI,KAAK,EAAE,QAAQ,CAAC;;AAGtH,IAAa,qBAAb,MAAgC;CAC9B;CACA;CACA,wBAAyB,IAAI,KAA2B;CAExD,YAAY,aAAqB;AAC/B,OAAK,cAAc;AACnB,OAAK,cAAc,KAAK,aAAa,UAAU,qBAAqB;EACpE,MAAM,QAAQ,KAAK,eAAe;AAClC,MAAI,UAAU,KACZ;AAEF,OAAK,4BAA4B,MAAM;AACvC,MAAI,KAAK,WAAW,KAAK,MACvB,MAAK,cAAc;;CAIvB,YAAY,UAAkB,UAA0C;EACtE,MAAM,OAAqB,IAAI,IAC7B,SAAS,KAAK,EAAE,IAAI,MAAM,QAAQ,kBAAyC,CACzE,IACA;GAAE;GAAU;GAAM;GAAQ;GAAa,CACxC,CAAC,CACH;AACD,MAAI,YAAY,KAAK,MAAM,IAAI,SAAS,oBAAI,IAAI,KAA0B,EAAE,KAAK,CAC/E;AAEF,MAAI,KAAK,SAAS,EAChB,MAAK,MAAM,OAAO,SAAS;MAE3B,MAAK,MAAM,IAAI,UAAU,KAAK;AAEhC,OAAK,cAAc;;CAGrB,gBAAuC;AACrC,MAAI;AACF,UAAO,WAAW,KAAK,YAAY,GAAG,aAAa,KAAK,aAAa,OAAO,GAAG;UACzE;AACN,UAAO;;;CAIX,4BAAoC,OAAqB;EACvD,MAAM,0BAAU,IAAI,KAAuC;AAC3D,OAAK,MAAM,CAAC,MAAM,YAAY,mBAAmB,MAAM,EAAE;AACvD,OAAI,CAAC,WAAW,KAAK,KAAK,aAAa,QAAQ,SAAS,CAAC,CACvD;GAEF,MAAM,UAAU,QAAQ,IAAI,QAAQ,SAAS,oBAAI,IAAI,KAA0B;AAC/E,WAAQ,IAAI,MAAM,QAAQ;AAC1B,WAAQ,IAAI,QAAQ,UAAU,QAAQ;;AAExC,OAAK,MAAM,CAAC,UAAU,YAAY,QAChC,MAAK,MAAM,IAAI,UAAU,QAAQ;;CAIrC,YAA4B;EAC1B,MAAM,QAAQ,CAAC,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC,SAAS,YAC9C,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,aAAa,GAAG,KAAK,UAAU,KAAK,CAAC,GAAG,KAAK,UAAU,QAAQ,GAAG,CAC5F;AACD,SAAO,MAAM,WAAW,IAAI,OAAO,MAAM,MAAM,KAAK,MAAM,CAAC;;CAG7D,eAA6B;EAG3B,MAAM,UAAU,GAAG,KAAK,YAAY,GAAG,QAAQ,IAAI;AACnD,MAAI;GACF,MAAM,MAAM,QAAQ,KAAK,YAAY;AACrC,OAAI,CAAC,WAAW,IAAI,CAClB,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AAMrC,iBAAc,SAAS,KAAK,WAAW,CAAC;AACxC,cAAW,SAAS,KAAK,YAAY;UAC/B;AAGN,OAAI;AACF,QAAI,WAAW,QAAQ,CACrB,YAAW,QAAQ;WAEf;;;;;;AClId,SAAgB,WAAW,SAA4B;CACrD,MAAM,EAAE,gBAAgB;CACxB,MAAM,cAAc,IAAI,mBAAmB,YAAY;AAEvD,QAAO;EACL,MAAM;EACN,SAAS;EAET,MAAM,UAAU,MAAc,IAAY;AACxC,OAAI,eAAe,GAAG,CACpB,QAAO;GAGT,MAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,OAAI,QAAQ,UAAU,QAAQ,OAC5B,QAAO;GAGT,MAAM,eAAe,KAAK,SAAS,aAAa,GAAG;GACnD,MAAM,EAAE,SAAS,MAAM,OAAO;AAE9B,OAAI;IACF,MAAM,MAAM,MAAM,MAAM;KACtB,YAAY;KACZ,SAAS,CAAC,OAAO,aAAa;KAC/B,CAAC;IAEF,MAAM,QAAQ,IAAI,YAAY,KAAK;IACnC,MAAM,WAA4B,EAAE;AAGpC,SAAK,KAAY,EACf,MAAM,MAAM;KAEV,MAAM,IAAI;AACV,SAAI,CAAC,iBAAiB,EAAW,CAC/B;KAGF,MAAM,iBAAiB,EAAE;KAIzB,MAAM,cAAc,eAAe,eAAwB;AAC3D,SAAI,CAAC,YACH;KAGF,MAAM,EAAE,OAAO,GAAG,SAAS,MAAM,eAAe,KAAK,SAAS,EAAE;KAChE,MAAM,WAA4B;MAAE;MAAM;MAAQ;MAAa;KAC/D,MAAM,QAAQ,gBAAgB,UAAU,EAAE,eAAe;KACzD,MAAM,YAAY,MAAM,UAAU,MAAM,OAAO;KAC/C,MAAM,YAAY,aAAa,YAAY,cAAc,SAAS;AAClE,cAAS,KAAK;MAAE,IAAI;MAAW,GAAG;MAAU,CAAC;AAE7C,SAAI,cAAc,KAChB,OAAM,WAAW,eAAe,KAAK,OAAO,GAAG,mBAAmB,UAAU,GAAG;AAGjF,SAAI,wBAAwB,EAAW,CACrC,OAAM,WAAW,eAAe,KAAK,OAAO,GAAG,yBAAyB;AAG1E,SAAI,iBAAiB,EAAW,CAC9B,OAAM,WAAW,eAAe,KAAK,OAAO,GAAG,wBAAwB;OAG5E,CAAC;AAEF,gBAAY,YAAY,cAAc,SAAS;AAE/C,QAAI,SAAS,SAAS,EACpB,QAAO;KACL,MAAM,MAAM,UAAU;KACtB,KAAK,MAAM,YAAY,EAAE,OAAO,MAAM,CAAC;KACxC;AAEH,WAAO;WACD;AACN,WAAO;;;EAIX,YAAY,IAAY,EAAE,SAA4B;AACpD,OAAI,UAAU,SACZ,aAAY,YAAY,KAAK,SAAS,aAAa,GAAG,EAAE,EAAE,CAAC;;EAGhE"}
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.ZodType<string | null, { attributes: unknown[] }> = z\n .object({ attributes: z.array(z.unknown()) })\n .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): 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 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,8 +1,8 @@
1
1
  {
2
2
  "name": "@runner-ai/vite-uuid-tagger",
3
- "version": "0.0.12",
4
- "runnerSourceCommit": "b50a41c5c5a59a90ad3db29c638ec210da4b2d09",
5
- "runnerSourceDigest": "bc5beaa443d6c0ae2d9bf0dca135b3cd7990656fa868243c96a5798384a1998b",
3
+ "version": "0.0.14",
4
+ "runnerSourceCommit": "dd8da1a180687a4e5c37ebd5deb5ed322dcb19b2",
5
+ "runnerSourceDigest": "89e12d05c8ee1c2d83d015676fa43b9e9493edf72ba465cb02efe75c3e812d00",
6
6
  "dependencies": {
7
7
  "@babel/parser": "7.29.2",
8
8
  "estree-walker": "^3.0.3",
@@ -35,9 +35,9 @@
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsdown",
38
- "test": "bun --bun vitest run",
38
+ "test": "bun --bun vp test run",
39
39
  "publish:prod": "bun run build && npm publish --access public",
40
- "test:coverage": "bun --bun vitest run --coverage"
40
+ "test:coverage": "bun --bun vp test run --coverage"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "vite": ">=4.0.0"