nextjs-ide-helper 1.2.0 → 1.3.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 +30 -2
- package/lib/__tests__/loader.test.js +480 -0
- package/lib/index.ts +8 -0
- package/lib/loader.js +222 -33
- package/lib/plugin.js +26 -2
- package/lib/withIdeButton.tsx +97 -0
- package/package.json +2 -1
- package/src/__tests__/loader.test.js +51 -0
- package/src/loader.js +28 -5
- package/src/plugin.js +25 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ const nextConfig = {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
module.exports = withIdeHelper({
|
|
58
|
-
componentPaths: ['src/components', 'components', 'src/ui'], // directories to scan
|
|
58
|
+
componentPaths: ['src/components', 'components', 'src/ui'], // directories to scan (supports glob patterns)
|
|
59
59
|
projectRoot: process.cwd(), // project root directory
|
|
60
60
|
importPath: 'nextjs-ide-plugin/withIdeButton', // import path for the HOC
|
|
61
61
|
enabled: process.env.NODE_ENV === 'development', // enable/disable
|
|
@@ -67,12 +67,34 @@ module.exports = withIdeHelper({
|
|
|
67
67
|
|
|
68
68
|
| Option | Type | Default | Description |
|
|
69
69
|
|--------|------|---------|-------------|
|
|
70
|
-
| `componentPaths` | `string[]` | `['src/components']` | Directories to scan for React components |
|
|
70
|
+
| `componentPaths` | `string[]` | `['src/components']` | Directories to scan for React components (supports glob patterns) |
|
|
71
71
|
| `projectRoot` | `string` | `process.cwd()` | Root directory of your project |
|
|
72
72
|
| `importPath` | `string` | `'nextjs-ide-plugin/withIdeButton'` | Import path for the withIdeButton HOC |
|
|
73
73
|
| `enabled` | `boolean` | `process.env.NODE_ENV === 'development'` | Enable/disable the plugin |
|
|
74
74
|
| `ideType` | `'cursor' \| 'vscode' \| 'webstorm' \| 'atom'` | `'cursor'` | IDE to open files in |
|
|
75
75
|
|
|
76
|
+
### Glob Pattern Support
|
|
77
|
+
|
|
78
|
+
The `componentPaths` option supports glob patterns for matching nested directory structures:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
module.exports = withIdeHelper({
|
|
82
|
+
componentPaths: [
|
|
83
|
+
'src/components/**', // matches all nested directories under src/components
|
|
84
|
+
'**/components/**', // matches components directories anywhere in the project
|
|
85
|
+
'app/**/ui/**', // matches ui directories nested anywhere under app
|
|
86
|
+
'modules/*/components/**' // matches components in any module subdirectory
|
|
87
|
+
]
|
|
88
|
+
})(nextConfig);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Glob Pattern Examples:**
|
|
92
|
+
- `*` - matches any characters within a single directory level
|
|
93
|
+
- `**` - matches any number of directories and subdirectories recursively
|
|
94
|
+
- `src/components/*` - matches `src/components/Button.tsx` but not `src/components/ui/Button.tsx`
|
|
95
|
+
- `src/components/**` - matches both `src/components/Button.tsx` and `src/components/ui/forms/Button.tsx`
|
|
96
|
+
- `**/components/**` - matches `app/components/Button.tsx`, `modules/feature/components/deep/Widget.tsx`, etc.
|
|
97
|
+
|
|
76
98
|
## Manual Usage
|
|
77
99
|
|
|
78
100
|
You can also manually wrap components:
|
|
@@ -305,6 +327,12 @@ See [CHANGELOG.md](./CHANGELOG.md) for detailed release notes.
|
|
|
305
327
|
|
|
306
328
|
### Recent Releases
|
|
307
329
|
|
|
330
|
+
### 1.3.0 - Glob Pattern Support
|
|
331
|
+
- Added support for glob patterns in `componentPaths` configuration
|
|
332
|
+
- Support for nested directory matching with `**` patterns
|
|
333
|
+
- Enhanced file path matching for complex project structures
|
|
334
|
+
- Updated tests and documentation
|
|
335
|
+
|
|
308
336
|
### 1.2.0 - Enhanced Component Support
|
|
309
337
|
- Added support for all React component export patterns
|
|
310
338
|
- AST-based code transformation for better reliability
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const loader = require('../loader.js');
|
|
3
|
+
|
|
4
|
+
describe('cursorButtonLoader', function() {
|
|
5
|
+
let mockContext;
|
|
6
|
+
|
|
7
|
+
beforeEach(function() {
|
|
8
|
+
mockContext = {
|
|
9
|
+
resourcePath: process.cwd() + '/src/components/Button.tsx',
|
|
10
|
+
getOptions: jest.fn(() => ({}))
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
process.env.NODE_ENV = 'development';
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(function() {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when enabled', function() {
|
|
21
|
+
beforeEach(function() {
|
|
22
|
+
mockContext.getOptions.mockReturnValue({ enabled: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should wrap a React component with withIdeButton', function() {
|
|
26
|
+
const source = `import React from 'react';
|
|
27
|
+
|
|
28
|
+
const Button = () => {
|
|
29
|
+
return <button>Click me</button>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default Button;`;
|
|
33
|
+
|
|
34
|
+
const result = loader.call(mockContext, source);
|
|
35
|
+
|
|
36
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
37
|
+
expect(result).toContain("export default withIdeButton(Button,");
|
|
38
|
+
expect(result).toMatch(/projectRoot: ['"][^'"]*['"]/); // Accept both single and double quotes and any valid path
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should handle JSX files', function() {
|
|
42
|
+
mockContext.resourcePath = process.cwd() + '/src/components/Button.jsx';
|
|
43
|
+
const source = `const Button = () => <button>Click</button>;
|
|
44
|
+
export default Button;`;
|
|
45
|
+
|
|
46
|
+
const result = loader.call(mockContext, source);
|
|
47
|
+
|
|
48
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
49
|
+
expect(result).toContain("export default withIdeButton(Button,");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle components with existing imports', function() {
|
|
53
|
+
const source = `import React from 'react';
|
|
54
|
+
import { useState } from 'react';
|
|
55
|
+
|
|
56
|
+
const Counter = () => {
|
|
57
|
+
const [count, setCount] = useState(0);
|
|
58
|
+
return <div>{count}</div>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default Counter;`;
|
|
62
|
+
|
|
63
|
+
const result = loader.call(mockContext, source);
|
|
64
|
+
|
|
65
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
66
|
+
expect(result).toContain("export default withIdeButton(Counter,");
|
|
67
|
+
|
|
68
|
+
const lines = result.split('\n');
|
|
69
|
+
const importIndex = lines.findIndex(line => line.includes("import { withIdeButton }"));
|
|
70
|
+
const useStateIndex = lines.findIndex(line => line.includes("import { useState }"));
|
|
71
|
+
expect(importIndex).toBeGreaterThan(useStateIndex);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle components with no imports', function() {
|
|
75
|
+
const source = `const SimpleButton = () => {
|
|
76
|
+
return <button>Simple</button>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default SimpleButton;`;
|
|
80
|
+
|
|
81
|
+
const result = loader.call(mockContext, source);
|
|
82
|
+
|
|
83
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
84
|
+
expect(result).toContain("export default withIdeButton(SimpleButton,");
|
|
85
|
+
expect(result.indexOf("import { withIdeButton }")).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should include relative path in the wrapper', function() {
|
|
89
|
+
mockContext.resourcePath = process.cwd() + '/src/components/ui/Button.tsx';
|
|
90
|
+
mockContext.getOptions.mockReturnValue({
|
|
91
|
+
enabled: true,
|
|
92
|
+
projectRoot: process.cwd()
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const source = `const Button = () => <button>Click</button>;
|
|
96
|
+
export default Button;`;
|
|
97
|
+
|
|
98
|
+
const result = loader.call(mockContext, source);
|
|
99
|
+
|
|
100
|
+
expect(result).toContain("'src/components/ui/Button.tsx'");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should use custom import path when specified', function() {
|
|
104
|
+
mockContext.getOptions.mockReturnValue({
|
|
105
|
+
enabled: true,
|
|
106
|
+
importPath: 'custom-ide-helper'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const source = `const Button = () => <button>Click</button>;
|
|
110
|
+
export default Button;`;
|
|
111
|
+
|
|
112
|
+
const result = loader.call(mockContext, source);
|
|
113
|
+
|
|
114
|
+
expect(result).toContain("import { withIdeButton } from 'custom-ide-helper';");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('when disabled', function() {
|
|
119
|
+
it('should return source unchanged when enabled is false', function() {
|
|
120
|
+
mockContext.getOptions.mockReturnValue({ enabled: false });
|
|
121
|
+
const source = `const Button = () => <button>Click</button>;
|
|
122
|
+
export default Button;`;
|
|
123
|
+
|
|
124
|
+
const result = loader.call(mockContext, source);
|
|
125
|
+
|
|
126
|
+
expect(result).toBe(source);
|
|
127
|
+
expect(result).not.toContain('withIdeButton');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return source unchanged when NODE_ENV is production and enabled not explicitly set', function() {
|
|
131
|
+
process.env.NODE_ENV = 'production';
|
|
132
|
+
const source = `const Button = () => <button>Click</button>;
|
|
133
|
+
export default Button;`;
|
|
134
|
+
|
|
135
|
+
const result = loader.call(mockContext, source);
|
|
136
|
+
|
|
137
|
+
expect(result).toBe(source);
|
|
138
|
+
expect(result).not.toContain('withIdeButton');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('file filtering', function() {
|
|
143
|
+
beforeEach(function() {
|
|
144
|
+
mockContext.getOptions.mockReturnValue({ enabled: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should not process files outside component directories', function() {
|
|
148
|
+
mockContext.resourcePath = process.cwd() + '/src/utils/helper.tsx';
|
|
149
|
+
const source = `const Helper = () => <div>Helper</div>;
|
|
150
|
+
export default Helper;`;
|
|
151
|
+
|
|
152
|
+
const result = loader.call(mockContext, source);
|
|
153
|
+
|
|
154
|
+
expect(result).toBe(source);
|
|
155
|
+
expect(result).not.toContain('withIdeButton');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should process files in custom component directories', function() {
|
|
159
|
+
mockContext.resourcePath = process.cwd() + '/src/widgets/MyWidget.tsx';
|
|
160
|
+
mockContext.getOptions.mockReturnValue({
|
|
161
|
+
enabled: true,
|
|
162
|
+
componentPaths: ['src/widgets'],
|
|
163
|
+
projectRoot: process.cwd()
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const source = `const MyWidget = () => <div>Widget</div>;
|
|
167
|
+
export default MyWidget;`;
|
|
168
|
+
|
|
169
|
+
const result = loader.call(mockContext, source);
|
|
170
|
+
|
|
171
|
+
expect(result).toContain('withIdeButton');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should not process non-JSX/TSX files', function() {
|
|
175
|
+
mockContext.resourcePath = process.cwd() + '/src/components/Button.js';
|
|
176
|
+
const source = `const Button = () => <button>Click</button>;
|
|
177
|
+
export default Button;`;
|
|
178
|
+
|
|
179
|
+
const result = loader.call(mockContext, source);
|
|
180
|
+
|
|
181
|
+
expect(result).toBe(source);
|
|
182
|
+
expect(result).not.toContain('withIdeButton');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('component detection', function() {
|
|
187
|
+
beforeEach(function() {
|
|
188
|
+
mockContext.getOptions.mockReturnValue({ enabled: true });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should not process files without default export', function() {
|
|
192
|
+
const source = `export const Button = () => <button>Click</button>;`;
|
|
193
|
+
|
|
194
|
+
const result = loader.call(mockContext, source);
|
|
195
|
+
|
|
196
|
+
expect(result).toBe(source);
|
|
197
|
+
expect(result).not.toContain('withIdeButton');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should not process files with lowercase default export', function() {
|
|
201
|
+
const source = `const button = () => <button>Click</button>;
|
|
202
|
+
export default button;`;
|
|
203
|
+
|
|
204
|
+
const result = loader.call(mockContext, source);
|
|
205
|
+
|
|
206
|
+
expect(result).toBe(source);
|
|
207
|
+
expect(result).not.toContain('withIdeButton');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should process files with PascalCase default export', function() {
|
|
211
|
+
const source = `const MyButton = () => <button>Click</button>;
|
|
212
|
+
export default MyButton;`;
|
|
213
|
+
|
|
214
|
+
const result = loader.call(mockContext, source);
|
|
215
|
+
|
|
216
|
+
expect(result).toContain('withIdeButton');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should process direct export default function declarations', function() {
|
|
220
|
+
const source = `export default function MyCoolComponent() {
|
|
221
|
+
return <div>My Cool Component</div>;
|
|
222
|
+
}`;
|
|
223
|
+
|
|
224
|
+
const result = loader.call(mockContext, source);
|
|
225
|
+
|
|
226
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
227
|
+
expect(result).toContain("export default withIdeButton(MyCoolComponent,");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should process direct export default arrow function expressions', function() {
|
|
231
|
+
const source = `export default () => {
|
|
232
|
+
return <div>Anonymous Component</div>;
|
|
233
|
+
};`;
|
|
234
|
+
|
|
235
|
+
const result = loader.call(mockContext, source);
|
|
236
|
+
|
|
237
|
+
// Should process anonymous components using file path
|
|
238
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
239
|
+
expect(result).toContain("export default withIdeButton(");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should process anonymous function expressions', function() {
|
|
243
|
+
const source = `export default function() {
|
|
244
|
+
return <div>Anonymous Function Component</div>;
|
|
245
|
+
}`;
|
|
246
|
+
|
|
247
|
+
const result = loader.call(mockContext, source);
|
|
248
|
+
|
|
249
|
+
// Should process anonymous function components
|
|
250
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
251
|
+
expect(result).toContain("export default withIdeButton(");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should process direct export default class components', function() {
|
|
255
|
+
const source = `import React from 'react';
|
|
256
|
+
|
|
257
|
+
export default class MyClassComponent extends React.Component {
|
|
258
|
+
render() {
|
|
259
|
+
return <div>Class Component</div>;
|
|
260
|
+
}
|
|
261
|
+
}`;
|
|
262
|
+
|
|
263
|
+
const result = loader.call(mockContext, source);
|
|
264
|
+
|
|
265
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
266
|
+
expect(result).toContain("export default withIdeButton(MyClassComponent,");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should not process lowercase direct export default functions', function() {
|
|
270
|
+
const source = `export default function myLowercaseComponent() {
|
|
271
|
+
return <div>Lowercase Component</div>;
|
|
272
|
+
}`;
|
|
273
|
+
|
|
274
|
+
const result = loader.call(mockContext, source);
|
|
275
|
+
|
|
276
|
+
expect(result).toBe(source);
|
|
277
|
+
expect(result).not.toContain('withIdeButton');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle direct export default with complex function names', function() {
|
|
281
|
+
const source = `export default function MyComplexComponent123WithNumbers() {
|
|
282
|
+
return <div>Complex Named Component</div>;
|
|
283
|
+
}`;
|
|
284
|
+
|
|
285
|
+
const result = loader.call(mockContext, source);
|
|
286
|
+
|
|
287
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
288
|
+
expect(result).toContain("export default withIdeButton(MyComplexComponent123WithNumbers,");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should handle function components with TypeScript', function() {
|
|
292
|
+
const source = `interface Props {
|
|
293
|
+
title: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export default function TypeScriptComponent(props: Props) {
|
|
297
|
+
return <div>{props.title}</div>;
|
|
298
|
+
}`;
|
|
299
|
+
|
|
300
|
+
const result = loader.call(mockContext, source);
|
|
301
|
+
|
|
302
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
303
|
+
expect(result).toContain("export default withIdeButton(TypeScriptComponent,");
|
|
304
|
+
expect(result).toContain("function TypeScriptComponent(props: Props)");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should handle class components with TypeScript', function() {
|
|
308
|
+
const source = `import React, { Component } from 'react';
|
|
309
|
+
|
|
310
|
+
interface Props {
|
|
311
|
+
name: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export default class TSClassComponent extends Component<Props> {
|
|
315
|
+
render() {
|
|
316
|
+
return <div>Hello {this.props.name}</div>;
|
|
317
|
+
}
|
|
318
|
+
}`;
|
|
319
|
+
|
|
320
|
+
const result = loader.call(mockContext, source);
|
|
321
|
+
|
|
322
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
323
|
+
expect(result).toContain("export default withIdeButton(TSClassComponent,");
|
|
324
|
+
expect(result).toContain("class TSClassComponent extends Component<Props>");
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('already wrapped components', function() {
|
|
329
|
+
beforeEach(function() {
|
|
330
|
+
mockContext.getOptions.mockReturnValue({ enabled: true });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should not wrap components that are already wrapped', function() {
|
|
334
|
+
const source = `import { withIdeButton } from 'nextjs-ide-helper';
|
|
335
|
+
|
|
336
|
+
const Button = () => <button>Click</button>;
|
|
337
|
+
|
|
338
|
+
export default withIdeButton(Button, 'src/components/Button.tsx');`;
|
|
339
|
+
|
|
340
|
+
const result = loader.call(mockContext, source);
|
|
341
|
+
|
|
342
|
+
expect(result).toBe(source);
|
|
343
|
+
expect((result.match(/withIdeButton/g) || []).length).toBe(2);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('edge cases', function() {
|
|
348
|
+
beforeEach(function() {
|
|
349
|
+
mockContext.getOptions.mockReturnValue({ enabled: true });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should handle export default with semicolon', function() {
|
|
353
|
+
const source = `const Button = () => <button>Click</button>;
|
|
354
|
+
export default Button;`;
|
|
355
|
+
|
|
356
|
+
const result = loader.call(mockContext, source);
|
|
357
|
+
|
|
358
|
+
expect(result).toContain("export default withIdeButton(Button,");
|
|
359
|
+
expect(result).not.toContain("export default Button;");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should handle export default without semicolon', function() {
|
|
363
|
+
const source = `const Button = () => <button>Click</button>
|
|
364
|
+
export default Button`;
|
|
365
|
+
|
|
366
|
+
const result = loader.call(mockContext, source);
|
|
367
|
+
|
|
368
|
+
expect(result).toContain("export default withIdeButton(Button,");
|
|
369
|
+
expect(result).not.toContain("export default Button");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should handle complex component names', function() {
|
|
373
|
+
const source = `const MyComplexComponent123 = () => <div>Complex</div>;
|
|
374
|
+
export default MyComplexComponent123;`;
|
|
375
|
+
|
|
376
|
+
const result = loader.call(mockContext, source);
|
|
377
|
+
|
|
378
|
+
expect(result).toContain("export default withIdeButton(MyComplexComponent123,");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should handle files with multiple component paths', function() {
|
|
382
|
+
mockContext.resourcePath = process.cwd() + '/lib/components/Button.tsx';
|
|
383
|
+
mockContext.getOptions.mockReturnValue({
|
|
384
|
+
enabled: true,
|
|
385
|
+
componentPaths: ['src/components', 'lib/components'],
|
|
386
|
+
projectRoot: process.cwd()
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const source = `const Button = () => <button>Click</button>;
|
|
390
|
+
export default Button;`;
|
|
391
|
+
|
|
392
|
+
const result = loader.call(mockContext, source);
|
|
393
|
+
|
|
394
|
+
expect(result).toContain('withIdeButton');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should support regex patterns in component paths for nested directories', function() {
|
|
398
|
+
mockContext.resourcePath = process.cwd() + '/src/components/ui/forms/Button.tsx';
|
|
399
|
+
mockContext.getOptions.mockReturnValue({
|
|
400
|
+
enabled: true,
|
|
401
|
+
componentPaths: ['src/components/**'],
|
|
402
|
+
projectRoot: process.cwd()
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const source = `const Button = () => <button>Click</button>;
|
|
406
|
+
export default Button;`;
|
|
407
|
+
|
|
408
|
+
const result = loader.call(mockContext, source);
|
|
409
|
+
|
|
410
|
+
expect(result).toContain('withIdeButton');
|
|
411
|
+
expect(result).toContain("'src/components/ui/forms/Button.tsx'");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should support regex patterns matching any nested directory structure', function() {
|
|
415
|
+
mockContext.resourcePath = process.cwd() + '/app/components/dashboard/widgets/Chart.tsx';
|
|
416
|
+
mockContext.getOptions.mockReturnValue({
|
|
417
|
+
enabled: true,
|
|
418
|
+
componentPaths: ['**/components/**'],
|
|
419
|
+
projectRoot: process.cwd()
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const source = `const Chart = () => <div>Chart</div>;
|
|
423
|
+
export default Chart;`;
|
|
424
|
+
|
|
425
|
+
const result = loader.call(mockContext, source);
|
|
426
|
+
|
|
427
|
+
expect(result).toContain('withIdeButton');
|
|
428
|
+
expect(result).toContain("'app/components/dashboard/widgets/Chart.tsx'");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should support multiple regex patterns for different nested structures', function() {
|
|
432
|
+
mockContext.resourcePath = process.cwd() + '/modules/feature-a/components/deep/nested/Widget.tsx';
|
|
433
|
+
mockContext.getOptions.mockReturnValue({
|
|
434
|
+
enabled: true,
|
|
435
|
+
componentPaths: ['src/components/**', 'modules/**/components/**', 'lib/ui/**'],
|
|
436
|
+
projectRoot: process.cwd()
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const source = `const Widget = () => <div>Widget</div>;
|
|
440
|
+
export default Widget;`;
|
|
441
|
+
|
|
442
|
+
const result = loader.call(mockContext, source);
|
|
443
|
+
|
|
444
|
+
expect(result).toContain('withIdeButton');
|
|
445
|
+
expect(result).toContain("'modules/feature-a/components/deep/nested/Widget.tsx'");
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('options handling', function() {
|
|
450
|
+
it('should use default options when none provided', function() {
|
|
451
|
+
mockContext.getOptions.mockReturnValue(null);
|
|
452
|
+
process.env.NODE_ENV = 'development';
|
|
453
|
+
|
|
454
|
+
const source = `const Button = () => <button>Click</button>;
|
|
455
|
+
export default Button;`;
|
|
456
|
+
|
|
457
|
+
const result = loader.call(mockContext, source);
|
|
458
|
+
|
|
459
|
+
expect(result).toContain('withIdeButton');
|
|
460
|
+
expect(result).toContain("'nextjs-ide-helper'");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should handle custom project root', function() {
|
|
464
|
+
const customRoot = '/custom/project';
|
|
465
|
+
mockContext.resourcePath = customRoot + '/src/components/Button.tsx';
|
|
466
|
+
mockContext.getOptions.mockReturnValue({
|
|
467
|
+
enabled: true,
|
|
468
|
+
projectRoot: customRoot
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const source = `const Button = () => <button>Click</button>;
|
|
472
|
+
export default Button;`;
|
|
473
|
+
|
|
474
|
+
const result = loader.call(mockContext, source);
|
|
475
|
+
|
|
476
|
+
expect(result).toMatch(new RegExp("projectRoot:\\s*['\"]" + customRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + "['\"]"));
|
|
477
|
+
expect(result).toContain("'src/components/Button.tsx'");
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
package/lib/index.ts
ADDED
package/lib/loader.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const { parse } = require('@babel/parser');
|
|
3
|
+
const traverse = require('@babel/traverse').default;
|
|
4
|
+
const generate = require('@babel/generator').default;
|
|
5
|
+
const t = require('@babel/types');
|
|
6
|
+
const { minimatch } = require('minimatch');
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Webpack loader that automatically wraps React components with cursor buttons
|
|
@@ -9,6 +14,8 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
9
14
|
const filename = this.resourcePath;
|
|
10
15
|
const options = this.getOptions() || {};
|
|
11
16
|
|
|
17
|
+
console.log('🔧 Loader called for file:', filename);
|
|
18
|
+
|
|
12
19
|
const {
|
|
13
20
|
componentPaths = ['src/components'],
|
|
14
21
|
projectRoot = process.cwd(),
|
|
@@ -18,62 +25,244 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
18
25
|
|
|
19
26
|
// Only process if enabled
|
|
20
27
|
if (!enabled) {
|
|
28
|
+
console.log('🔧 Loader disabled, returning original source');
|
|
21
29
|
return source;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
// Only process files in specified component directories
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
const relativePath = path.relative(projectRoot, filename);
|
|
34
|
+
console.log('🔧 Processing file:', relativePath, 'against paths:', componentPaths);
|
|
35
|
+
|
|
36
|
+
const shouldProcess = componentPaths.some(componentPath => {
|
|
37
|
+
console.log('🔧 Checking path:', componentPath);
|
|
38
|
+
// Check if componentPath contains glob patterns
|
|
39
|
+
if (componentPath.includes('*')) {
|
|
40
|
+
const matches = minimatch(relativePath, componentPath);
|
|
41
|
+
console.log('🔧 Glob match result:', matches, 'for pattern:', componentPath);
|
|
42
|
+
return matches;
|
|
43
|
+
} else {
|
|
44
|
+
// Fallback to the original behavior for non-glob patterns
|
|
45
|
+
const matches = filename.includes(path.resolve(projectRoot, componentPath));
|
|
46
|
+
console.log('🔧 Direct path match result:', matches);
|
|
47
|
+
return matches;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log('🔧 Should process:', shouldProcess);
|
|
28
52
|
|
|
29
53
|
if (!shouldProcess || (!filename.endsWith('.tsx') && !filename.endsWith('.jsx'))) {
|
|
54
|
+
console.log('🔧 File does not match criteria, skipping');
|
|
30
55
|
return source;
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
// Check if it's already wrapped
|
|
58
|
+
// Check if it's already wrapped using simple string check for performance
|
|
34
59
|
if (source.includes('withIdeButton')) {
|
|
60
|
+
console.log('🔧 File already contains withIdeButton, skipping');
|
|
35
61
|
return source;
|
|
36
62
|
}
|
|
37
63
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
console.log('🔧 Proceeding to transform file:', relativePath);
|
|
65
|
+
|
|
66
|
+
let ast;
|
|
67
|
+
try {
|
|
68
|
+
// Parse the source code into an AST
|
|
69
|
+
ast = parse(source, {
|
|
70
|
+
sourceType: 'module',
|
|
71
|
+
plugins: [
|
|
72
|
+
'jsx',
|
|
73
|
+
'typescript',
|
|
74
|
+
'decorators-legacy',
|
|
75
|
+
'classProperties',
|
|
76
|
+
'objectRestSpread',
|
|
77
|
+
'asyncGenerators',
|
|
78
|
+
'functionBind',
|
|
79
|
+
'exportDefaultFrom',
|
|
80
|
+
'exportNamespaceFrom',
|
|
81
|
+
'dynamicImport',
|
|
82
|
+
'nullishCoalescingOperator',
|
|
83
|
+
'optionalChaining'
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// If parsing fails, return original source
|
|
88
|
+
return source;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let hasDefaultExport = false;
|
|
92
|
+
let defaultExportName = null;
|
|
93
|
+
let isAnonymousComponent = false;
|
|
94
|
+
let hasWithIdeButtonImport = false;
|
|
95
|
+
let lastImportPath = null;
|
|
96
|
+
let defaultExportPath = null;
|
|
97
|
+
|
|
98
|
+
// Traverse the AST to analyze the code
|
|
99
|
+
traverse(ast, {
|
|
100
|
+
ImportDeclaration(path) {
|
|
101
|
+
lastImportPath = path;
|
|
102
|
+
|
|
103
|
+
// Check if withIdeButton is already imported
|
|
104
|
+
if (path.node.source.value === importPath) {
|
|
105
|
+
path.node.specifiers.forEach(spec => {
|
|
106
|
+
if (t.isImportSpecifier(spec) && spec.imported.name === 'withIdeButton') {
|
|
107
|
+
hasWithIdeButtonImport = true;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
ExportDefaultDeclaration(path) {
|
|
114
|
+
hasDefaultExport = true;
|
|
115
|
+
defaultExportPath = path;
|
|
116
|
+
|
|
117
|
+
if (t.isIdentifier(path.node.declaration)) {
|
|
118
|
+
// export default ComponentName
|
|
119
|
+
defaultExportName = path.node.declaration.name;
|
|
120
|
+
} else if (t.isFunctionDeclaration(path.node.declaration)) {
|
|
121
|
+
if (path.node.declaration.id) {
|
|
122
|
+
// export default function ComponentName() {}
|
|
123
|
+
defaultExportName = path.node.declaration.id.name;
|
|
124
|
+
} else {
|
|
125
|
+
// export default function() {} - anonymous function
|
|
126
|
+
isAnonymousComponent = true;
|
|
127
|
+
}
|
|
128
|
+
} else if (t.isClassDeclaration(path.node.declaration) && path.node.declaration.id) {
|
|
129
|
+
// export default class ComponentName extends ... {}
|
|
130
|
+
defaultExportName = path.node.declaration.id.name;
|
|
131
|
+
} else if (t.isArrowFunctionExpression(path.node.declaration)) {
|
|
132
|
+
// export default () => {} - anonymous arrow function
|
|
133
|
+
isAnonymousComponent = true;
|
|
134
|
+
} else if (t.isVariableDeclaration(path.node.declaration)) {
|
|
135
|
+
// export default const ComponentName = ...
|
|
136
|
+
const declarator = path.node.declaration.declarations[0];
|
|
137
|
+
if (t.isIdentifier(declarator.id)) {
|
|
138
|
+
defaultExportName = declarator.id.name;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Stop traversal once we find the default export
|
|
143
|
+
path.stop();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Check if we should process this file
|
|
148
|
+
if (!hasDefaultExport || (!defaultExportName && !isAnonymousComponent)) {
|
|
41
149
|
return source;
|
|
42
150
|
}
|
|
43
151
|
|
|
44
|
-
const componentName = defaultExportMatch[1];
|
|
45
|
-
|
|
46
152
|
// Check if component name starts with uppercase (React component convention)
|
|
47
|
-
|
|
153
|
+
// Skip this check for anonymous components
|
|
154
|
+
if (defaultExportName && defaultExportName[0] !== defaultExportName[0].toUpperCase()) {
|
|
48
155
|
return source;
|
|
49
156
|
}
|
|
50
157
|
|
|
51
|
-
//
|
|
52
|
-
|
|
158
|
+
// If withIdeButton is already imported, don't process
|
|
159
|
+
if (hasWithIdeButtonImport) {
|
|
160
|
+
return source;
|
|
161
|
+
}
|
|
53
162
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
163
|
+
// relativePath already declared above for glob matching
|
|
164
|
+
|
|
165
|
+
// Transform the AST
|
|
166
|
+
let modified = false;
|
|
167
|
+
|
|
168
|
+
// Add the withIdeButton import
|
|
169
|
+
const withIdeButtonImport = t.importDeclaration(
|
|
170
|
+
[t.importSpecifier(t.identifier('withIdeButton'), t.identifier('withIdeButton'))],
|
|
171
|
+
t.stringLiteral(importPath)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Insert import after last existing import or at the beginning
|
|
175
|
+
if (lastImportPath) {
|
|
176
|
+
lastImportPath.insertAfter(withIdeButtonImport);
|
|
177
|
+
modified = true;
|
|
64
178
|
} else {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
179
|
+
// No imports found, add at the beginning
|
|
180
|
+
if (ast.program && ast.program.body && Array.isArray(ast.program.body)) {
|
|
181
|
+
ast.program.body.unshift(withIdeButtonImport);
|
|
182
|
+
modified = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Replace the default export with wrapped version
|
|
187
|
+
if (defaultExportPath && modified) {
|
|
188
|
+
let wrappedCall;
|
|
72
189
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
190
|
+
if (isAnonymousComponent) {
|
|
191
|
+
// For anonymous components, wrap the original function/arrow function
|
|
192
|
+
let componentExpression;
|
|
193
|
+
|
|
194
|
+
if (t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
|
|
195
|
+
// Convert anonymous function declaration to function expression
|
|
196
|
+
componentExpression = t.functionExpression(
|
|
197
|
+
null, // id is null for anonymous
|
|
198
|
+
defaultExportPath.node.declaration.params,
|
|
199
|
+
defaultExportPath.node.declaration.body,
|
|
200
|
+
defaultExportPath.node.declaration.generator,
|
|
201
|
+
defaultExportPath.node.declaration.async
|
|
202
|
+
);
|
|
203
|
+
} else {
|
|
204
|
+
// For arrow functions and other expressions, use directly
|
|
205
|
+
componentExpression = defaultExportPath.node.declaration;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
wrappedCall = t.callExpression(
|
|
209
|
+
t.identifier('withIdeButton'),
|
|
210
|
+
[
|
|
211
|
+
componentExpression,
|
|
212
|
+
t.stringLiteral(relativePath),
|
|
213
|
+
t.objectExpression([
|
|
214
|
+
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
215
|
+
])
|
|
216
|
+
]
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
// For named components, wrap with the component name identifier
|
|
220
|
+
wrappedCall = t.callExpression(
|
|
221
|
+
t.identifier('withIdeButton'),
|
|
222
|
+
[
|
|
223
|
+
t.identifier(defaultExportName),
|
|
224
|
+
t.stringLiteral(relativePath),
|
|
225
|
+
t.objectExpression([
|
|
226
|
+
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
227
|
+
])
|
|
228
|
+
]
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// If the export is a named function or class declaration, we need to handle it differently
|
|
233
|
+
if (!isAnonymousComponent &&
|
|
234
|
+
(t.isFunctionDeclaration(defaultExportPath.node.declaration) ||
|
|
235
|
+
t.isClassDeclaration(defaultExportPath.node.declaration))) {
|
|
236
|
+
// Insert the function/class declaration before the export
|
|
237
|
+
const declaration = defaultExportPath.node.declaration;
|
|
238
|
+
defaultExportPath.insertBefore(declaration);
|
|
239
|
+
|
|
240
|
+
// Replace the export declaration with the wrapped call
|
|
241
|
+
defaultExportPath.node.declaration = wrappedCall;
|
|
242
|
+
} else {
|
|
243
|
+
// For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
|
|
244
|
+
defaultExportPath.node.declaration = wrappedCall;
|
|
245
|
+
}
|
|
76
246
|
}
|
|
77
247
|
|
|
78
|
-
|
|
248
|
+
// Generate the transformed code
|
|
249
|
+
if (modified) {
|
|
250
|
+
try {
|
|
251
|
+
const result = generate(ast, {
|
|
252
|
+
retainLines: false,
|
|
253
|
+
compact: false,
|
|
254
|
+
jsescOption: {
|
|
255
|
+
quotes: 'single'
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return result.code;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// If generation fails, return original source
|
|
261
|
+
return source;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// If we reach here, nothing was modified
|
|
266
|
+
|
|
267
|
+
return source;
|
|
79
268
|
};
|
package/lib/plugin.js
CHANGED
|
@@ -9,6 +9,16 @@ const path = require('path');
|
|
|
9
9
|
* @property {boolean} [enabled=process.env.NODE_ENV === 'development'] - Enable/disable the plugin
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extract base directory from a glob pattern
|
|
14
|
+
* @param {string} globPattern - The glob pattern
|
|
15
|
+
* @returns {string} - The base directory
|
|
16
|
+
*/
|
|
17
|
+
function extractBaseDirectory(globPattern) {
|
|
18
|
+
// Remove glob patterns (**/ */ etc.) to get the base directory
|
|
19
|
+
return globPattern.replace(/\/\*\*.*$/, '').replace(/\/\*.*$/, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* NextJS Cursor Helper plugin
|
|
14
24
|
* @param {CursorHelperOptions} options - Configuration options
|
|
@@ -23,21 +33,32 @@ function withCursorHelper(options = {}) {
|
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
const config = { ...defaultOptions, ...options };
|
|
36
|
+
|
|
37
|
+
console.log('🔧 Plugin initialized with config:', config);
|
|
26
38
|
|
|
27
39
|
return (nextConfig = {}) => {
|
|
28
40
|
return {
|
|
29
41
|
...nextConfig,
|
|
30
42
|
webpack: (webpackConfig, context) => {
|
|
31
43
|
const { dev, isServer } = context;
|
|
44
|
+
|
|
45
|
+
console.log('🔧 Webpack config called:', { dev, isServer, enabled: config.enabled });
|
|
32
46
|
|
|
33
47
|
// Only apply in development and for client-side
|
|
34
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
|
+
|
|
54
|
+
console.log('🔧 Adding webpack rule with include directories:', includeDirectories);
|
|
55
|
+
|
|
35
56
|
const rule = {
|
|
36
57
|
test: /\.tsx?$/,
|
|
37
|
-
include:
|
|
58
|
+
include: includeDirectories,
|
|
38
59
|
use: [
|
|
39
60
|
{
|
|
40
|
-
loader: require.resolve('./loader'),
|
|
61
|
+
loader: require.resolve('./loader.js'),
|
|
41
62
|
options: config
|
|
42
63
|
}
|
|
43
64
|
],
|
|
@@ -45,6 +66,9 @@ function withCursorHelper(options = {}) {
|
|
|
45
66
|
};
|
|
46
67
|
|
|
47
68
|
webpackConfig.module.rules.unshift(rule);
|
|
69
|
+
console.log('🔧 Webpack rule added successfully');
|
|
70
|
+
} else {
|
|
71
|
+
console.log('🔧 Plugin conditions not met, skipping webpack rule');
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
// Call the existing webpack function if it exists
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
interface IdeButtonProps {
|
|
5
|
+
filePath: string;
|
|
6
|
+
projectRoot?: string;
|
|
7
|
+
ideType?: 'cursor' | 'vscode' | 'webstorm' | 'atom';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
|
|
11
|
+
const getIdeUrl = (ide: string, path: string) => {
|
|
12
|
+
const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
|
|
13
|
+
|
|
14
|
+
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}`;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleClick = () => {
|
|
29
|
+
const url = getIdeUrl(ideType, filePath);
|
|
30
|
+
window.open(url, '_blank');
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
onClick={handleClick}
|
|
36
|
+
style={{
|
|
37
|
+
position: 'absolute',
|
|
38
|
+
top: '8px',
|
|
39
|
+
right: '8px',
|
|
40
|
+
background: '#007acc',
|
|
41
|
+
color: 'white',
|
|
42
|
+
border: 'none',
|
|
43
|
+
borderRadius: '4px',
|
|
44
|
+
padding: '4px 8px',
|
|
45
|
+
fontSize: '12px',
|
|
46
|
+
cursor: 'pointer',
|
|
47
|
+
zIndex: 1000,
|
|
48
|
+
opacity: 0.7,
|
|
49
|
+
transition: 'opacity 0.2s',
|
|
50
|
+
fontFamily: 'monospace'
|
|
51
|
+
}}
|
|
52
|
+
onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
|
|
53
|
+
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.7')}
|
|
54
|
+
title={`Open ${filePath} in ${ideType.toUpperCase()}`}
|
|
55
|
+
>
|
|
56
|
+
📝
|
|
57
|
+
</button>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface WithIdeButtonOptions {
|
|
62
|
+
projectRoot?: string;
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
ideType?: 'cursor' | 'vscode' | 'webstorm' | 'atom';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function withIdeButton<T extends object>(
|
|
68
|
+
WrappedComponent: React.ComponentType<T>,
|
|
69
|
+
filePath: string,
|
|
70
|
+
options: WithIdeButtonOptions = {}
|
|
71
|
+
) {
|
|
72
|
+
const { projectRoot, enabled = process.env.NODE_ENV === 'development', ideType = 'cursor' } = options;
|
|
73
|
+
|
|
74
|
+
const WithIdeButtonComponent: React.FC<T> = (props) => {
|
|
75
|
+
const [isClient, setIsClient] = useState(false);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
setIsClient(true);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// In production or when disabled, just return the component without wrapper
|
|
82
|
+
if (!enabled || !isClient) {
|
|
83
|
+
return <WrappedComponent {...props} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div style={{ position: 'relative' }}>
|
|
88
|
+
<IdeButton filePath={filePath} projectRoot={projectRoot} ideType={ideType} />
|
|
89
|
+
<WrappedComponent {...props} />
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
|
|
95
|
+
|
|
96
|
+
return WithIdeButtonComponent;
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextjs-ide-helper",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@types/node": "^20.19.9",
|
|
42
42
|
"@types/react": "^18.3.23",
|
|
43
43
|
"jest": "^30.0.5",
|
|
44
|
+
"minimatch": "^10.0.3",
|
|
44
45
|
"typescript": "^5.8.3"
|
|
45
46
|
},
|
|
46
47
|
"repository": {
|
|
@@ -393,6 +393,57 @@ export default Button;`;
|
|
|
393
393
|
|
|
394
394
|
expect(result).toContain('withIdeButton');
|
|
395
395
|
});
|
|
396
|
+
|
|
397
|
+
it('should support regex patterns in component paths for nested directories', function() {
|
|
398
|
+
mockContext.resourcePath = process.cwd() + '/src/components/ui/forms/Button.tsx';
|
|
399
|
+
mockContext.getOptions.mockReturnValue({
|
|
400
|
+
enabled: true,
|
|
401
|
+
componentPaths: ['src/components/**'],
|
|
402
|
+
projectRoot: process.cwd()
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const source = `const Button = () => <button>Click</button>;
|
|
406
|
+
export default Button;`;
|
|
407
|
+
|
|
408
|
+
const result = loader.call(mockContext, source);
|
|
409
|
+
|
|
410
|
+
expect(result).toContain('withIdeButton');
|
|
411
|
+
expect(result).toContain("'src/components/ui/forms/Button.tsx'");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should support regex patterns matching any nested directory structure', function() {
|
|
415
|
+
mockContext.resourcePath = process.cwd() + '/app/components/dashboard/widgets/Chart.tsx';
|
|
416
|
+
mockContext.getOptions.mockReturnValue({
|
|
417
|
+
enabled: true,
|
|
418
|
+
componentPaths: ['**/components/**'],
|
|
419
|
+
projectRoot: process.cwd()
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const source = `const Chart = () => <div>Chart</div>;
|
|
423
|
+
export default Chart;`;
|
|
424
|
+
|
|
425
|
+
const result = loader.call(mockContext, source);
|
|
426
|
+
|
|
427
|
+
expect(result).toContain('withIdeButton');
|
|
428
|
+
expect(result).toContain("'app/components/dashboard/widgets/Chart.tsx'");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should support multiple regex patterns for different nested structures', function() {
|
|
432
|
+
mockContext.resourcePath = process.cwd() + '/modules/feature-a/components/deep/nested/Widget.tsx';
|
|
433
|
+
mockContext.getOptions.mockReturnValue({
|
|
434
|
+
enabled: true,
|
|
435
|
+
componentPaths: ['src/components/**', 'modules/**/components/**', 'lib/ui/**'],
|
|
436
|
+
projectRoot: process.cwd()
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const source = `const Widget = () => <div>Widget</div>;
|
|
440
|
+
export default Widget;`;
|
|
441
|
+
|
|
442
|
+
const result = loader.call(mockContext, source);
|
|
443
|
+
|
|
444
|
+
expect(result).toContain('withIdeButton');
|
|
445
|
+
expect(result).toContain("'modules/feature-a/components/deep/nested/Widget.tsx'");
|
|
446
|
+
});
|
|
396
447
|
});
|
|
397
448
|
|
|
398
449
|
describe('options handling', function() {
|
package/src/loader.js
CHANGED
|
@@ -3,6 +3,7 @@ const { parse } = require('@babel/parser');
|
|
|
3
3
|
const traverse = require('@babel/traverse').default;
|
|
4
4
|
const generate = require('@babel/generator').default;
|
|
5
5
|
const t = require('@babel/types');
|
|
6
|
+
const { minimatch } = require('minimatch');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Webpack loader that automatically wraps React components with cursor buttons
|
|
@@ -13,6 +14,8 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
13
14
|
const filename = this.resourcePath;
|
|
14
15
|
const options = this.getOptions() || {};
|
|
15
16
|
|
|
17
|
+
console.log('🔧 Loader called for file:', filename);
|
|
18
|
+
|
|
16
19
|
const {
|
|
17
20
|
componentPaths = ['src/components'],
|
|
18
21
|
projectRoot = process.cwd(),
|
|
@@ -22,23 +25,44 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
22
25
|
|
|
23
26
|
// Only process if enabled
|
|
24
27
|
if (!enabled) {
|
|
28
|
+
console.log('🔧 Loader disabled, returning original source');
|
|
25
29
|
return source;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
// Only process files in specified component directories
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const relativePath = path.relative(projectRoot, filename);
|
|
34
|
+
console.log('🔧 Processing file:', relativePath, 'against paths:', componentPaths);
|
|
35
|
+
|
|
36
|
+
const shouldProcess = componentPaths.some(componentPath => {
|
|
37
|
+
console.log('🔧 Checking path:', componentPath);
|
|
38
|
+
// Check if componentPath contains glob patterns
|
|
39
|
+
if (componentPath.includes('*')) {
|
|
40
|
+
const matches = minimatch(relativePath, componentPath);
|
|
41
|
+
console.log('🔧 Glob match result:', matches, 'for pattern:', componentPath);
|
|
42
|
+
return matches;
|
|
43
|
+
} else {
|
|
44
|
+
// Fallback to the original behavior for non-glob patterns
|
|
45
|
+
const matches = filename.includes(path.resolve(projectRoot, componentPath));
|
|
46
|
+
console.log('🔧 Direct path match result:', matches);
|
|
47
|
+
return matches;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log('🔧 Should process:', shouldProcess);
|
|
32
52
|
|
|
33
53
|
if (!shouldProcess || (!filename.endsWith('.tsx') && !filename.endsWith('.jsx'))) {
|
|
54
|
+
console.log('🔧 File does not match criteria, skipping');
|
|
34
55
|
return source;
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
// Check if it's already wrapped using simple string check for performance
|
|
38
59
|
if (source.includes('withIdeButton')) {
|
|
60
|
+
console.log('🔧 File already contains withIdeButton, skipping');
|
|
39
61
|
return source;
|
|
40
62
|
}
|
|
41
63
|
|
|
64
|
+
console.log('🔧 Proceeding to transform file:', relativePath);
|
|
65
|
+
|
|
42
66
|
let ast;
|
|
43
67
|
try {
|
|
44
68
|
// Parse the source code into an AST
|
|
@@ -136,8 +160,7 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
136
160
|
return source;
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
//
|
|
140
|
-
const relativePath = path.relative(projectRoot, filename);
|
|
163
|
+
// relativePath already declared above for glob matching
|
|
141
164
|
|
|
142
165
|
// Transform the AST
|
|
143
166
|
let modified = false;
|
package/src/plugin.js
CHANGED
|
@@ -9,6 +9,16 @@ const path = require('path');
|
|
|
9
9
|
* @property {boolean} [enabled=process.env.NODE_ENV === 'development'] - Enable/disable the plugin
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extract base directory from a glob pattern
|
|
14
|
+
* @param {string} globPattern - The glob pattern
|
|
15
|
+
* @returns {string} - The base directory
|
|
16
|
+
*/
|
|
17
|
+
function extractBaseDirectory(globPattern) {
|
|
18
|
+
// Remove glob patterns (**/ */ etc.) to get the base directory
|
|
19
|
+
return globPattern.replace(/\/\*\*.*$/, '').replace(/\/\*.*$/, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* NextJS Cursor Helper plugin
|
|
14
24
|
* @param {CursorHelperOptions} options - Configuration options
|
|
@@ -23,18 +33,29 @@ function withCursorHelper(options = {}) {
|
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
const config = { ...defaultOptions, ...options };
|
|
36
|
+
|
|
37
|
+
console.log('🔧 Plugin initialized with config:', config);
|
|
26
38
|
|
|
27
39
|
return (nextConfig = {}) => {
|
|
28
40
|
return {
|
|
29
41
|
...nextConfig,
|
|
30
42
|
webpack: (webpackConfig, context) => {
|
|
31
43
|
const { dev, isServer } = context;
|
|
44
|
+
|
|
45
|
+
console.log('🔧 Webpack config called:', { dev, isServer, enabled: config.enabled });
|
|
32
46
|
|
|
33
47
|
// Only apply in development and for client-side
|
|
34
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
|
+
|
|
54
|
+
console.log('🔧 Adding webpack rule with include directories:', includeDirectories);
|
|
55
|
+
|
|
35
56
|
const rule = {
|
|
36
57
|
test: /\.tsx?$/,
|
|
37
|
-
include:
|
|
58
|
+
include: includeDirectories,
|
|
38
59
|
use: [
|
|
39
60
|
{
|
|
40
61
|
loader: require.resolve('./loader.js'),
|
|
@@ -45,6 +66,9 @@ function withCursorHelper(options = {}) {
|
|
|
45
66
|
};
|
|
46
67
|
|
|
47
68
|
webpackConfig.module.rules.unshift(rule);
|
|
69
|
+
console.log('🔧 Webpack rule added successfully');
|
|
70
|
+
} else {
|
|
71
|
+
console.log('🔧 Plugin conditions not met, skipping webpack rule');
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
// Call the existing webpack function if it exists
|