redev-vite-plugin 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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/package.json +50 -0
- package/src/html-injector.js +11 -0
- package/src/index.js +60 -0
- package/src/jsx-transformer.js +125 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 abhishek4544
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# redev-vite-plugin
|
|
2
|
+
|
|
3
|
+
Vite plugin for [Redev](../README.md). Injects `data-redev-*` attributes into every DOM-level JSX element at dev-server time so the browser overlay can identify which source file and component was clicked.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Parses every `.jsx` / `.tsx` file in your project with Babel
|
|
8
|
+
- Adds four attributes to every lowercase JSX element (`<div>`, `<button>`, `<h1>`, etc.):
|
|
9
|
+
- `data-redev-file` — path relative to the Vite project root
|
|
10
|
+
- `data-redev-line` — 1-indexed line number in the source file
|
|
11
|
+
- `data-redev-column` — 0-indexed column
|
|
12
|
+
- `data-redev-component` — name of the enclosing React component (function or class)
|
|
13
|
+
- Injects `<script src=".../redev/overlay.js">` into `index.html`
|
|
14
|
+
- Only runs during `vite dev` — production builds are untouched
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -D redev-vite-plugin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// vite.config.ts
|
|
26
|
+
import { defineConfig } from 'vite';
|
|
27
|
+
import react from '@vitejs/plugin-react';
|
|
28
|
+
import redev from 'redev-vite-plugin';
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [
|
|
32
|
+
redev({ backendUrl: 'http://localhost:5050' }),
|
|
33
|
+
react(),
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`redev()` must run **before** `@vitejs/plugin-react` — `enforce: 'pre'` is set on the plugin, so Vite orders it correctly, but if you have other pre-plugins, keep Redev first.
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
| Option | Default | What it does |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `backendUrl` | `http://localhost:5050` | Where the Redev backend serves the overlay script. |
|
|
45
|
+
| `enabled` | `true` | Set false to disable the plugin without removing it from the config. |
|
|
46
|
+
| `include` | `null` | (Reserved for future use — currently transforms all `.jsx`/`.tsx`.) |
|
|
47
|
+
| `exclude` | `['**/node_modules/**']` | (Reserved for future use — `node_modules` is always skipped.) |
|
|
48
|
+
|
|
49
|
+
## What gets skipped
|
|
50
|
+
|
|
51
|
+
- Uppercase JSX elements (`<Button>`, `<MyComponent>`) — attributes go on the DOM element these render, not on the component tag itself.
|
|
52
|
+
- Fragments (`<>...</>` and `<React.Fragment>`).
|
|
53
|
+
- Files in `node_modules`.
|
|
54
|
+
- Elements that already have `data-redev-file` (idempotent).
|
|
55
|
+
- Non-JSX files.
|
|
56
|
+
|
|
57
|
+
## Verifying it works
|
|
58
|
+
|
|
59
|
+
Start Vite, open the browser dev tools, and inspect any element. You should see:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<button
|
|
63
|
+
class="btn"
|
|
64
|
+
data-redev-file="src/components/Button.tsx"
|
|
65
|
+
data-redev-line="42"
|
|
66
|
+
data-redev-column="4"
|
|
67
|
+
data-redev-component="Button"
|
|
68
|
+
>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the attributes are missing, check that the plugin appears before `react()` in your config and that the file being inspected is a `.tsx`/`.jsx` file (not compiled JS from a library).
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redev-vite-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vite plugin that injects data-redev-* attributes so DOM clicks map back to source files. Powers redev-cli.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vite",
|
|
7
|
+
"vite-plugin",
|
|
8
|
+
"redev",
|
|
9
|
+
"developer-tools",
|
|
10
|
+
"click-to-edit",
|
|
11
|
+
"react",
|
|
12
|
+
"jsx"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "src/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"author": "abhishek4544",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"homepage": "https://github.com/abhishek4544/Redev#readme",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/abhishek4544/Redev.git",
|
|
33
|
+
"directory": "plugin"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/abhishek4544/Redev/issues"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vite": "^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@babel/parser": "^7.24.0",
|
|
46
|
+
"@babel/traverse": "^7.24.0",
|
|
47
|
+
"@babel/generator": "^7.24.0",
|
|
48
|
+
"@babel/types": "^7.24.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function buildOverlayTag(backendUrl) {
|
|
2
|
+
return `<script src="${backendUrl}/redev/overlay.js" async></script>`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function injectOverlayIntoHtml(html, backendUrl) {
|
|
6
|
+
const tag = buildOverlayTag(backendUrl);
|
|
7
|
+
if (html.includes('/redev/overlay.js')) return html;
|
|
8
|
+
if (html.includes('</head>')) return html.replace('</head>', ` ${tag}\n</head>`);
|
|
9
|
+
if (html.includes('</body>')) return html.replace('</body>', ` ${tag}\n</body>`);
|
|
10
|
+
return html + `\n${tag}\n`;
|
|
11
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { transformJsx } from './jsx-transformer.js';
|
|
3
|
+
import { injectOverlayIntoHtml } from './html-injector.js';
|
|
4
|
+
|
|
5
|
+
const JSX_EXTENSIONS = new Set(['.jsx', '.tsx']);
|
|
6
|
+
const DEFAULT_BACKEND = 'http://localhost:5050';
|
|
7
|
+
|
|
8
|
+
export default function redevVitePlugin(userOptions = {}) {
|
|
9
|
+
const options = {
|
|
10
|
+
backendUrl: userOptions.backendUrl || DEFAULT_BACKEND,
|
|
11
|
+
include: userOptions.include || null,
|
|
12
|
+
exclude: userOptions.exclude || ['**/node_modules/**'],
|
|
13
|
+
enabled: userOptions.enabled !== false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let projectRoot = process.cwd();
|
|
17
|
+
|
|
18
|
+
function shouldTransform(id) {
|
|
19
|
+
if (!options.enabled) return false;
|
|
20
|
+
const clean = id.split('?')[0];
|
|
21
|
+
const ext = path.extname(clean);
|
|
22
|
+
if (!JSX_EXTENSIONS.has(ext)) return false;
|
|
23
|
+
if (clean.includes('node_modules')) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toRelativePath(id) {
|
|
28
|
+
const clean = id.split('?')[0];
|
|
29
|
+
const rel = path.relative(projectRoot, clean);
|
|
30
|
+
return rel.split(path.sep).join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
name: 'redev',
|
|
35
|
+
enforce: 'pre',
|
|
36
|
+
apply: 'serve',
|
|
37
|
+
|
|
38
|
+
configResolved(config) {
|
|
39
|
+
projectRoot = config.root;
|
|
40
|
+
if (options.enabled) {
|
|
41
|
+
console.log(`[redev] enabled — backend at ${options.backendUrl}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
transform(code, id) {
|
|
46
|
+
if (!shouldTransform(id)) return null;
|
|
47
|
+
const relativePath = toRelativePath(id);
|
|
48
|
+
const result = transformJsx(code, relativePath);
|
|
49
|
+
if (!result) return null;
|
|
50
|
+
return result;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
transformIndexHtml(html) {
|
|
54
|
+
if (!options.enabled) return html;
|
|
55
|
+
return injectOverlayIntoHtml(html, options.backendUrl);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { redevVitePlugin };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
import _generate from '@babel/generator';
|
|
4
|
+
import * as t from '@babel/types';
|
|
5
|
+
|
|
6
|
+
const traverse = _traverse.default || _traverse;
|
|
7
|
+
const generate = _generate.default || _generate;
|
|
8
|
+
|
|
9
|
+
const PARSER_OPTIONS = {
|
|
10
|
+
sourceType: 'module',
|
|
11
|
+
plugins: [
|
|
12
|
+
'jsx',
|
|
13
|
+
'typescript',
|
|
14
|
+
'decorators-legacy',
|
|
15
|
+
'classProperties',
|
|
16
|
+
'objectRestSpread',
|
|
17
|
+
'asyncGenerators',
|
|
18
|
+
'topLevelAwait',
|
|
19
|
+
'importMeta',
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isLowercaseElement(name) {
|
|
24
|
+
if (name.type !== 'JSXIdentifier') return false;
|
|
25
|
+
const first = name.name.charAt(0);
|
|
26
|
+
return first === first.toLowerCase() && first !== first.toUpperCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function elementDisplayName(name) {
|
|
30
|
+
if (name.type === 'JSXIdentifier') return name.name;
|
|
31
|
+
if (name.type === 'JSXMemberExpression') {
|
|
32
|
+
const parts = [];
|
|
33
|
+
let current = name;
|
|
34
|
+
while (current.type === 'JSXMemberExpression') {
|
|
35
|
+
parts.unshift(current.property.name);
|
|
36
|
+
current = current.object;
|
|
37
|
+
}
|
|
38
|
+
if (current.type === 'JSXIdentifier') parts.unshift(current.name);
|
|
39
|
+
return parts.join('.');
|
|
40
|
+
}
|
|
41
|
+
return 'unknown';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findEnclosingComponent(path) {
|
|
45
|
+
let current = path.parentPath;
|
|
46
|
+
while (current) {
|
|
47
|
+
const node = current.node;
|
|
48
|
+
if (t.isFunctionDeclaration(node) && node.id) return node.id.name;
|
|
49
|
+
if (t.isClassDeclaration(node) && node.id) return node.id.name;
|
|
50
|
+
if (t.isVariableDeclarator(node) && t.isIdentifier(node.id)) {
|
|
51
|
+
const init = node.init;
|
|
52
|
+
if (
|
|
53
|
+
init &&
|
|
54
|
+
(t.isArrowFunctionExpression(init) ||
|
|
55
|
+
t.isFunctionExpression(init) ||
|
|
56
|
+
t.isCallExpression(init))
|
|
57
|
+
) {
|
|
58
|
+
return node.id.name;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (t.isFunctionExpression(node) && node.id) return node.id.name;
|
|
62
|
+
if (t.isExportDefaultDeclaration(node)) {
|
|
63
|
+
const decl = node.declaration;
|
|
64
|
+
if (t.isIdentifier(decl)) return decl.name;
|
|
65
|
+
}
|
|
66
|
+
current = current.parentPath;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasRedevAttr(attributes, name) {
|
|
72
|
+
return attributes.some(
|
|
73
|
+
(attr) =>
|
|
74
|
+
t.isJSXAttribute(attr) &&
|
|
75
|
+
t.isJSXIdentifier(attr.name) &&
|
|
76
|
+
attr.name.name === name,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeStringAttr(name, value) {
|
|
81
|
+
return t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(String(value)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function transformJsx(code, relativePath) {
|
|
85
|
+
let ast;
|
|
86
|
+
try {
|
|
87
|
+
ast = parse(code, PARSER_OPTIONS);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let modified = false;
|
|
93
|
+
|
|
94
|
+
traverse(ast, {
|
|
95
|
+
JSXOpeningElement(path) {
|
|
96
|
+
const node = path.node;
|
|
97
|
+
const name = node.name;
|
|
98
|
+
|
|
99
|
+
if (!isLowercaseElement(name)) return;
|
|
100
|
+
if (hasRedevAttr(node.attributes, 'data-redev-file')) return;
|
|
101
|
+
|
|
102
|
+
const line = node.loc && node.loc.start ? node.loc.start.line : 0;
|
|
103
|
+
const column = node.loc && node.loc.start ? node.loc.start.column : 0;
|
|
104
|
+
const componentName = findEnclosingComponent(path) || elementDisplayName(name);
|
|
105
|
+
|
|
106
|
+
node.attributes.push(makeStringAttr('data-redev-file', relativePath));
|
|
107
|
+
node.attributes.push(makeStringAttr('data-redev-line', line));
|
|
108
|
+
node.attributes.push(makeStringAttr('data-redev-column', column));
|
|
109
|
+
node.attributes.push(makeStringAttr('data-redev-component', componentName));
|
|
110
|
+
|
|
111
|
+
modified = true;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!modified) return null;
|
|
116
|
+
|
|
117
|
+
const output = generate(ast, {
|
|
118
|
+
retainLines: true,
|
|
119
|
+
compact: false,
|
|
120
|
+
sourceMaps: true,
|
|
121
|
+
sourceFileName: relativePath,
|
|
122
|
+
}, code);
|
|
123
|
+
|
|
124
|
+
return { code: output.code, map: output.map };
|
|
125
|
+
}
|