nextjs-ide-helper 1.0.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/README.md +167 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +10 -0
- package/lib/loader.js +79 -0
- package/lib/plugin.js +61 -0
- package/lib/withCursorButton.d.ts +6 -0
- package/lib/withCursorButton.js +44 -0
- package/package.json +45 -0
- package/src/index.ts +8 -0
- package/src/loader.js +79 -0
- package/src/plugin.js +61 -0
- package/src/withCursorButton.tsx +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# NextJS Cursor Helper
|
|
2
|
+
|
|
3
|
+
A Next.js plugin that automatically adds cursor buttons to React components in development mode, enabling seamless IDE integration and faster development workflow.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Automatic Integration**: Automatically wraps React components with cursor buttons
|
|
8
|
+
- 🎯 **Smart Detection**: Only processes components in specified directories
|
|
9
|
+
- 🔧 **Zero Configuration**: Works out of the box with sensible defaults
|
|
10
|
+
- 🏎️ **Development Only**: Only active in development mode, no production overhead
|
|
11
|
+
- 🎨 **Non-intrusive**: Uses absolute positioning to avoid layout disruption
|
|
12
|
+
- ⚡ **Hydration Safe**: No SSR/client hydration mismatches
|
|
13
|
+
- 📱 **TypeScript Support**: Full TypeScript definitions included
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install nextjs-cursor-helper --save-dev
|
|
19
|
+
# or
|
|
20
|
+
yarn add nextjs-cursor-helper --dev
|
|
21
|
+
# or
|
|
22
|
+
pnpm add nextjs-cursor-helper --save-dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Configure Next.js
|
|
28
|
+
|
|
29
|
+
Add the plugin to your `next.config.js`:
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
const withCursorHelper = require('nextjs-cursor-helper');
|
|
33
|
+
|
|
34
|
+
/** @type {import('next').NextConfig} */
|
|
35
|
+
const nextConfig = {
|
|
36
|
+
// your existing config
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
module.exports = withCursorHelper()(nextConfig);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. That's it!
|
|
43
|
+
|
|
44
|
+
All React components in your `src/components` directory will automatically get cursor buttons in development mode.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
You can customize the plugin behavior:
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const withCursorHelper = require('nextjs-cursor-helper');
|
|
52
|
+
|
|
53
|
+
const nextConfig = {
|
|
54
|
+
// your existing config
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = withCursorHelper({
|
|
58
|
+
componentPaths: ['src/components', 'components', 'src/ui'], // directories to scan
|
|
59
|
+
projectRoot: process.cwd(), // project root directory
|
|
60
|
+
importPath: 'nextjs-cursor-helper/withCursorButton', // import path for the HOC
|
|
61
|
+
enabled: process.env.NODE_ENV === 'development' // enable/disable
|
|
62
|
+
})(nextConfig);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Configuration Options
|
|
66
|
+
|
|
67
|
+
| Option | Type | Default | Description |
|
|
68
|
+
|--------|------|---------|-------------|
|
|
69
|
+
| `componentPaths` | `string[]` | `['src/components']` | Directories to scan for React components |
|
|
70
|
+
| `projectRoot` | `string` | `process.cwd()` | Root directory of your project |
|
|
71
|
+
| `importPath` | `string` | `'nextjs-cursor-helper/withCursorButton'` | Import path for the withCursorButton HOC |
|
|
72
|
+
| `enabled` | `boolean` | `process.env.NODE_ENV === 'development'` | Enable/disable the plugin |
|
|
73
|
+
|
|
74
|
+
## Manual Usage
|
|
75
|
+
|
|
76
|
+
You can also manually wrap components:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { withCursorButton } from 'nextjs-cursor-helper';
|
|
80
|
+
|
|
81
|
+
const MyComponent = () => {
|
|
82
|
+
return <div>Hello World</div>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default withCursorButton(MyComponent, 'src/components/MyComponent.tsx');
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How It Works
|
|
89
|
+
|
|
90
|
+
1. **Webpack Loader**: The plugin uses a custom webpack loader to transform your React components at build time
|
|
91
|
+
2. **Automatic Detection**: It scans specified directories for `.tsx` and `.jsx` files
|
|
92
|
+
3. **Smart Wrapping**: Only wraps components that export a default React component
|
|
93
|
+
4. **Development Only**: The cursor buttons only appear in development mode
|
|
94
|
+
5. **Hydration Safe**: Uses client-side state to prevent SSR/hydration mismatches
|
|
95
|
+
|
|
96
|
+
## Example
|
|
97
|
+
|
|
98
|
+
Before (your original component):
|
|
99
|
+
```tsx
|
|
100
|
+
// src/components/Button.tsx
|
|
101
|
+
import React from 'react';
|
|
102
|
+
|
|
103
|
+
const Button = ({ children, onClick }) => {
|
|
104
|
+
return (
|
|
105
|
+
<button onClick={onClick}>
|
|
106
|
+
{children}
|
|
107
|
+
</button>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default Button;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
After (automatically transformed):
|
|
115
|
+
```tsx
|
|
116
|
+
// src/components/Button.tsx (transformed by the plugin)
|
|
117
|
+
import React from 'react';
|
|
118
|
+
import { withCursorButton } from 'nextjs-cursor-helper/withCursorButton';
|
|
119
|
+
|
|
120
|
+
const Button = ({ children, onClick }) => {
|
|
121
|
+
return (
|
|
122
|
+
<button onClick={onClick}>
|
|
123
|
+
{children}
|
|
124
|
+
</button>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default withCursorButton(Button, 'src/components/Button.tsx', { projectRoot: '/path/to/project' });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Troubleshooting
|
|
132
|
+
|
|
133
|
+
### Components not getting wrapped
|
|
134
|
+
|
|
135
|
+
1. Check that your components are in the specified `componentPaths`
|
|
136
|
+
2. Ensure your components export a default export with a capitalized name
|
|
137
|
+
3. Verify you're in development mode (`NODE_ENV=development`)
|
|
138
|
+
|
|
139
|
+
### Hydration errors
|
|
140
|
+
|
|
141
|
+
The plugin is designed to prevent hydration errors, but if you encounter any:
|
|
142
|
+
|
|
143
|
+
1. Make sure you're using the latest version
|
|
144
|
+
2. Check that the 'use client' directive is present in the withCursorButton module
|
|
145
|
+
3. File an issue with details about your setup
|
|
146
|
+
|
|
147
|
+
### Cursor not opening files
|
|
148
|
+
|
|
149
|
+
1. Ensure you have Cursor IDE installed
|
|
150
|
+
2. Check that the file paths are correct
|
|
151
|
+
3. Verify your browser allows `cursor://` protocol links
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
|
|
155
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
160
|
+
|
|
161
|
+
## Changelog
|
|
162
|
+
|
|
163
|
+
### 1.0.0
|
|
164
|
+
- Initial release
|
|
165
|
+
- Automatic component wrapping
|
|
166
|
+
- TypeScript support
|
|
167
|
+
- Hydration-safe implementation
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withCursorHelper = exports.withCursorButton = void 0;
|
|
4
|
+
// Main exports
|
|
5
|
+
var withCursorButton_1 = require("./withCursorButton");
|
|
6
|
+
Object.defineProperty(exports, "withCursorButton", { enumerable: true, get: function () { return withCursorButton_1.withCursorButton; } });
|
|
7
|
+
// Next.js plugin
|
|
8
|
+
const withCursorHelper = require('./plugin');
|
|
9
|
+
exports.withCursorHelper = withCursorHelper;
|
|
10
|
+
exports.default = withCursorHelper;
|
package/lib/loader.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webpack loader that automatically wraps React components with cursor buttons
|
|
5
|
+
* @param {string} source - The source code of the file
|
|
6
|
+
* @returns {string} - The transformed source code
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function cursorButtonLoader(source) {
|
|
9
|
+
const filename = this.resourcePath;
|
|
10
|
+
const options = this.getOptions() || {};
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
componentPaths = ['src/components'],
|
|
14
|
+
projectRoot = process.cwd(),
|
|
15
|
+
importPath = 'nextjs-cursor-helper',
|
|
16
|
+
enabled = process.env.NODE_ENV === 'development'
|
|
17
|
+
} = options;
|
|
18
|
+
|
|
19
|
+
// Only process if enabled
|
|
20
|
+
if (!enabled) {
|
|
21
|
+
return source;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Only process files in specified component directories
|
|
25
|
+
const shouldProcess = componentPaths.some(componentPath =>
|
|
26
|
+
filename.includes(path.resolve(projectRoot, componentPath))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!shouldProcess || (!filename.endsWith('.tsx') && !filename.endsWith('.jsx'))) {
|
|
30
|
+
return source;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if it's already wrapped
|
|
34
|
+
if (source.includes('withCursorButton')) {
|
|
35
|
+
return source;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if it has a default export that looks like a React component
|
|
39
|
+
const defaultExportMatch = source.match(/export\s+default\s+(\w+)/);
|
|
40
|
+
if (!defaultExportMatch) {
|
|
41
|
+
return source;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const componentName = defaultExportMatch[1];
|
|
45
|
+
|
|
46
|
+
// Check if component name starts with uppercase (React component convention)
|
|
47
|
+
if (!componentName || componentName[0] !== componentName[0].toUpperCase()) {
|
|
48
|
+
return source;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get relative path from project root
|
|
52
|
+
const relativePath = path.relative(projectRoot, filename);
|
|
53
|
+
|
|
54
|
+
// Find the last import to insert our import after it
|
|
55
|
+
const imports = source.match(/^import.*$/gm) || [];
|
|
56
|
+
|
|
57
|
+
let modifiedSource;
|
|
58
|
+
|
|
59
|
+
if (imports.length === 0) {
|
|
60
|
+
// No imports found, add at top
|
|
61
|
+
const importStatement = `import { withCursorButton } from '${importPath}';\n`;
|
|
62
|
+
const wrappedExport = `export default withCursorButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
|
|
63
|
+
modifiedSource = importStatement + source.replace(/export\s+default\s+\w+;?/, wrappedExport);
|
|
64
|
+
} else {
|
|
65
|
+
const lastImportIndex = source.lastIndexOf(imports[imports.length - 1]) + imports[imports.length - 1].length;
|
|
66
|
+
|
|
67
|
+
// Add import statement
|
|
68
|
+
const importStatement = `\nimport { withCursorButton } from '${importPath}';`;
|
|
69
|
+
|
|
70
|
+
// Replace the export
|
|
71
|
+
const wrappedExport = `export default withCursorButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
|
|
72
|
+
|
|
73
|
+
modifiedSource = source.slice(0, lastImportIndex) +
|
|
74
|
+
importStatement +
|
|
75
|
+
source.slice(lastImportIndex).replace(/export\s+default\s+\w+;?/, wrappedExport);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return modifiedSource;
|
|
79
|
+
};
|
package/lib/plugin.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the NextJS Cursor Helper plugin
|
|
5
|
+
* @typedef {Object} CursorHelperOptions
|
|
6
|
+
* @property {string[]} [componentPaths=['src/components']] - Paths to scan for React components
|
|
7
|
+
* @property {string} [projectRoot=process.cwd()] - Root directory of the project
|
|
8
|
+
* @property {string} [importPath='nextjs-cursor-helper/withCursorButton'] - Import path for withCursorButton
|
|
9
|
+
* @property {boolean} [enabled=process.env.NODE_ENV === 'development'] - Enable/disable the plugin
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* NextJS Cursor Helper plugin
|
|
14
|
+
* @param {CursorHelperOptions} options - Configuration options
|
|
15
|
+
* @returns {function} Next.js config modifier function
|
|
16
|
+
*/
|
|
17
|
+
function withCursorHelper(options = {}) {
|
|
18
|
+
const defaultOptions = {
|
|
19
|
+
componentPaths: ['src/components'],
|
|
20
|
+
projectRoot: process.cwd(),
|
|
21
|
+
importPath: 'nextjs-cursor-helper/withCursorButton',
|
|
22
|
+
enabled: process.env.NODE_ENV === 'development'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const config = { ...defaultOptions, ...options };
|
|
26
|
+
|
|
27
|
+
return (nextConfig = {}) => {
|
|
28
|
+
return {
|
|
29
|
+
...nextConfig,
|
|
30
|
+
webpack: (webpackConfig, context) => {
|
|
31
|
+
const { dev, isServer } = context;
|
|
32
|
+
|
|
33
|
+
// Only apply in development and for client-side
|
|
34
|
+
if (config.enabled && dev && !isServer) {
|
|
35
|
+
const rule = {
|
|
36
|
+
test: /\.tsx?$/,
|
|
37
|
+
include: config.componentPaths.map(p => path.resolve(config.projectRoot, p)),
|
|
38
|
+
use: [
|
|
39
|
+
{
|
|
40
|
+
loader: require.resolve('./loader'),
|
|
41
|
+
options: config
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
enforce: 'pre'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
webpackConfig.module.rules.unshift(rule);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Call the existing webpack function if it exists
|
|
51
|
+
if (typeof nextConfig.webpack === 'function') {
|
|
52
|
+
return nextConfig.webpack(webpackConfig, context);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return webpackConfig;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = withCursorHelper;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface WithCursorButtonOptions {
|
|
3
|
+
projectRoot?: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function withCursorButton<T extends object>(WrappedComponent: React.ComponentType<T>, filePath: string, options?: WithCursorButtonOptions): React.FC<T>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.withCursorButton = withCursorButton;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const CursorButton = ({ filePath, projectRoot }) => {
|
|
8
|
+
const handleClick = () => {
|
|
9
|
+
const fullPath = projectRoot ? `${projectRoot}/${filePath}` : filePath;
|
|
10
|
+
window.open(`cursor://file${fullPath}`, '_blank');
|
|
11
|
+
};
|
|
12
|
+
return ((0, jsx_runtime_1.jsx)("button", { onClick: handleClick, style: {
|
|
13
|
+
position: 'absolute',
|
|
14
|
+
top: '8px',
|
|
15
|
+
right: '8px',
|
|
16
|
+
background: '#007acc',
|
|
17
|
+
color: 'white',
|
|
18
|
+
border: 'none',
|
|
19
|
+
borderRadius: '4px',
|
|
20
|
+
padding: '4px 8px',
|
|
21
|
+
fontSize: '12px',
|
|
22
|
+
cursor: 'pointer',
|
|
23
|
+
zIndex: 1000,
|
|
24
|
+
opacity: 0.7,
|
|
25
|
+
transition: 'opacity 0.2s',
|
|
26
|
+
fontFamily: 'monospace'
|
|
27
|
+
}, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.7'), title: `Open ${filePath} in Cursor`, children: "\uD83D\uDCDD" }));
|
|
28
|
+
};
|
|
29
|
+
function withCursorButton(WrappedComponent, filePath, options = {}) {
|
|
30
|
+
const { projectRoot, enabled = process.env.NODE_ENV === 'development' } = options;
|
|
31
|
+
const WithCursorButtonComponent = (props) => {
|
|
32
|
+
const [isClient, setIsClient] = (0, react_1.useState)(false);
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
setIsClient(true);
|
|
35
|
+
}, []);
|
|
36
|
+
// In production or when disabled, just return the component without wrapper
|
|
37
|
+
if (!enabled || !isClient) {
|
|
38
|
+
return (0, jsx_runtime_1.jsx)(WrappedComponent, { ...props });
|
|
39
|
+
}
|
|
40
|
+
return ((0, jsx_runtime_1.jsxs)("div", { style: { position: 'relative' }, children: [(0, jsx_runtime_1.jsx)(CursorButton, { filePath: filePath, projectRoot: projectRoot }), (0, jsx_runtime_1.jsx)(WrappedComponent, { ...props })] }));
|
|
41
|
+
};
|
|
42
|
+
WithCursorButtonComponent.displayName = `withCursorButton(${WrappedComponent.displayName || WrappedComponent.name})`;
|
|
43
|
+
return WithCursorButtonComponent;
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-ide-helper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Next.js plugin that automatically adds cursor buttons to React components for seamless IDE integration",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepare": "npm run build",
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nextjs",
|
|
18
|
+
"react",
|
|
19
|
+
"cursor",
|
|
20
|
+
"ide",
|
|
21
|
+
"development",
|
|
22
|
+
"webpack",
|
|
23
|
+
"plugin",
|
|
24
|
+
"components"
|
|
25
|
+
],
|
|
26
|
+
"author": "Your Name",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"next": ">=13.0.0",
|
|
30
|
+
"react": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.19.9",
|
|
34
|
+
"@types/react": "^18.3.23",
|
|
35
|
+
"typescript": "^5.8.3"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/yourusername/nextjs-cursor-helper.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/yourusername/nextjs-cursor-helper/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/yourusername/nextjs-cursor-helper#readme"
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { withCursorButton } from './withCursorButton';
|
|
3
|
+
export type { WithCursorButtonOptions } from './withCursorButton';
|
|
4
|
+
|
|
5
|
+
// Next.js plugin
|
|
6
|
+
const withCursorHelper = require('./plugin');
|
|
7
|
+
export default withCursorHelper;
|
|
8
|
+
export { withCursorHelper };
|
package/src/loader.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webpack loader that automatically wraps React components with cursor buttons
|
|
5
|
+
* @param {string} source - The source code of the file
|
|
6
|
+
* @returns {string} - The transformed source code
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function cursorButtonLoader(source) {
|
|
9
|
+
const filename = this.resourcePath;
|
|
10
|
+
const options = this.getOptions() || {};
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
componentPaths = ['src/components'],
|
|
14
|
+
projectRoot = process.cwd(),
|
|
15
|
+
importPath = 'nextjs-cursor-helper/withCursorButton',
|
|
16
|
+
enabled = process.env.NODE_ENV === 'development'
|
|
17
|
+
} = options;
|
|
18
|
+
|
|
19
|
+
// Only process if enabled
|
|
20
|
+
if (!enabled) {
|
|
21
|
+
return source;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Only process files in specified component directories
|
|
25
|
+
const shouldProcess = componentPaths.some(componentPath =>
|
|
26
|
+
filename.includes(path.resolve(projectRoot, componentPath))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!shouldProcess || (!filename.endsWith('.tsx') && !filename.endsWith('.jsx'))) {
|
|
30
|
+
return source;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if it's already wrapped
|
|
34
|
+
if (source.includes('withCursorButton')) {
|
|
35
|
+
return source;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if it has a default export that looks like a React component
|
|
39
|
+
const defaultExportMatch = source.match(/export\s+default\s+(\w+)/);
|
|
40
|
+
if (!defaultExportMatch) {
|
|
41
|
+
return source;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const componentName = defaultExportMatch[1];
|
|
45
|
+
|
|
46
|
+
// Check if component name starts with uppercase (React component convention)
|
|
47
|
+
if (!componentName || componentName[0] !== componentName[0].toUpperCase()) {
|
|
48
|
+
return source;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get relative path from project root
|
|
52
|
+
const relativePath = path.relative(projectRoot, filename);
|
|
53
|
+
|
|
54
|
+
// Find the last import to insert our import after it
|
|
55
|
+
const imports = source.match(/^import.*$/gm) || [];
|
|
56
|
+
|
|
57
|
+
let modifiedSource;
|
|
58
|
+
|
|
59
|
+
if (imports.length === 0) {
|
|
60
|
+
// No imports found, add at top
|
|
61
|
+
const importStatement = `import { withCursorButton } from '${importPath}';\n`;
|
|
62
|
+
const wrappedExport = `export default withCursorButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
|
|
63
|
+
modifiedSource = importStatement + source.replace(/export\s+default\s+\w+;?/, wrappedExport);
|
|
64
|
+
} else {
|
|
65
|
+
const lastImportIndex = source.lastIndexOf(imports[imports.length - 1]) + imports[imports.length - 1].length;
|
|
66
|
+
|
|
67
|
+
// Add import statement
|
|
68
|
+
const importStatement = `\nimport { withCursorButton } from '${importPath}';`;
|
|
69
|
+
|
|
70
|
+
// Replace the export
|
|
71
|
+
const wrappedExport = `export default withCursorButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
|
|
72
|
+
|
|
73
|
+
modifiedSource = source.slice(0, lastImportIndex) +
|
|
74
|
+
importStatement +
|
|
75
|
+
source.slice(lastImportIndex).replace(/export\s+default\s+\w+;?/, wrappedExport);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return modifiedSource;
|
|
79
|
+
};
|
package/src/plugin.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the NextJS Cursor Helper plugin
|
|
5
|
+
* @typedef {Object} CursorHelperOptions
|
|
6
|
+
* @property {string[]} [componentPaths=['src/components']] - Paths to scan for React components
|
|
7
|
+
* @property {string} [projectRoot=process.cwd()] - Root directory of the project
|
|
8
|
+
* @property {string} [importPath='nextjs-cursor-helper/withCursorButton'] - Import path for withCursorButton
|
|
9
|
+
* @property {boolean} [enabled=process.env.NODE_ENV === 'development'] - Enable/disable the plugin
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* NextJS Cursor Helper plugin
|
|
14
|
+
* @param {CursorHelperOptions} options - Configuration options
|
|
15
|
+
* @returns {function} Next.js config modifier function
|
|
16
|
+
*/
|
|
17
|
+
function withCursorHelper(options = {}) {
|
|
18
|
+
const defaultOptions = {
|
|
19
|
+
componentPaths: ['src/components'],
|
|
20
|
+
projectRoot: process.cwd(),
|
|
21
|
+
importPath: 'nextjs-cursor-helper/withCursorButton',
|
|
22
|
+
enabled: process.env.NODE_ENV === 'development'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const config = { ...defaultOptions, ...options };
|
|
26
|
+
|
|
27
|
+
return (nextConfig = {}) => {
|
|
28
|
+
return {
|
|
29
|
+
...nextConfig,
|
|
30
|
+
webpack: (webpackConfig, context) => {
|
|
31
|
+
const { dev, isServer } = context;
|
|
32
|
+
|
|
33
|
+
// Only apply in development and for client-side
|
|
34
|
+
if (config.enabled && dev && !isServer) {
|
|
35
|
+
const rule = {
|
|
36
|
+
test: /\.tsx?$/,
|
|
37
|
+
include: config.componentPaths.map(p => path.resolve(config.projectRoot, p)),
|
|
38
|
+
use: [
|
|
39
|
+
{
|
|
40
|
+
loader: require.resolve('./loader.js'),
|
|
41
|
+
options: config
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
enforce: 'pre'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
webpackConfig.module.rules.unshift(rule);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Call the existing webpack function if it exists
|
|
51
|
+
if (typeof nextConfig.webpack === 'function') {
|
|
52
|
+
return nextConfig.webpack(webpackConfig, context);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return webpackConfig;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = withCursorHelper;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
interface CursorButtonProps {
|
|
5
|
+
filePath: string;
|
|
6
|
+
projectRoot?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CursorButton: React.FC<CursorButtonProps> = ({ filePath, projectRoot }) => {
|
|
10
|
+
const handleClick = () => {
|
|
11
|
+
const fullPath = projectRoot ? `${projectRoot}/${filePath}` : filePath;
|
|
12
|
+
window.open(`cursor://file${fullPath}`, '_blank');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<button
|
|
17
|
+
onClick={handleClick}
|
|
18
|
+
style={{
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
top: '8px',
|
|
21
|
+
right: '8px',
|
|
22
|
+
background: '#007acc',
|
|
23
|
+
color: 'white',
|
|
24
|
+
border: 'none',
|
|
25
|
+
borderRadius: '4px',
|
|
26
|
+
padding: '4px 8px',
|
|
27
|
+
fontSize: '12px',
|
|
28
|
+
cursor: 'pointer',
|
|
29
|
+
zIndex: 1000,
|
|
30
|
+
opacity: 0.7,
|
|
31
|
+
transition: 'opacity 0.2s',
|
|
32
|
+
fontFamily: 'monospace'
|
|
33
|
+
}}
|
|
34
|
+
onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
|
|
35
|
+
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.7')}
|
|
36
|
+
title={`Open ${filePath} in Cursor`}
|
|
37
|
+
>
|
|
38
|
+
📝
|
|
39
|
+
</button>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface WithCursorButtonOptions {
|
|
44
|
+
projectRoot?: string;
|
|
45
|
+
enabled?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function withCursorButton<T extends object>(
|
|
49
|
+
WrappedComponent: React.ComponentType<T>,
|
|
50
|
+
filePath: string,
|
|
51
|
+
options: WithCursorButtonOptions = {}
|
|
52
|
+
) {
|
|
53
|
+
const { projectRoot, enabled = process.env.NODE_ENV === 'development' } = options;
|
|
54
|
+
|
|
55
|
+
const WithCursorButtonComponent: React.FC<T> = (props) => {
|
|
56
|
+
const [isClient, setIsClient] = useState(false);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setIsClient(true);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
// In production or when disabled, just return the component without wrapper
|
|
63
|
+
if (!enabled || !isClient) {
|
|
64
|
+
return <WrappedComponent {...props} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div style={{ position: 'relative' }}>
|
|
69
|
+
<CursorButton filePath={filePath} projectRoot={projectRoot} />
|
|
70
|
+
<WrappedComponent {...props} />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
WithCursorButtonComponent.displayName = `withCursorButton(${WrappedComponent.displayName || WrappedComponent.name})`;
|
|
76
|
+
|
|
77
|
+
return WithCursorButtonComponent;
|
|
78
|
+
}
|