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 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
@@ -0,0 +1,8 @@
1
+ // Main exports
2
+ export { withIdeButton } from './withIdeButton';
3
+ export type { WithIdeButtonOptions } from './withIdeButton';
4
+
5
+ // Next.js plugin
6
+ const withIdeHelper = require('./plugin');
7
+ export default withIdeHelper;
8
+ export { withIdeHelper };
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 shouldProcess = componentPaths.some(componentPath =>
26
- filename.includes(path.resolve(projectRoot, componentPath))
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
- // 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) {
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
- if (!componentName || componentName[0] !== componentName[0].toUpperCase()) {
153
+ // Skip this check for anonymous components
154
+ if (defaultExportName && defaultExportName[0] !== defaultExportName[0].toUpperCase()) {
48
155
  return source;
49
156
  }
50
157
 
51
- // Get relative path from project root
52
- const relativePath = path.relative(projectRoot, filename);
158
+ // If withIdeButton is already imported, don't process
159
+ if (hasWithIdeButtonImport) {
160
+ return source;
161
+ }
53
162
 
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 { withIdeButton } from '${importPath}';\n`;
62
- const wrappedExport = `export default withIdeButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
63
- modifiedSource = importStatement + source.replace(/export\s+default\s+\w+;?/, wrappedExport);
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
- const lastImportIndex = source.lastIndexOf(imports[imports.length - 1]) + imports[imports.length - 1].length;
66
-
67
- // Add import statement
68
- const importStatement = `\nimport { withIdeButton } from '${importPath}';`;
69
-
70
- // Replace the export
71
- const wrappedExport = `export default withIdeButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
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
- modifiedSource = source.slice(0, lastImportIndex) +
74
- importStatement +
75
- source.slice(lastImportIndex).replace(/export\s+default\s+\w+;?/, wrappedExport);
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
- return modifiedSource;
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: config.componentPaths.map(p => path.resolve(config.projectRoot, p)),
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.0",
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 shouldProcess = componentPaths.some(componentPath =>
30
- filename.includes(path.resolve(projectRoot, componentPath))
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
- // Get relative path from project root
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: config.componentPaths.map(p => path.resolve(config.projectRoot, p)),
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