nextjs-ide-helper 1.4.1 → 1.5.3

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 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**: Uses absolute positioning to avoid layout disruption
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
- # or
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-plugin');
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-plugin');
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-plugin/withIdeButton', // import path for the HOC
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-plugin/withIdeButton'` | Import path for the withIdeButton HOC |
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-plugin';
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-plugin/withIdeButton';
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-plugin/withIdeButton';
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-plugin/withIdeButton';
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-plugin/withIdeButton';
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-plugin/withIdeButton';
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
- const funcDeclaration = namedExport.declaration;
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
- t.identifier(namedExport.name),
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
- // Insert the function declaration before the export
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 or class declaration, we need to handle it differently
294
- if (!isAnonymousComponent &&
295
- (t.isFunctionDeclaration(defaultExportPath.node.declaration) ||
296
- t.isClassDeclaration(defaultExportPath.node.declaration))) {
297
- // Insert the function/class declaration before the export
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
@@ -4,27 +4,96 @@ 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
+ const STORAGE_KEY = 'ide-helper-hidden';
33
+ function injectToggle() {
34
+ if (toggleInjected || typeof document === 'undefined')
35
+ return;
36
+ toggleInjected = true;
37
+ // Restore saved state from localStorage
38
+ try {
39
+ const savedHidden = localStorage.getItem(STORAGE_KEY) === 'true';
40
+ if (savedHidden) {
41
+ document.body.classList.add('ide-helper-hidden');
42
+ }
43
+ }
44
+ catch {
45
+ // localStorage unavailable (private browsing)
46
+ }
47
+ const btn = document.createElement('button');
48
+ btn.className = 'ide-helper-toggle';
49
+ btn.title = 'Toggle IDE dots (click to show/hide)';
50
+ Object.assign(btn.style, {
51
+ position: 'fixed',
52
+ bottom: '16px',
53
+ right: '16px',
54
+ width: '18px',
55
+ height: '18px',
56
+ border: 'none',
57
+ borderRadius: '50%',
58
+ padding: '0',
59
+ cursor: 'pointer',
60
+ zIndex: '9999',
61
+ opacity: '0.7',
62
+ transition: 'opacity 0.2s'
63
+ });
64
+ btn.onmouseenter = () => btn.style.opacity = '1';
65
+ btn.onmouseleave = () => btn.style.opacity = '0.7';
66
+ btn.onclick = () => {
67
+ document.body.classList.toggle('ide-helper-hidden');
68
+ try {
69
+ const isHidden = document.body.classList.contains('ide-helper-hidden');
70
+ localStorage.setItem(STORAGE_KEY, String(isHidden));
71
+ }
72
+ catch {
73
+ // localStorage unavailable (private browsing)
74
+ }
75
+ };
76
+ document.body.appendChild(btn);
77
+ }
7
78
  const IdeButton = ({ filePath, projectRoot, ideType = 'cursor' }) => {
79
+ (0, react_1.useEffect)(() => {
80
+ injectStyles();
81
+ injectToggle();
82
+ }, []);
8
83
  const getIdeUrl = (ide, path) => {
9
84
  const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
10
85
  switch (ide) {
11
- case 'cursor':
12
- return `cursor://file${fullPath}`;
13
- case 'vscode':
14
- return `vscode://file${fullPath}`;
15
- case 'webstorm':
16
- return `webstorm://open?file=${fullPath}`;
17
- case 'atom':
18
- return `atom://open?path=${fullPath}`;
19
- default:
20
- return `cursor://file${fullPath}`;
86
+ case 'cursor': return `cursor://file${fullPath}`;
87
+ case 'vscode': return `vscode://file${fullPath}`;
88
+ case 'webstorm': return `webstorm://open?file=${fullPath}`;
89
+ case 'atom': return `atom://open?path=${fullPath}`;
90
+ default: return `cursor://file${fullPath}`;
21
91
  }
22
92
  };
23
93
  const handleClick = () => {
24
- const url = getIdeUrl(ideType, filePath);
25
- window.open(url, '_blank');
94
+ window.open(getIdeUrl(ideType, filePath), '_blank');
26
95
  };
27
- return ((0, jsx_runtime_1.jsx)("button", { onClick: handleClick, style: {
96
+ return ((0, jsx_runtime_1.jsx)("button", { className: "ide-helper-dot", onClick: handleClick, style: {
28
97
  position: 'absolute',
29
98
  top: '4px',
30
99
  right: '4px',
@@ -47,7 +116,6 @@ function withIdeButton(WrappedComponent, filePath, options = {}) {
47
116
  (0, react_1.useEffect)(() => {
48
117
  setIsClient(true);
49
118
  }, []);
50
- // In production or when disabled, just return the component without wrapper
51
119
  if (!enabled || !isClient) {
52
120
  return (0, jsx_runtime_1.jsx)(WrappedComponent, { ...props });
53
121
  }
@@ -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
- return `cursor://file${fullPath}`;
17
- case 'vscode':
18
- return `vscode://file${fullPath}`;
19
- case 'webstorm':
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
- const url = getIdeUrl(ideType, filePath);
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.4.1",
3
+ "version": "1.5.3",
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",
@@ -23,19 +23,22 @@
23
23
  "ide",
24
24
  "development",
25
25
  "webpack",
26
+ "turbopack",
26
27
  "plugin",
27
28
  "components"
28
29
  ],
29
30
  "author": "Your Name",
30
31
  "license": "MIT",
32
+ "engines": {
33
+ "node": ">=18.17.0"
34
+ },
31
35
  "peerDependencies": {
32
36
  "next": ">=13.0.0",
33
37
  "react": ">=18.0.0"
34
38
  },
35
- "dependencies": {
39
+ "dependencies": {
36
40
  "minimatch": "^10.0.3"
37
41
  },
38
-
39
42
  "devDependencies": {
40
43
  "@babel/generator": "^7.28.0",
41
44
  "@babel/parser": "^7.28.0",
@@ -44,7 +47,7 @@
44
47
  "@types/jest": "^30.0.0",
45
48
  "@types/node": "^20.19.9",
46
49
  "@types/react": "^18.3.23",
47
- "jest": "^30.0.5",
50
+ "jest": "^30.0.5",
48
51
  "typescript": "^5.8.3"
49
52
  },
50
53
  "repository": {
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
- const funcDeclaration = namedExport.declaration;
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
- t.identifier(namedExport.name),
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
- // Insert the function declaration before the export
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 or class declaration, we need to handle it differently
294
- if (!isAnonymousComponent &&
295
- (t.isFunctionDeclaration(defaultExportPath.node.declaration) ||
296
- t.isClassDeclaration(defaultExportPath.node.declaration))) {
297
- // Insert the function/class declaration before the export
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/plugin.js CHANGED
@@ -19,8 +19,25 @@ function extractBaseDirectory(globPattern) {
19
19
  return globPattern.replace(/\/\*\*.*$/, '').replace(/\/\*.*$/, '');
20
20
  }
21
21
 
22
+ /**
23
+ * Convert glob pattern to regex pattern for Turbopack path matching
24
+ * @param {string} globPattern - The glob pattern (e.g., 'src/components/**\/*.tsx')
25
+ * @returns {RegExp} - A regex pattern for path matching
26
+ */
27
+ function globToRegex(globPattern) {
28
+ // Escape special regex characters except * and **
29
+ let pattern = globPattern
30
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
31
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
32
+ .replace(/\*/g, '[^/]*')
33
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*');
34
+
35
+ return new RegExp(pattern);
36
+ }
37
+
22
38
  /**
23
39
  * NextJS Cursor Helper plugin
40
+ * Supports both Webpack (Next.js 13-15) and Turbopack (Next.js 16+)
24
41
  * @param {CursorHelperOptions} options - Configuration options
25
42
  * @returns {function} Next.js config modifier function
26
43
  */
@@ -33,24 +50,75 @@ function withCursorHelper(options = {}) {
33
50
  };
34
51
 
35
52
  const config = { ...defaultOptions, ...options };
36
-
53
+
37
54
  console.log('🔧 Plugin initialized with config:', config);
38
55
 
56
+ // Resolve loader path once
57
+ const loaderPath = require.resolve('./loader.js');
58
+
39
59
  return (nextConfig = {}) => {
60
+ // If plugin is disabled, return config unchanged
61
+ if (!config.enabled) {
62
+ console.log('🔧 Plugin disabled, returning original config');
63
+ return nextConfig;
64
+ }
65
+
66
+ // Extract base directories from glob patterns for webpack's include
67
+ const includeDirectories = [...new Set(
68
+ config.componentPaths.map(p => extractBaseDirectory(p))
69
+ )].map(p => path.resolve(config.projectRoot, p));
70
+
71
+ // Create path regex patterns for Turbopack conditions
72
+ const pathPatterns = config.componentPaths.map(p => globToRegex(p));
73
+
74
+ // Build Turbopack rules for .tsx and .jsx files
75
+ const turbopackRules = {};
76
+
77
+ // Create loader config for Turbopack
78
+ const turbopackLoaderConfig = {
79
+ loaders: [{
80
+ loader: loaderPath,
81
+ options: config
82
+ }]
83
+ };
84
+
85
+ // Add rules for .tsx files
86
+ turbopackRules['*.tsx'] = pathPatterns.map(pathRegex => ({
87
+ condition: {
88
+ all: [
89
+ 'browser', // Client-side only
90
+ 'development', // Dev mode only
91
+ { path: pathRegex }
92
+ ]
93
+ },
94
+ ...turbopackLoaderConfig,
95
+ as: '*.tsx'
96
+ }));
97
+
98
+ // Add rules for .jsx files
99
+ turbopackRules['*.jsx'] = pathPatterns.map(pathRegex => ({
100
+ condition: {
101
+ all: [
102
+ 'browser',
103
+ 'development',
104
+ { path: pathRegex }
105
+ ]
106
+ },
107
+ ...turbopackLoaderConfig,
108
+ as: '*.jsx'
109
+ }));
110
+
40
111
  return {
41
112
  ...nextConfig,
113
+
114
+ // Webpack config (Next.js 13-15, or Next.js 16+ with --webpack flag)
42
115
  webpack: (webpackConfig, context) => {
43
116
  const { dev, isServer } = context;
44
-
117
+
45
118
  console.log('🔧 Webpack config called:', { dev, isServer, enabled: config.enabled });
46
119
 
47
120
  // Only apply in development and for client-side
48
- if (config.enabled && dev && !isServer) {
49
- // Extract base directories from glob patterns for webpack's include
50
- const includeDirectories = [...new Set(
51
- config.componentPaths.map(p => extractBaseDirectory(p))
52
- )].map(p => path.resolve(config.projectRoot, p));
53
-
121
+ if (dev && !isServer) {
54
122
  console.log('🔧 Adding webpack rule with include directories:', includeDirectories);
55
123
 
56
124
  const rule = {
@@ -58,7 +126,7 @@ function withCursorHelper(options = {}) {
58
126
  include: includeDirectories,
59
127
  use: [
60
128
  {
61
- loader: require.resolve('./loader.js'),
129
+ loader: loaderPath,
62
130
  options: config
63
131
  }
64
132
  ],
@@ -68,7 +136,7 @@ function withCursorHelper(options = {}) {
68
136
  webpackConfig.module.rules.unshift(rule);
69
137
  console.log('🔧 Webpack rule added successfully');
70
138
  } else {
71
- console.log('🔧 Plugin conditions not met, skipping webpack rule');
139
+ console.log('🔧 Webpack conditions not met (dev:', dev, 'isServer:', isServer, ')');
72
140
  }
73
141
 
74
142
  // Call the existing webpack function if it exists
@@ -77,6 +145,15 @@ function withCursorHelper(options = {}) {
77
145
  }
78
146
 
79
147
  return webpackConfig;
148
+ },
149
+
150
+ // Turbopack config (Next.js 16+ default bundler)
151
+ turbopack: {
152
+ ...nextConfig.turbopack,
153
+ rules: {
154
+ ...nextConfig.turbopack?.rules,
155
+ ...turbopackRules
156
+ }
80
157
  }
81
158
  };
82
159
  };
@@ -1,6 +1,81 @@
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
+ const STORAGE_KEY = 'ide-helper-hidden';
33
+
34
+ function injectToggle() {
35
+ if (toggleInjected || typeof document === 'undefined') return;
36
+ toggleInjected = true;
37
+
38
+ // Restore saved state from localStorage
39
+ try {
40
+ const savedHidden = localStorage.getItem(STORAGE_KEY) === 'true';
41
+ if (savedHidden) {
42
+ document.body.classList.add('ide-helper-hidden');
43
+ }
44
+ } catch {
45
+ // localStorage unavailable (private browsing)
46
+ }
47
+
48
+ const btn = document.createElement('button');
49
+ btn.className = 'ide-helper-toggle';
50
+ btn.title = 'Toggle IDE dots (click to show/hide)';
51
+ Object.assign(btn.style, {
52
+ position: 'fixed',
53
+ bottom: '16px',
54
+ right: '16px',
55
+ width: '18px',
56
+ height: '18px',
57
+ border: 'none',
58
+ borderRadius: '50%',
59
+ padding: '0',
60
+ cursor: 'pointer',
61
+ zIndex: '9999',
62
+ opacity: '0.7',
63
+ transition: 'opacity 0.2s'
64
+ });
65
+ btn.onmouseenter = () => btn.style.opacity = '1';
66
+ btn.onmouseleave = () => btn.style.opacity = '0.7';
67
+ btn.onclick = () => {
68
+ document.body.classList.toggle('ide-helper-hidden');
69
+ try {
70
+ const isHidden = document.body.classList.contains('ide-helper-hidden');
71
+ localStorage.setItem(STORAGE_KEY, String(isHidden));
72
+ } catch {
73
+ // localStorage unavailable (private browsing)
74
+ }
75
+ };
76
+ document.body.appendChild(btn);
77
+ }
78
+
4
79
  interface IdeButtonProps {
5
80
  filePath: string;
6
81
  projectRoot?: string;
@@ -8,30 +83,29 @@ interface IdeButtonProps {
8
83
  }
9
84
 
10
85
  const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
86
+ useEffect(() => {
87
+ injectStyles();
88
+ injectToggle();
89
+ }, []);
90
+
11
91
  const getIdeUrl = (ide: string, path: string) => {
12
92
  const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
13
-
14
93
  switch (ide) {
15
- case 'cursor':
16
- return `cursor://file${fullPath}`;
17
- case 'vscode':
18
- return `vscode://file${fullPath}`;
19
- case 'webstorm':
20
- return `webstorm://open?file=${fullPath}`;
21
- case 'atom':
22
- return `atom://open?path=${fullPath}`;
23
- default:
24
- return `cursor://file${fullPath}`;
94
+ case 'cursor': return `cursor://file${fullPath}`;
95
+ case 'vscode': return `vscode://file${fullPath}`;
96
+ case 'webstorm': return `webstorm://open?file=${fullPath}`;
97
+ case 'atom': return `atom://open?path=${fullPath}`;
98
+ default: return `cursor://file${fullPath}`;
25
99
  }
26
100
  };
27
101
 
28
102
  const handleClick = () => {
29
- const url = getIdeUrl(ideType, filePath);
30
- window.open(url, '_blank');
103
+ window.open(getIdeUrl(ideType, filePath), '_blank');
31
104
  };
32
105
 
33
106
  return (
34
107
  <button
108
+ className="ide-helper-dot"
35
109
  onClick={handleClick}
36
110
  style={{
37
111
  position: 'absolute',
@@ -75,7 +149,6 @@ export function withIdeButton<T extends object>(
75
149
  setIsClient(true);
76
150
  }, []);
77
151
 
78
- // In production or when disabled, just return the component without wrapper
79
152
  if (!enabled || !isClient) {
80
153
  return <WrappedComponent {...props} />;
81
154
  }
@@ -91,4 +164,4 @@ export function withIdeButton<T extends object>(
91
164
  WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
92
165
 
93
166
  return WithIdeButtonComponent;
94
- }
167
+ }