nextjs-ide-helper 1.4.1 → 1.5.2
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/README.md +42 -13
- package/lib/loader.js +38 -14
- package/lib/withIdeButton.js +62 -14
- package/lib/withIdeButton.tsx +68 -15
- package/package.json +1 -1
- package/src/loader.js +38 -14
- package/src/withIdeButton.tsx +68 -15
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ A Next.js plugin that automatically adds IDE buttons to React components in deve
|
|
|
8
8
|
- 🎯 **Smart Detection**: Only processes components in specified directories
|
|
9
9
|
- 🔧 **Zero Configuration**: Works out of the box with sensible defaults (Cursor as default IDE)
|
|
10
10
|
- 🏎️ **Development Only**: Only active in development mode, no production overhead
|
|
11
|
-
- 🎨 **Non-intrusive**:
|
|
11
|
+
- 🎨 **Non-intrusive**: Minimal blue dot indicators that don't disrupt your layout
|
|
12
|
+
- 👁️ **Toggle Visibility**: Floating button to show/hide all IDE dots instantly
|
|
12
13
|
- ⚡ **Hydration Safe**: No SSR/client hydration mismatches
|
|
13
14
|
- 📱 **TypeScript Support**: Full TypeScript definitions included
|
|
14
15
|
- 🔌 **Multi-IDE Support**: Supports Cursor, VS Code, WebStorm, and Atom
|
|
@@ -18,8 +19,7 @@ A Next.js plugin that automatically adds IDE buttons to React components in deve
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
|
-
|
|
22
|
-
# or
|
|
22
|
+
npm install nextjs-ide-helper
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
@@ -29,7 +29,7 @@ A Next.js plugin that automatically adds IDE buttons to React components in deve
|
|
|
29
29
|
Add the plugin to your `next.config.js`:
|
|
30
30
|
|
|
31
31
|
```javascript
|
|
32
|
-
const withIdeHelper = require('nextjs-ide-
|
|
32
|
+
const withIdeHelper = require('nextjs-ide-helper');
|
|
33
33
|
|
|
34
34
|
/** @type {import('next').NextConfig} */
|
|
35
35
|
const nextConfig = {
|
|
@@ -43,12 +43,30 @@ module.exports = withIdeHelper()(nextConfig);
|
|
|
43
43
|
|
|
44
44
|
All React components in your `src/components` directory will automatically get IDE buttons in development mode (defaults to Cursor IDE).
|
|
45
45
|
|
|
46
|
+
## User Interface
|
|
47
|
+
|
|
48
|
+
### IDE Indicator Dots
|
|
49
|
+
|
|
50
|
+
Each wrapped component displays a small **blue dot** (10x10px) in the top-right corner:
|
|
51
|
+
- **Click** the dot to open the component's source file in your IDE
|
|
52
|
+
- **Hover** to see the file path tooltip
|
|
53
|
+
- The dots are subtle (60% opacity) and brighten on hover
|
|
54
|
+
|
|
55
|
+
### Toggle Button
|
|
56
|
+
|
|
57
|
+
A **floating toggle button** appears in the bottom-right corner of your page:
|
|
58
|
+
- **Blue** = IDE dots are visible
|
|
59
|
+
- **Gray** = IDE dots are hidden
|
|
60
|
+
- **Click** to toggle all dots on/off instantly
|
|
61
|
+
|
|
62
|
+
This lets you quickly hide the dots when you want an unobstructed view of your app, then bring them back when needed.
|
|
63
|
+
|
|
46
64
|
## Configuration
|
|
47
65
|
|
|
48
66
|
You can customize the plugin behavior:
|
|
49
67
|
|
|
50
68
|
```javascript
|
|
51
|
-
const withIdeHelper = require('nextjs-ide-
|
|
69
|
+
const withIdeHelper = require('nextjs-ide-helper');
|
|
52
70
|
|
|
53
71
|
const nextConfig = {
|
|
54
72
|
// your existing config
|
|
@@ -57,7 +75,7 @@ const nextConfig = {
|
|
|
57
75
|
module.exports = withIdeHelper({
|
|
58
76
|
componentPaths: ['src/components', 'components', 'src/ui'], // directories to scan (supports glob patterns)
|
|
59
77
|
projectRoot: process.cwd(), // project root directory
|
|
60
|
-
importPath: 'nextjs-ide-
|
|
78
|
+
importPath: 'nextjs-ide-helper/withIdeButton', // import path for the HOC
|
|
61
79
|
enabled: process.env.NODE_ENV === 'development', // enable/disable
|
|
62
80
|
ideType: 'cursor' // IDE to use: 'cursor', 'vscode', 'webstorm', 'atom'
|
|
63
81
|
})(nextConfig);
|
|
@@ -69,7 +87,7 @@ module.exports = withIdeHelper({
|
|
|
69
87
|
|--------|------|---------|-------------|
|
|
70
88
|
| `componentPaths` | `string[]` | `['src/components']` | Directories to scan for React components (supports glob patterns) |
|
|
71
89
|
| `projectRoot` | `string` | `process.cwd()` | Root directory of your project |
|
|
72
|
-
| `importPath` | `string` | `'nextjs-ide-
|
|
90
|
+
| `importPath` | `string` | `'nextjs-ide-helper/withIdeButton'` | Import path for the withIdeButton HOC |
|
|
73
91
|
| `enabled` | `boolean` | `process.env.NODE_ENV === 'development'` | Enable/disable the plugin |
|
|
74
92
|
| `ideType` | `'cursor' \| 'vscode' \| 'webstorm' \| 'atom'` | `'cursor'` | IDE to open files in |
|
|
75
93
|
|
|
@@ -100,7 +118,7 @@ module.exports = withIdeHelper({
|
|
|
100
118
|
You can also manually wrap components:
|
|
101
119
|
|
|
102
120
|
```tsx
|
|
103
|
-
import { withIdeButton } from 'nextjs-ide-
|
|
121
|
+
import { withIdeButton } from 'nextjs-ide-helper';
|
|
104
122
|
|
|
105
123
|
const MyComponent = () => {
|
|
106
124
|
return <div>Hello World</div>;
|
|
@@ -257,7 +275,7 @@ After (automatically transformed):
|
|
|
257
275
|
```tsx
|
|
258
276
|
// src/components/Button.tsx (transformed by the plugin)
|
|
259
277
|
import React from 'react';
|
|
260
|
-
import { withIdeButton } from 'nextjs-ide-
|
|
278
|
+
import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
|
|
261
279
|
|
|
262
280
|
const Button = ({ children, onClick }) => {
|
|
263
281
|
return (
|
|
@@ -297,7 +315,7 @@ export const SecondaryButton = ({ children, onClick }) => {
|
|
|
297
315
|
After (automatically transformed):
|
|
298
316
|
```tsx
|
|
299
317
|
// src/components/Buttons.tsx (transformed by the plugin)
|
|
300
|
-
import { withIdeButton } from 'nextjs-ide-
|
|
318
|
+
import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
|
|
301
319
|
|
|
302
320
|
export const PrimaryButton = withIdeButton(({ children, onClick }) => {
|
|
303
321
|
return (
|
|
@@ -343,7 +361,7 @@ export default Layout;
|
|
|
343
361
|
After (automatically transformed):
|
|
344
362
|
```tsx
|
|
345
363
|
// src/components/Layout.tsx (transformed by the plugin)
|
|
346
|
-
import { withIdeButton } from 'nextjs-ide-
|
|
364
|
+
import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
|
|
347
365
|
|
|
348
366
|
export const Header = withIdeButton(() => <header>My App Header</header>, 'src/components/Layout.tsx', {
|
|
349
367
|
projectRoot: '/path/to/project',
|
|
@@ -381,7 +399,7 @@ export default function Header() {
|
|
|
381
399
|
After (automatically transformed):
|
|
382
400
|
```tsx
|
|
383
401
|
// src/components/Header.tsx (transformed by the plugin)
|
|
384
|
-
import { withIdeButton } from 'nextjs-ide-
|
|
402
|
+
import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
|
|
385
403
|
|
|
386
404
|
function Header() {
|
|
387
405
|
return <header>My App Header</header>;
|
|
@@ -405,7 +423,7 @@ export default () => {
|
|
|
405
423
|
After (automatically transformed):
|
|
406
424
|
```tsx
|
|
407
425
|
// src/components/Footer.tsx (transformed by the plugin)
|
|
408
|
-
import { withIdeButton } from 'nextjs-ide-
|
|
426
|
+
import { withIdeButton } from 'nextjs-ide-helper/withIdeButton';
|
|
409
427
|
|
|
410
428
|
export default withIdeButton(() => {
|
|
411
429
|
return <footer>© 2025 My App</footer>;
|
|
@@ -456,6 +474,17 @@ See [CHANGELOG.md](./CHANGELOG.md) for detailed release notes.
|
|
|
456
474
|
|
|
457
475
|
### Recent Releases
|
|
458
476
|
|
|
477
|
+
### 1.5.0 - Toggle Visibility
|
|
478
|
+
- Added floating toggle button to show/hide all IDE dots
|
|
479
|
+
- Toggle button appears in bottom-right corner
|
|
480
|
+
- Blue when dots visible, gray when hidden
|
|
481
|
+
- State synchronized across all components via custom events
|
|
482
|
+
|
|
483
|
+
### 1.4.1 - Minimal UI
|
|
484
|
+
- Changed IDE buttons from text+emoji to minimal 10x10px blue dots
|
|
485
|
+
- Reduced visual footprint for less intrusive development experience
|
|
486
|
+
- Removed console.log statements from plugin
|
|
487
|
+
|
|
459
488
|
### 1.4.0 - Named Export Support
|
|
460
489
|
- Added comprehensive support for ES6 named exports in React components
|
|
461
490
|
- Support for `export const Component = () => {}` and `export function Component() {}` patterns
|
package/lib/loader.js
CHANGED
|
@@ -221,22 +221,28 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
221
221
|
modified = true;
|
|
222
222
|
} else if (namedExport.declaration) {
|
|
223
223
|
// Handle export function Component() {}
|
|
224
|
-
|
|
224
|
+
// Convert FunctionDeclaration to FunctionExpression (preserves name for stack traces)
|
|
225
|
+
const funcDecl = namedExport.declaration;
|
|
226
|
+
const funcExpr = t.functionExpression(
|
|
227
|
+
funcDecl.id, // Keep the name for debugging
|
|
228
|
+
funcDecl.params,
|
|
229
|
+
funcDecl.body,
|
|
230
|
+
funcDecl.generator,
|
|
231
|
+
funcDecl.async
|
|
232
|
+
);
|
|
233
|
+
|
|
225
234
|
const wrappedCall = t.callExpression(
|
|
226
235
|
t.identifier('withIdeButton'),
|
|
227
236
|
[
|
|
228
|
-
|
|
237
|
+
funcExpr,
|
|
229
238
|
t.stringLiteral(relativePath),
|
|
230
239
|
t.objectExpression([
|
|
231
240
|
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
232
241
|
])
|
|
233
242
|
]
|
|
234
243
|
);
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
namedExport.path.insertBefore(funcDeclaration);
|
|
238
|
-
|
|
239
|
-
// Replace the export with wrapped call
|
|
244
|
+
|
|
245
|
+
// Replace: export function X() {} -> export const X = withIdeButton(function X() {}, ...)
|
|
240
246
|
namedExport.path.node.declaration = t.variableDeclaration('const', [
|
|
241
247
|
t.variableDeclarator(t.identifier(namedExport.name), wrappedCall)
|
|
242
248
|
]);
|
|
@@ -290,15 +296,33 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
290
296
|
);
|
|
291
297
|
}
|
|
292
298
|
|
|
293
|
-
// If the export is a named function
|
|
294
|
-
if (!isAnonymousComponent &&
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
299
|
+
// If the export is a named function declaration, convert to function expression inline
|
|
300
|
+
if (!isAnonymousComponent && t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
|
|
301
|
+
const funcDecl = defaultExportPath.node.declaration;
|
|
302
|
+
const funcExpr = t.functionExpression(
|
|
303
|
+
funcDecl.id, // Keep the name for debugging
|
|
304
|
+
funcDecl.params,
|
|
305
|
+
funcDecl.body,
|
|
306
|
+
funcDecl.generator,
|
|
307
|
+
funcDecl.async
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Wrap the function expression directly
|
|
311
|
+
wrappedCall = t.callExpression(
|
|
312
|
+
t.identifier('withIdeButton'),
|
|
313
|
+
[
|
|
314
|
+
funcExpr,
|
|
315
|
+
t.stringLiteral(relativePath),
|
|
316
|
+
t.objectExpression([
|
|
317
|
+
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
318
|
+
])
|
|
319
|
+
]
|
|
320
|
+
);
|
|
321
|
+
defaultExportPath.node.declaration = wrappedCall;
|
|
322
|
+
} else if (!isAnonymousComponent && t.isClassDeclaration(defaultExportPath.node.declaration)) {
|
|
323
|
+
// For classes, we still need to separate declaration (can't inline class expressions easily)
|
|
298
324
|
const declaration = defaultExportPath.node.declaration;
|
|
299
325
|
defaultExportPath.insertBefore(declaration);
|
|
300
|
-
|
|
301
|
-
// Replace the export declaration with the wrapped call
|
|
302
326
|
defaultExportPath.node.declaration = wrappedCall;
|
|
303
327
|
} else {
|
|
304
328
|
// For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
|
package/lib/withIdeButton.js
CHANGED
|
@@ -4,27 +4,76 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.withIdeButton = withIdeButton;
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
6
|
const react_1 = require("react");
|
|
7
|
+
// Inject global styles once
|
|
8
|
+
let stylesInjected = false;
|
|
9
|
+
function injectStyles() {
|
|
10
|
+
if (stylesInjected || typeof document === 'undefined')
|
|
11
|
+
return;
|
|
12
|
+
stylesInjected = true;
|
|
13
|
+
const style = document.createElement('style');
|
|
14
|
+
style.id = 'ide-helper-styles';
|
|
15
|
+
style.textContent = `
|
|
16
|
+
.ide-helper-dot { display: block; }
|
|
17
|
+
.ide-helper-hidden .ide-helper-dot { display: none; }
|
|
18
|
+
.ide-helper-toggle {
|
|
19
|
+
background: transparent;
|
|
20
|
+
border: 2px solid #007acc !important;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
}
|
|
23
|
+
.ide-helper-hidden .ide-helper-toggle {
|
|
24
|
+
background: #666;
|
|
25
|
+
border-color: #666 !important;
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
document.head.appendChild(style);
|
|
29
|
+
}
|
|
30
|
+
// Toggle button - renders once via singleton pattern
|
|
31
|
+
let toggleInjected = false;
|
|
32
|
+
function injectToggle() {
|
|
33
|
+
if (toggleInjected || typeof document === 'undefined')
|
|
34
|
+
return;
|
|
35
|
+
toggleInjected = true;
|
|
36
|
+
const btn = document.createElement('button');
|
|
37
|
+
btn.className = 'ide-helper-toggle';
|
|
38
|
+
btn.title = 'Toggle IDE dots (click to show/hide)';
|
|
39
|
+
Object.assign(btn.style, {
|
|
40
|
+
position: 'fixed',
|
|
41
|
+
bottom: '16px',
|
|
42
|
+
right: '16px',
|
|
43
|
+
width: '18px',
|
|
44
|
+
height: '18px',
|
|
45
|
+
border: 'none',
|
|
46
|
+
borderRadius: '50%',
|
|
47
|
+
padding: '0',
|
|
48
|
+
cursor: 'pointer',
|
|
49
|
+
zIndex: '9999',
|
|
50
|
+
opacity: '0.7',
|
|
51
|
+
transition: 'opacity 0.2s'
|
|
52
|
+
});
|
|
53
|
+
btn.onmouseenter = () => btn.style.opacity = '1';
|
|
54
|
+
btn.onmouseleave = () => btn.style.opacity = '0.7';
|
|
55
|
+
btn.onclick = () => document.body.classList.toggle('ide-helper-hidden');
|
|
56
|
+
document.body.appendChild(btn);
|
|
57
|
+
}
|
|
7
58
|
const IdeButton = ({ filePath, projectRoot, ideType = 'cursor' }) => {
|
|
59
|
+
(0, react_1.useEffect)(() => {
|
|
60
|
+
injectStyles();
|
|
61
|
+
injectToggle();
|
|
62
|
+
}, []);
|
|
8
63
|
const getIdeUrl = (ide, path) => {
|
|
9
64
|
const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
|
|
10
65
|
switch (ide) {
|
|
11
|
-
case 'cursor':
|
|
12
|
-
|
|
13
|
-
case '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return `webstorm://open?file=${fullPath}`;
|
|
17
|
-
case 'atom':
|
|
18
|
-
return `atom://open?path=${fullPath}`;
|
|
19
|
-
default:
|
|
20
|
-
return `cursor://file${fullPath}`;
|
|
66
|
+
case 'cursor': return `cursor://file${fullPath}`;
|
|
67
|
+
case 'vscode': return `vscode://file${fullPath}`;
|
|
68
|
+
case 'webstorm': return `webstorm://open?file=${fullPath}`;
|
|
69
|
+
case 'atom': return `atom://open?path=${fullPath}`;
|
|
70
|
+
default: return `cursor://file${fullPath}`;
|
|
21
71
|
}
|
|
22
72
|
};
|
|
23
73
|
const handleClick = () => {
|
|
24
|
-
|
|
25
|
-
window.open(url, '_blank');
|
|
74
|
+
window.open(getIdeUrl(ideType, filePath), '_blank');
|
|
26
75
|
};
|
|
27
|
-
return ((0, jsx_runtime_1.jsx)("button", { onClick: handleClick, style: {
|
|
76
|
+
return ((0, jsx_runtime_1.jsx)("button", { className: "ide-helper-dot", onClick: handleClick, style: {
|
|
28
77
|
position: 'absolute',
|
|
29
78
|
top: '4px',
|
|
30
79
|
right: '4px',
|
|
@@ -47,7 +96,6 @@ function withIdeButton(WrappedComponent, filePath, options = {}) {
|
|
|
47
96
|
(0, react_1.useEffect)(() => {
|
|
48
97
|
setIsClient(true);
|
|
49
98
|
}, []);
|
|
50
|
-
// In production or when disabled, just return the component without wrapper
|
|
51
99
|
if (!enabled || !isClient) {
|
|
52
100
|
return (0, jsx_runtime_1.jsx)(WrappedComponent, { ...props });
|
|
53
101
|
}
|
package/lib/withIdeButton.tsx
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { useState, useEffect } from 'react';
|
|
3
3
|
|
|
4
|
+
// Inject global styles once
|
|
5
|
+
let stylesInjected = false;
|
|
6
|
+
|
|
7
|
+
function injectStyles() {
|
|
8
|
+
if (stylesInjected || typeof document === 'undefined') return;
|
|
9
|
+
stylesInjected = true;
|
|
10
|
+
|
|
11
|
+
const style = document.createElement('style');
|
|
12
|
+
style.id = 'ide-helper-styles';
|
|
13
|
+
style.textContent = `
|
|
14
|
+
.ide-helper-dot { display: block; }
|
|
15
|
+
.ide-helper-hidden .ide-helper-dot { display: none; }
|
|
16
|
+
.ide-helper-toggle {
|
|
17
|
+
background: transparent;
|
|
18
|
+
border: 2px solid #007acc !important;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
.ide-helper-hidden .ide-helper-toggle {
|
|
22
|
+
background: #666;
|
|
23
|
+
border-color: #666 !important;
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
document.head.appendChild(style);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Toggle button - renders once via singleton pattern
|
|
30
|
+
let toggleInjected = false;
|
|
31
|
+
|
|
32
|
+
function injectToggle() {
|
|
33
|
+
if (toggleInjected || typeof document === 'undefined') return;
|
|
34
|
+
toggleInjected = true;
|
|
35
|
+
|
|
36
|
+
const btn = document.createElement('button');
|
|
37
|
+
btn.className = 'ide-helper-toggle';
|
|
38
|
+
btn.title = 'Toggle IDE dots (click to show/hide)';
|
|
39
|
+
Object.assign(btn.style, {
|
|
40
|
+
position: 'fixed',
|
|
41
|
+
bottom: '16px',
|
|
42
|
+
right: '16px',
|
|
43
|
+
width: '18px',
|
|
44
|
+
height: '18px',
|
|
45
|
+
border: 'none',
|
|
46
|
+
borderRadius: '50%',
|
|
47
|
+
padding: '0',
|
|
48
|
+
cursor: 'pointer',
|
|
49
|
+
zIndex: '9999',
|
|
50
|
+
opacity: '0.7',
|
|
51
|
+
transition: 'opacity 0.2s'
|
|
52
|
+
});
|
|
53
|
+
btn.onmouseenter = () => btn.style.opacity = '1';
|
|
54
|
+
btn.onmouseleave = () => btn.style.opacity = '0.7';
|
|
55
|
+
btn.onclick = () => document.body.classList.toggle('ide-helper-hidden');
|
|
56
|
+
document.body.appendChild(btn);
|
|
57
|
+
}
|
|
58
|
+
|
|
4
59
|
interface IdeButtonProps {
|
|
5
60
|
filePath: string;
|
|
6
61
|
projectRoot?: string;
|
|
@@ -8,30 +63,29 @@ interface IdeButtonProps {
|
|
|
8
63
|
}
|
|
9
64
|
|
|
10
65
|
const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
injectStyles();
|
|
68
|
+
injectToggle();
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
11
71
|
const getIdeUrl = (ide: string, path: string) => {
|
|
12
72
|
const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
|
|
13
|
-
|
|
14
73
|
switch (ide) {
|
|
15
|
-
case 'cursor':
|
|
16
|
-
|
|
17
|
-
case '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return `webstorm://open?file=${fullPath}`;
|
|
21
|
-
case 'atom':
|
|
22
|
-
return `atom://open?path=${fullPath}`;
|
|
23
|
-
default:
|
|
24
|
-
return `cursor://file${fullPath}`;
|
|
74
|
+
case 'cursor': return `cursor://file${fullPath}`;
|
|
75
|
+
case 'vscode': return `vscode://file${fullPath}`;
|
|
76
|
+
case 'webstorm': return `webstorm://open?file=${fullPath}`;
|
|
77
|
+
case 'atom': return `atom://open?path=${fullPath}`;
|
|
78
|
+
default: return `cursor://file${fullPath}`;
|
|
25
79
|
}
|
|
26
80
|
};
|
|
27
81
|
|
|
28
82
|
const handleClick = () => {
|
|
29
|
-
|
|
30
|
-
window.open(url, '_blank');
|
|
83
|
+
window.open(getIdeUrl(ideType, filePath), '_blank');
|
|
31
84
|
};
|
|
32
85
|
|
|
33
86
|
return (
|
|
34
87
|
<button
|
|
88
|
+
className="ide-helper-dot"
|
|
35
89
|
onClick={handleClick}
|
|
36
90
|
style={{
|
|
37
91
|
position: 'absolute',
|
|
@@ -75,7 +129,6 @@ export function withIdeButton<T extends object>(
|
|
|
75
129
|
setIsClient(true);
|
|
76
130
|
}, []);
|
|
77
131
|
|
|
78
|
-
// In production or when disabled, just return the component without wrapper
|
|
79
132
|
if (!enabled || !isClient) {
|
|
80
133
|
return <WrappedComponent {...props} />;
|
|
81
134
|
}
|
|
@@ -91,4 +144,4 @@ export function withIdeButton<T extends object>(
|
|
|
91
144
|
WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
|
|
92
145
|
|
|
93
146
|
return WithIdeButtonComponent;
|
|
94
|
-
}
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextjs-ide-helper",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "A Next.js plugin that automatically adds IDE buttons to React components for seamless IDE integration. Supports Cursor, VS Code, WebStorm, and Atom.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
package/src/loader.js
CHANGED
|
@@ -221,22 +221,28 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
221
221
|
modified = true;
|
|
222
222
|
} else if (namedExport.declaration) {
|
|
223
223
|
// Handle export function Component() {}
|
|
224
|
-
|
|
224
|
+
// Convert FunctionDeclaration to FunctionExpression (preserves name for stack traces)
|
|
225
|
+
const funcDecl = namedExport.declaration;
|
|
226
|
+
const funcExpr = t.functionExpression(
|
|
227
|
+
funcDecl.id, // Keep the name for debugging
|
|
228
|
+
funcDecl.params,
|
|
229
|
+
funcDecl.body,
|
|
230
|
+
funcDecl.generator,
|
|
231
|
+
funcDecl.async
|
|
232
|
+
);
|
|
233
|
+
|
|
225
234
|
const wrappedCall = t.callExpression(
|
|
226
235
|
t.identifier('withIdeButton'),
|
|
227
236
|
[
|
|
228
|
-
|
|
237
|
+
funcExpr,
|
|
229
238
|
t.stringLiteral(relativePath),
|
|
230
239
|
t.objectExpression([
|
|
231
240
|
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
232
241
|
])
|
|
233
242
|
]
|
|
234
243
|
);
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
namedExport.path.insertBefore(funcDeclaration);
|
|
238
|
-
|
|
239
|
-
// Replace the export with wrapped call
|
|
244
|
+
|
|
245
|
+
// Replace: export function X() {} -> export const X = withIdeButton(function X() {}, ...)
|
|
240
246
|
namedExport.path.node.declaration = t.variableDeclaration('const', [
|
|
241
247
|
t.variableDeclarator(t.identifier(namedExport.name), wrappedCall)
|
|
242
248
|
]);
|
|
@@ -290,15 +296,33 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
290
296
|
);
|
|
291
297
|
}
|
|
292
298
|
|
|
293
|
-
// If the export is a named function
|
|
294
|
-
if (!isAnonymousComponent &&
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
299
|
+
// If the export is a named function declaration, convert to function expression inline
|
|
300
|
+
if (!isAnonymousComponent && t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
|
|
301
|
+
const funcDecl = defaultExportPath.node.declaration;
|
|
302
|
+
const funcExpr = t.functionExpression(
|
|
303
|
+
funcDecl.id, // Keep the name for debugging
|
|
304
|
+
funcDecl.params,
|
|
305
|
+
funcDecl.body,
|
|
306
|
+
funcDecl.generator,
|
|
307
|
+
funcDecl.async
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Wrap the function expression directly
|
|
311
|
+
wrappedCall = t.callExpression(
|
|
312
|
+
t.identifier('withIdeButton'),
|
|
313
|
+
[
|
|
314
|
+
funcExpr,
|
|
315
|
+
t.stringLiteral(relativePath),
|
|
316
|
+
t.objectExpression([
|
|
317
|
+
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
318
|
+
])
|
|
319
|
+
]
|
|
320
|
+
);
|
|
321
|
+
defaultExportPath.node.declaration = wrappedCall;
|
|
322
|
+
} else if (!isAnonymousComponent && t.isClassDeclaration(defaultExportPath.node.declaration)) {
|
|
323
|
+
// For classes, we still need to separate declaration (can't inline class expressions easily)
|
|
298
324
|
const declaration = defaultExportPath.node.declaration;
|
|
299
325
|
defaultExportPath.insertBefore(declaration);
|
|
300
|
-
|
|
301
|
-
// Replace the export declaration with the wrapped call
|
|
302
326
|
defaultExportPath.node.declaration = wrappedCall;
|
|
303
327
|
} else {
|
|
304
328
|
// For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
|
package/src/withIdeButton.tsx
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { useState, useEffect } from 'react';
|
|
3
3
|
|
|
4
|
+
// Inject global styles once
|
|
5
|
+
let stylesInjected = false;
|
|
6
|
+
|
|
7
|
+
function injectStyles() {
|
|
8
|
+
if (stylesInjected || typeof document === 'undefined') return;
|
|
9
|
+
stylesInjected = true;
|
|
10
|
+
|
|
11
|
+
const style = document.createElement('style');
|
|
12
|
+
style.id = 'ide-helper-styles';
|
|
13
|
+
style.textContent = `
|
|
14
|
+
.ide-helper-dot { display: block; }
|
|
15
|
+
.ide-helper-hidden .ide-helper-dot { display: none; }
|
|
16
|
+
.ide-helper-toggle {
|
|
17
|
+
background: transparent;
|
|
18
|
+
border: 2px solid #007acc !important;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
.ide-helper-hidden .ide-helper-toggle {
|
|
22
|
+
background: #666;
|
|
23
|
+
border-color: #666 !important;
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
document.head.appendChild(style);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Toggle button - renders once via singleton pattern
|
|
30
|
+
let toggleInjected = false;
|
|
31
|
+
|
|
32
|
+
function injectToggle() {
|
|
33
|
+
if (toggleInjected || typeof document === 'undefined') return;
|
|
34
|
+
toggleInjected = true;
|
|
35
|
+
|
|
36
|
+
const btn = document.createElement('button');
|
|
37
|
+
btn.className = 'ide-helper-toggle';
|
|
38
|
+
btn.title = 'Toggle IDE dots (click to show/hide)';
|
|
39
|
+
Object.assign(btn.style, {
|
|
40
|
+
position: 'fixed',
|
|
41
|
+
bottom: '16px',
|
|
42
|
+
right: '16px',
|
|
43
|
+
width: '18px',
|
|
44
|
+
height: '18px',
|
|
45
|
+
border: 'none',
|
|
46
|
+
borderRadius: '50%',
|
|
47
|
+
padding: '0',
|
|
48
|
+
cursor: 'pointer',
|
|
49
|
+
zIndex: '9999',
|
|
50
|
+
opacity: '0.7',
|
|
51
|
+
transition: 'opacity 0.2s'
|
|
52
|
+
});
|
|
53
|
+
btn.onmouseenter = () => btn.style.opacity = '1';
|
|
54
|
+
btn.onmouseleave = () => btn.style.opacity = '0.7';
|
|
55
|
+
btn.onclick = () => document.body.classList.toggle('ide-helper-hidden');
|
|
56
|
+
document.body.appendChild(btn);
|
|
57
|
+
}
|
|
58
|
+
|
|
4
59
|
interface IdeButtonProps {
|
|
5
60
|
filePath: string;
|
|
6
61
|
projectRoot?: string;
|
|
@@ -8,30 +63,29 @@ interface IdeButtonProps {
|
|
|
8
63
|
}
|
|
9
64
|
|
|
10
65
|
const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
injectStyles();
|
|
68
|
+
injectToggle();
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
11
71
|
const getIdeUrl = (ide: string, path: string) => {
|
|
12
72
|
const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
|
|
13
|
-
|
|
14
73
|
switch (ide) {
|
|
15
|
-
case 'cursor':
|
|
16
|
-
|
|
17
|
-
case '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return `webstorm://open?file=${fullPath}`;
|
|
21
|
-
case 'atom':
|
|
22
|
-
return `atom://open?path=${fullPath}`;
|
|
23
|
-
default:
|
|
24
|
-
return `cursor://file${fullPath}`;
|
|
74
|
+
case 'cursor': return `cursor://file${fullPath}`;
|
|
75
|
+
case 'vscode': return `vscode://file${fullPath}`;
|
|
76
|
+
case 'webstorm': return `webstorm://open?file=${fullPath}`;
|
|
77
|
+
case 'atom': return `atom://open?path=${fullPath}`;
|
|
78
|
+
default: return `cursor://file${fullPath}`;
|
|
25
79
|
}
|
|
26
80
|
};
|
|
27
81
|
|
|
28
82
|
const handleClick = () => {
|
|
29
|
-
|
|
30
|
-
window.open(url, '_blank');
|
|
83
|
+
window.open(getIdeUrl(ideType, filePath), '_blank');
|
|
31
84
|
};
|
|
32
85
|
|
|
33
86
|
return (
|
|
34
87
|
<button
|
|
88
|
+
className="ide-helper-dot"
|
|
35
89
|
onClick={handleClick}
|
|
36
90
|
style={{
|
|
37
91
|
position: 'absolute',
|
|
@@ -75,7 +129,6 @@ export function withIdeButton<T extends object>(
|
|
|
75
129
|
setIsClient(true);
|
|
76
130
|
}, []);
|
|
77
131
|
|
|
78
|
-
// In production or when disabled, just return the component without wrapper
|
|
79
132
|
if (!enabled || !isClient) {
|
|
80
133
|
return <WrappedComponent {...props} />;
|
|
81
134
|
}
|
|
@@ -91,4 +144,4 @@ export function withIdeButton<T extends object>(
|
|
|
91
144
|
WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
|
|
92
145
|
|
|
93
146
|
return WithIdeButtonComponent;
|
|
94
|
-
}
|
|
147
|
+
}
|