diff --git a/template/client/vite-plugin-react-source-loc.ts b/template/client/vite-plugin-react-source-loc.ts new file mode 100644 index 000000000..a099b7731 --- /dev/null +++ b/template/client/vite-plugin-react-source-loc.ts @@ -0,0 +1,70 @@ +import { parse } from '@babel/parser'; +import _traverse from '@babel/traverse'; +import MagicString from 'magic-string'; +import path from 'node:path'; +import type { Plugin } from 'vite'; + +// @babel/traverse has different CJS/ESM default export handling +const traverse = (_traverse as unknown as { default: typeof _traverse }).default ?? _traverse; + +export default function reactSourceLoc(): Plugin { + let projectRoot: string; + + return { + name: 'react-source-loc', + enforce: 'pre', + + configResolved(config) { + // Vite root is client/; project root is one level up + projectRoot = path.resolve(config.root, '..'); + }, + + transform(code, id) { + if (!/\.[jt]sx$/.test(id)) return; + if (id.includes('node_modules')) return; + + const ast = parse(code, { + sourceType: 'module', + plugins: ['jsx', 'typescript'], + }); + + const s = new MagicString(code); + const relPath = path.relative(projectRoot, id); + + traverse(ast, { + JSXOpeningElement(nodePath) { + const name = nodePath.node.name; + + // Skip fragments + if (name.type === 'JSXIdentifier' && name.name === '') return; + + // Skip React components (uppercase or member expressions) + if (name.type === 'JSXMemberExpression') return; + if (name.type === 'JSXIdentifier' && /^[A-Z]/.test(name.name)) return; + + const loc = nodePath.node.loc; + if (!loc) return; + + // Skip if data-source already exists + const alreadyHas = nodePath.node.attributes.some( + (attr) => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'data-source' + ); + if (alreadyHas) return; + + const value = `${relPath}:${loc.start.line}:${loc.start.column}`; + const attr = ` data-source="${value}"`; + + // Find the tag name end position to insert the attribute + const nameNode = nodePath.node.name; + const insertPos = nameNode.end!; + s.appendLeft(insertPos, attr); + }, + }); + + return { + code: s.toString(), + map: s.generateMap({ hires: true }), + }; + }, + }; +} diff --git a/template/client/vite.config.ts b/template/client/vite.config.ts index 6a2998cd1..76649884f 100644 --- a/template/client/vite.config.ts +++ b/template/client/vite.config.ts @@ -2,20 +2,19 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import path from 'node:path'; +import reactSourceLoc from './vite-plugin-react-source-loc'; // https://vite.dev/config/ export default defineConfig({ root: __dirname, - plugins: [ - react(), - tailwindcss(), - ], + plugins: [react(), reactSourceLoc(), tailwindcss()], server: { middlewareMode: true, }, build: { outDir: path.resolve(__dirname, './dist'), emptyOutDir: true, + sourcemap: true, }, optimizeDeps: { include: ['react', 'react-dom', 'react/jsx-dev-runtime', 'react/jsx-runtime', 'recharts'], diff --git a/template/package-lock.json b/template/package-lock.json index 2356957bb..c97f47ff9 100644 --- a/template/package-lock.json +++ b/template/package-lock.json @@ -28,6 +28,8 @@ }, "devDependencies": { "@ast-grep/napi": "0.37.0", + "@babel/parser": "7.29.2", + "@babel/traverse": "7.29.0", "@eslint/compat": "2.0.0", "@eslint/js": "9.39.1", "@playwright/test": "1.57.0", @@ -46,6 +48,7 @@ "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-react-refresh": "0.4.24", + "magic-string": "0.30.21", "prettier": "3.6.2", "sharp": "0.34.5", "tailwindcss": "4.0.14", @@ -441,9 +444,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://npm-proxy.dev.databricks.com/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/template/package.json b/template/package.json index f6269cde3..ba5b6c939 100644 --- a/template/package.json +++ b/template/package.json @@ -50,6 +50,8 @@ }, "devDependencies": { "@ast-grep/napi": "0.37.0", + "@babel/parser": "7.29.2", + "@babel/traverse": "7.29.0", "@eslint/compat": "2.0.0", "@eslint/js": "9.39.1", "@playwright/test": "1.57.0", @@ -68,6 +70,7 @@ "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-react-refresh": "0.4.24", + "magic-string": "0.30.21", "prettier": "3.6.2", "sharp": "0.34.5", "tailwindcss": "4.0.14",