/** * Injects data-webild-source="relative/path.tsx:line:column" on every JSX opening element * for visual-edit targeting (stable source location in generated DOM). */ const path = require('path'); module.exports = function babelPluginWebildSource(api) { const { types: t } = api; return { name: 'babel-plugin-webild-source', visitor: { JSXOpeningElement(openPath, state) { const opts = state.opts || {}; const rootDir = opts.rootDir ? path.resolve(opts.rootDir) : process.cwd(); const nameNode = openPath.node.name; if (t.isJSXIdentifier(nameNode) && nameNode.name === 'Fragment') { return; } if (t.isJSXMemberExpression(nameNode)) { if ( t.isJSXIdentifier(nameNode.property) && nameNode.property.name === 'Fragment' ) { return; } } const hasAttr = openPath.node.attributes.some((attr) => { if (!t.isJSXAttribute(attr)) return false; if (!t.isJSXIdentifier(attr.name)) return false; return attr.name.name === 'data-webild-source'; }); if (hasAttr) return; const file = openPath.hub.file; const filename = file.opts.filename || file.opts.sourceFileName; if (!filename || filename.includes('node_modules')) return; let rel = path.relative(rootDir, filename); if (rel.startsWith('..')) return; rel = rel.split(path.sep).join('/'); const loc = openPath.node.loc?.start; if (!loc) return; const line = loc.line; const column = loc.column + 1; const sourceValue = `${rel}:${line}:${column}`; openPath.node.attributes.push( t.jsxAttribute( t.jsxIdentifier('data-webild-source'), t.stringLiteral(sourceValue), ), ); }, }, }; };