nextjs-ide-helper 1.3.4 → 1.4.1
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 +141 -5
- package/lib/loader.js +91 -35
- package/lib/plugin.js +0 -3
- package/lib/withIdeButton.js +9 -10
- package/lib/withIdeButton.tsx +10 -13
- package/package.json +2 -2
- package/src/__tests__/loader.test.js +338 -2
- package/src/loader.js +91 -30
- package/src/withIdeButton.tsx +10 -13
- package/lib/__tests__/loader.test.js +0 -480
|
@@ -188,8 +188,9 @@ export default Button;`;
|
|
|
188
188
|
mockContext.getOptions.mockReturnValue({ enabled: true });
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
-
it('should not process files without
|
|
192
|
-
const source = `export const
|
|
191
|
+
it('should not process files without any React component exports', function() {
|
|
192
|
+
const source = `export const API_URL = 'http://localhost:3000';
|
|
193
|
+
export const utils = { format: () => {} };`;
|
|
193
194
|
|
|
194
195
|
const result = loader.call(mockContext, source);
|
|
195
196
|
|
|
@@ -197,6 +198,127 @@ export default Button;`;
|
|
|
197
198
|
expect(result).not.toContain('withIdeButton');
|
|
198
199
|
});
|
|
199
200
|
|
|
201
|
+
it('should process named exports and wrap them with withIdeButton', function() {
|
|
202
|
+
const source = `export const Button = () => <button>Click</button>;
|
|
203
|
+
export const Modal = () => <div>Modal</div>;`;
|
|
204
|
+
|
|
205
|
+
const result = loader.call(mockContext, source);
|
|
206
|
+
|
|
207
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
208
|
+
expect(result).toContain("export const Button = withIdeButton(() => <button>Click</button>,");
|
|
209
|
+
expect(result).toContain("export const Modal = withIdeButton(() => <div>Modal</div>,");
|
|
210
|
+
expect(result).toMatch(/projectRoot: ['"][^'"]*['"]/);
|
|
211
|
+
|
|
212
|
+
// Verify the complete structure
|
|
213
|
+
const lines = result.split('\n');
|
|
214
|
+
const importLine = lines.find(line => line.includes("import { withIdeButton }"));
|
|
215
|
+
const buttonLine = lines.find(line => line.includes("export const Button = withIdeButton"));
|
|
216
|
+
const modalLine = lines.find(line => line.includes("export const Modal = withIdeButton"));
|
|
217
|
+
|
|
218
|
+
expect(importLine).toBeDefined();
|
|
219
|
+
expect(buttonLine).toBeDefined();
|
|
220
|
+
expect(modalLine).toBeDefined();
|
|
221
|
+
|
|
222
|
+
// Verify each wrapped export contains the file path
|
|
223
|
+
expect(buttonLine).toContain("'src/components/Button.tsx'");
|
|
224
|
+
expect(modalLine).toContain("'src/components/Button.tsx'");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should process mixed named and default exports', function() {
|
|
228
|
+
const source = `export const Button = () => <button>Click</button>;
|
|
229
|
+
|
|
230
|
+
const MainComponent = () => <div>Main</div>;
|
|
231
|
+
export default MainComponent;`;
|
|
232
|
+
|
|
233
|
+
const result = loader.call(mockContext, source);
|
|
234
|
+
|
|
235
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
236
|
+
expect(result).toContain("export const Button = withIdeButton(() => <button>Click</button>,");
|
|
237
|
+
expect(result).toContain("export default withIdeButton(MainComponent,");
|
|
238
|
+
|
|
239
|
+
// Verify both exports are properly wrapped
|
|
240
|
+
const lines = result.split('\n');
|
|
241
|
+
const namedExportLine = lines.find(line => line.includes("export const Button = withIdeButton"));
|
|
242
|
+
const defaultExportLine = lines.find(line => line.includes("export default withIdeButton(MainComponent"));
|
|
243
|
+
|
|
244
|
+
expect(namedExportLine).toBeDefined();
|
|
245
|
+
expect(defaultExportLine).toBeDefined();
|
|
246
|
+
|
|
247
|
+
// Verify file paths are included
|
|
248
|
+
expect(namedExportLine).toContain("'src/components/Button.tsx'");
|
|
249
|
+
expect(defaultExportLine).toContain("'src/components/Button.tsx'");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should process named function exports', function() {
|
|
253
|
+
const source = `export function MyButton() {
|
|
254
|
+
return <button>Function Button</button>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function MyModal() {
|
|
258
|
+
return <div>Function Modal</div>;
|
|
259
|
+
}`;
|
|
260
|
+
|
|
261
|
+
const result = loader.call(mockContext, source);
|
|
262
|
+
|
|
263
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
264
|
+
|
|
265
|
+
// For function exports, the function should be declared first, then wrapped
|
|
266
|
+
expect(result).toContain("function MyButton()");
|
|
267
|
+
expect(result).toContain("function MyModal()");
|
|
268
|
+
expect(result).toContain("const MyButton = withIdeButton(MyButton,");
|
|
269
|
+
expect(result).toContain("const MyModal = withIdeButton(MyModal,");
|
|
270
|
+
|
|
271
|
+
// Verify file paths
|
|
272
|
+
expect(result).toContain("'src/components/Button.tsx'");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should not process lowercase named exports', function() {
|
|
276
|
+
const source = `export const button = () => <button>lowercase</button>;
|
|
277
|
+
export const modal = () => <div>lowercase modal</div>;`;
|
|
278
|
+
|
|
279
|
+
const result = loader.call(mockContext, source);
|
|
280
|
+
|
|
281
|
+
expect(result).toBe(source);
|
|
282
|
+
expect(result).not.toContain('withIdeButton');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should process complex mixed scenario with multiple export types', function() {
|
|
286
|
+
const source = `import React from 'react';
|
|
287
|
+
|
|
288
|
+
export const PrimaryButton = () => <button className="primary">Primary</button>;
|
|
289
|
+
export const SecondaryButton = () => <button className="secondary">Secondary</button>;
|
|
290
|
+
|
|
291
|
+
export function DialogModal() {
|
|
292
|
+
return <div>Dialog Content</div>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const MainLayout = ({ children }) => <div className="layout">{children}</div>;
|
|
296
|
+
export default MainLayout;`;
|
|
297
|
+
|
|
298
|
+
const result = loader.call(mockContext, source);
|
|
299
|
+
|
|
300
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
301
|
+
|
|
302
|
+
// Named arrow function exports
|
|
303
|
+
expect(result).toContain("export const PrimaryButton = withIdeButton(() => <button className=\"primary\">Primary</button>,");
|
|
304
|
+
expect(result).toContain("export const SecondaryButton = withIdeButton(() => <button className=\"secondary\">Secondary</button>,");
|
|
305
|
+
|
|
306
|
+
// Named function export
|
|
307
|
+
expect(result).toContain("function DialogModal()");
|
|
308
|
+
expect(result).toContain("const DialogModal = withIdeButton(DialogModal,");
|
|
309
|
+
|
|
310
|
+
// Default export
|
|
311
|
+
expect(result).toContain("export default withIdeButton(MainLayout,");
|
|
312
|
+
|
|
313
|
+
// Verify React import is preserved and withIdeButton import is added after it
|
|
314
|
+
const lines = result.split('\n');
|
|
315
|
+
const reactImportIndex = lines.findIndex(line => line.includes("import React from 'react'"));
|
|
316
|
+
const withIdeButtonImportIndex = lines.findIndex(line => line.includes("import { withIdeButton }"));
|
|
317
|
+
|
|
318
|
+
expect(reactImportIndex).toBeGreaterThan(-1);
|
|
319
|
+
expect(withIdeButtonImportIndex).toBeGreaterThan(reactImportIndex);
|
|
320
|
+
});
|
|
321
|
+
|
|
200
322
|
it('should not process files with lowercase default export', function() {
|
|
201
323
|
const source = `const button = () => <button>Click</button>;
|
|
202
324
|
export default button;`;
|
|
@@ -323,6 +445,218 @@ export default class TSClassComponent extends Component<Props> {
|
|
|
323
445
|
expect(result).toContain("export default withIdeButton(TSClassComponent,");
|
|
324
446
|
expect(result).toContain("class TSClassComponent extends Component<Props>");
|
|
325
447
|
});
|
|
448
|
+
|
|
449
|
+
it('should process named exports with TypeScript types', function() {
|
|
450
|
+
const source = `interface ButtonProps {
|
|
451
|
+
onClick: () => void;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export const TypedButton = ({ onClick }: ButtonProps) => <button onClick={onClick}>Typed Button</button>;
|
|
455
|
+
|
|
456
|
+
type ModalProps = {
|
|
457
|
+
isOpen: boolean;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export const TypedModal = ({ isOpen }: ModalProps) => isOpen ? <div>Modal</div> : null;`;
|
|
461
|
+
|
|
462
|
+
const result = loader.call(mockContext, source);
|
|
463
|
+
|
|
464
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
465
|
+
expect(result).toContain("export const TypedButton = withIdeButton(({");
|
|
466
|
+
expect(result).toContain("onClick");
|
|
467
|
+
expect(result).toContain("}: ButtonProps) => <button onClick={onClick}>Typed Button</button>,");
|
|
468
|
+
expect(result).toContain("export const TypedModal = withIdeButton(({");
|
|
469
|
+
expect(result).toContain("isOpen");
|
|
470
|
+
expect(result).toContain("}: ModalProps) => isOpen ? <div>Modal</div> : null,");
|
|
471
|
+
|
|
472
|
+
// Verify interfaces and types are preserved
|
|
473
|
+
expect(result).toContain("interface ButtonProps");
|
|
474
|
+
expect(result).toContain("type ModalProps");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should handle mixed named exports with non-component exports', function() {
|
|
478
|
+
const source = `export const API_URL = 'https://api.example.com';
|
|
479
|
+
export const utils = { format: () => {} };
|
|
480
|
+
|
|
481
|
+
export const Button = () => <button>Click</button>;
|
|
482
|
+
export const Modal = () => <div>Modal</div>;
|
|
483
|
+
|
|
484
|
+
export const config = { theme: 'dark' };`;
|
|
485
|
+
|
|
486
|
+
const result = loader.call(mockContext, source);
|
|
487
|
+
|
|
488
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
489
|
+
|
|
490
|
+
// Component exports should be wrapped
|
|
491
|
+
expect(result).toContain("export const Button = withIdeButton(() => <button>Click</button>,");
|
|
492
|
+
expect(result).toContain("export const Modal = withIdeButton(() => <div>Modal</div>,");
|
|
493
|
+
|
|
494
|
+
// Non-component exports should remain unchanged (check for key parts since formatting may vary)
|
|
495
|
+
expect(result).toContain("export const API_URL = 'https://api.example.com';");
|
|
496
|
+
expect(result).toContain("export const utils = {");
|
|
497
|
+
expect(result).toContain("format: () => {}");
|
|
498
|
+
expect(result).toContain("export const config = {");
|
|
499
|
+
expect(result).toContain("theme: 'dark'");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('should handle complex export patterns with existing imports', function() {
|
|
503
|
+
const source = `import React, { useState, useEffect } from 'react';
|
|
504
|
+
import { someUtil } from './utils';
|
|
505
|
+
|
|
506
|
+
export const InteractiveButton = () => {
|
|
507
|
+
const [count, setCount] = useState(0);
|
|
508
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
export function StatefulModal() {
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
console.log('Modal mounted');
|
|
514
|
+
}, []);
|
|
515
|
+
return <div>Stateful Modal</div>;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const ComplexComponent = () => {
|
|
519
|
+
return <div>Complex</div>;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
export default ComplexComponent;`;
|
|
523
|
+
|
|
524
|
+
const result = loader.call(mockContext, source);
|
|
525
|
+
|
|
526
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
527
|
+
|
|
528
|
+
// Verify all imports are preserved
|
|
529
|
+
expect(result).toContain("import React, { useState, useEffect } from 'react';");
|
|
530
|
+
expect(result).toContain("import { someUtil } from './utils';");
|
|
531
|
+
|
|
532
|
+
// Verify withIdeButton import is added after existing imports
|
|
533
|
+
const lines = result.split('\n');
|
|
534
|
+
const reactImportIndex = lines.findIndex(line => line.includes("import React"));
|
|
535
|
+
const utilImportIndex = lines.findIndex(line => line.includes("import { someUtil }"));
|
|
536
|
+
const withIdeButtonImportIndex = lines.findIndex(line => line.includes("import { withIdeButton }"));
|
|
537
|
+
|
|
538
|
+
expect(withIdeButtonImportIndex).toBeGreaterThan(Math.max(reactImportIndex, utilImportIndex));
|
|
539
|
+
|
|
540
|
+
// Verify component transformations
|
|
541
|
+
expect(result).toContain("export const InteractiveButton = withIdeButton(() => {");
|
|
542
|
+
expect(result).toContain("function StatefulModal()");
|
|
543
|
+
expect(result).toContain("const StatefulModal = withIdeButton(StatefulModal,");
|
|
544
|
+
expect(result).toContain("export default withIdeButton(ComplexComponent,");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should handle files with only named exports (no default export)', function() {
|
|
548
|
+
const source = `export const HeaderButton = () => <button>Header</button>;
|
|
549
|
+
export const FooterButton = () => <button>Footer</button>;
|
|
550
|
+
export const SidebarButton = () => <button>Sidebar</button>;`;
|
|
551
|
+
|
|
552
|
+
const result = loader.call(mockContext, source);
|
|
553
|
+
|
|
554
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
555
|
+
expect(result).toContain("export const HeaderButton = withIdeButton(() => <button>Header</button>,");
|
|
556
|
+
expect(result).toContain("export const FooterButton = withIdeButton(() => <button>Footer</button>,");
|
|
557
|
+
expect(result).toContain("export const SidebarButton = withIdeButton(() => <button>Sidebar</button>,");
|
|
558
|
+
|
|
559
|
+
// Verify no default export processing
|
|
560
|
+
expect(result).not.toContain("export default");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should preserve comments and formatting in named exports', function() {
|
|
564
|
+
const source = `// Header comment
|
|
565
|
+
export const Button = () => <button>Click</button>; // Inline comment
|
|
566
|
+
|
|
567
|
+
/* Block comment */
|
|
568
|
+
export const Modal = () => <div>Modal</div>;`;
|
|
569
|
+
|
|
570
|
+
const result = loader.call(mockContext, source);
|
|
571
|
+
|
|
572
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
573
|
+
expect(result).toContain("export const Button = withIdeButton(() => <button>Click</button>,");
|
|
574
|
+
expect(result).toContain("export const Modal = withIdeButton(() => <div>Modal</div>,");
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should not double-wrap components that are both named and default exports', function() {
|
|
578
|
+
const source = `const MyComponent = () => <div>Shared Component</div>;
|
|
579
|
+
|
|
580
|
+
export { MyComponent }; // named export
|
|
581
|
+
export default MyComponent; // default export`;
|
|
582
|
+
|
|
583
|
+
const result = loader.call(mockContext, source);
|
|
584
|
+
|
|
585
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
586
|
+
|
|
587
|
+
// Should only have one withIdeButton import
|
|
588
|
+
const importMatches = (result.match(/import.*withIdeButton/g) || []);
|
|
589
|
+
expect(importMatches).toHaveLength(1);
|
|
590
|
+
|
|
591
|
+
// Should only wrap once - either as named or default, not both
|
|
592
|
+
const withIdeButtonMatches = (result.match(/withIdeButton\(/g) || []);
|
|
593
|
+
expect(withIdeButtonMatches.length).toBeLessThanOrEqual(2); // At most one for named, one for default
|
|
594
|
+
|
|
595
|
+
// Verify the component isn't wrapped multiple times in the same export
|
|
596
|
+
expect(result).not.toContain("withIdeButton(withIdeButton(");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should handle component exported as both const and default', function() {
|
|
600
|
+
const source = `export const MyComponent = () => <div>Component</div>;
|
|
601
|
+
export default MyComponent;`;
|
|
602
|
+
|
|
603
|
+
const result = loader.call(mockContext, source);
|
|
604
|
+
|
|
605
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
606
|
+
|
|
607
|
+
// Should only have one withIdeButton import
|
|
608
|
+
const importMatches = (result.match(/import.*withIdeButton/g) || []);
|
|
609
|
+
expect(importMatches).toHaveLength(1);
|
|
610
|
+
|
|
611
|
+
// Should wrap both the named export and default export separately
|
|
612
|
+
expect(result).toContain("export const MyComponent = withIdeButton(() => <div>Component</div>,");
|
|
613
|
+
expect(result).toContain("export default withIdeButton(MyComponent,");
|
|
614
|
+
|
|
615
|
+
// But shouldn't double-wrap
|
|
616
|
+
expect(result).not.toContain("withIdeButton(withIdeButton(");
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should handle TypeScript React.FC named exports', function() {
|
|
620
|
+
const source = `import React from 'react';
|
|
621
|
+
|
|
622
|
+
interface TestComponentProps {
|
|
623
|
+
message: string;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export const TestComponent: React.FC<TestComponentProps> = ({ message }) => {
|
|
627
|
+
return (
|
|
628
|
+
<div style={{
|
|
629
|
+
padding: '20px',
|
|
630
|
+
margin: '10px',
|
|
631
|
+
border: '2px solid #007acc',
|
|
632
|
+
borderRadius: '8px',
|
|
633
|
+
backgroundColor: '#f0f8ff'
|
|
634
|
+
}}>
|
|
635
|
+
<h2>Test Component</h2>
|
|
636
|
+
<p>{message}</p>
|
|
637
|
+
</div>
|
|
638
|
+
);
|
|
639
|
+
};`;
|
|
640
|
+
|
|
641
|
+
const result = loader.call(mockContext, source);
|
|
642
|
+
|
|
643
|
+
expect(result).toContain("import { withIdeButton } from 'nextjs-ide-helper';");
|
|
644
|
+
expect(result).toContain("export const TestComponent: React.FC<TestComponentProps> = withIdeButton(");
|
|
645
|
+
expect(result).toContain("'src/components/Button.tsx'");
|
|
646
|
+
|
|
647
|
+
// Verify the original React import is preserved
|
|
648
|
+
expect(result).toContain("import React from 'react';");
|
|
649
|
+
|
|
650
|
+
// Verify the interface is preserved
|
|
651
|
+
expect(result).toContain("interface TestComponentProps");
|
|
652
|
+
|
|
653
|
+
// Verify the component function is properly wrapped (check for message parameter)
|
|
654
|
+
expect(result).toContain("message");
|
|
655
|
+
expect(result).toContain("}) => {");
|
|
656
|
+
|
|
657
|
+
// Verify TypeScript type annotation is preserved
|
|
658
|
+
expect(result).toContain("React.FC<TestComponentProps>");
|
|
659
|
+
});
|
|
326
660
|
});
|
|
327
661
|
|
|
328
662
|
describe('already wrapped components', function() {
|
|
@@ -477,4 +811,6 @@ export default Button;`;
|
|
|
477
811
|
expect(result).toContain("'src/components/Button.tsx'");
|
|
478
812
|
});
|
|
479
813
|
});
|
|
814
|
+
|
|
815
|
+
|
|
480
816
|
});
|
package/src/loader.js
CHANGED
|
@@ -14,8 +14,6 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
14
14
|
const filename = this.resourcePath;
|
|
15
15
|
const options = this.getOptions() || {};
|
|
16
16
|
|
|
17
|
-
console.log('🔧 Loader called for file:', filename);
|
|
18
|
-
|
|
19
17
|
const {
|
|
20
18
|
componentPaths = ['src/components'],
|
|
21
19
|
projectRoot = process.cwd(),
|
|
@@ -25,44 +23,34 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
25
23
|
|
|
26
24
|
// Only process if enabled
|
|
27
25
|
if (!enabled) {
|
|
28
|
-
console.log('🔧 Loader disabled, returning original source');
|
|
29
26
|
return source;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
// Only process files in specified component directories
|
|
33
30
|
const relativePath = path.relative(projectRoot, filename);
|
|
34
|
-
console.log('🔧 Processing file:', relativePath, 'against paths:', componentPaths);
|
|
35
31
|
|
|
36
32
|
const shouldProcess = componentPaths.some(componentPath => {
|
|
37
|
-
console.log('🔧 Checking path:', componentPath);
|
|
38
33
|
// Check if componentPath contains glob patterns
|
|
39
34
|
if (componentPath.includes('*')) {
|
|
40
35
|
const matches = minimatch(relativePath, componentPath);
|
|
41
|
-
console.log('🔧 Glob match result:', matches, 'for pattern:', componentPath);
|
|
42
36
|
return matches;
|
|
43
37
|
} else {
|
|
44
38
|
// Fallback to the original behavior for non-glob patterns
|
|
45
39
|
const matches = filename.includes(path.resolve(projectRoot, componentPath));
|
|
46
|
-
console.log('🔧 Direct path match result:', matches);
|
|
47
40
|
return matches;
|
|
48
41
|
}
|
|
49
42
|
});
|
|
50
43
|
|
|
51
|
-
console.log('🔧 Should process:', shouldProcess);
|
|
52
44
|
|
|
53
45
|
if (!shouldProcess || (!filename.endsWith('.tsx') && !filename.endsWith('.jsx'))) {
|
|
54
|
-
console.log('🔧 File does not match criteria, skipping');
|
|
55
46
|
return source;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
// Check if it's already wrapped using simple string check for performance
|
|
59
50
|
if (source.includes('withIdeButton')) {
|
|
60
|
-
console.log('🔧 File already contains withIdeButton, skipping');
|
|
61
51
|
return source;
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
console.log('🔧 Proceeding to transform file:', relativePath);
|
|
65
|
-
|
|
66
54
|
let ast;
|
|
67
55
|
try {
|
|
68
56
|
// Parse the source code into an AST
|
|
@@ -94,6 +82,7 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
94
82
|
let hasWithIdeButtonImport = false;
|
|
95
83
|
let lastImportPath = null;
|
|
96
84
|
let defaultExportPath = null;
|
|
85
|
+
let namedExports = []; // Track named component exports
|
|
97
86
|
|
|
98
87
|
// Traverse the AST to analyze the code
|
|
99
88
|
traverse(ast, {
|
|
@@ -102,7 +91,7 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
102
91
|
|
|
103
92
|
// Check if withIdeButton is already imported
|
|
104
93
|
if (path.node.source.value === importPath) {
|
|
105
|
-
path.node.specifiers.forEach(spec => {
|
|
94
|
+
path.node.specifiers.forEach((spec) => {
|
|
106
95
|
if (t.isImportSpecifier(spec) && spec.imported.name === 'withIdeButton') {
|
|
107
96
|
hasWithIdeButtonImport = true;
|
|
108
97
|
}
|
|
@@ -141,17 +130,46 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
141
130
|
|
|
142
131
|
// Stop traversal once we find the default export
|
|
143
132
|
path.stop();
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
ExportNamedDeclaration(path) {
|
|
136
|
+
// Handle named exports like: export const Button = () => {}
|
|
137
|
+
if (path.node.declaration && t.isVariableDeclaration(path.node.declaration)) {
|
|
138
|
+
path.node.declaration.declarations.forEach((declarator) => {
|
|
139
|
+
if (t.isIdentifier(declarator.id) &&
|
|
140
|
+
declarator.id.name[0] === declarator.id.name[0].toUpperCase() &&
|
|
141
|
+
(t.isArrowFunctionExpression(declarator.init) ||
|
|
142
|
+
t.isFunctionExpression(declarator.init))) {
|
|
143
|
+
namedExports.push({
|
|
144
|
+
name: declarator.id.name,
|
|
145
|
+
path: path,
|
|
146
|
+
declarator: declarator
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Handle named function exports like: export function Button() {}
|
|
152
|
+
else if (path.node.declaration && t.isFunctionDeclaration(path.node.declaration)) {
|
|
153
|
+
const funcName = path.node.declaration.id.name;
|
|
154
|
+
if (funcName[0] === funcName[0].toUpperCase()) {
|
|
155
|
+
namedExports.push({
|
|
156
|
+
name: funcName,
|
|
157
|
+
path: path,
|
|
158
|
+
declaration: path.node.declaration
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
144
162
|
}
|
|
145
163
|
});
|
|
146
164
|
|
|
147
165
|
// Check if we should process this file
|
|
148
|
-
if (!hasDefaultExport || (!defaultExportName && !isAnonymousComponent)) {
|
|
166
|
+
if ((!hasDefaultExport || (!defaultExportName && !isAnonymousComponent)) && namedExports.length === 0) {
|
|
149
167
|
return source;
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
// Check if component name starts with uppercase (React component convention)
|
|
153
|
-
// Skip this check for anonymous components
|
|
154
|
-
if (defaultExportName && defaultExportName[0] !== defaultExportName[0].toUpperCase()) {
|
|
171
|
+
// Skip this check for anonymous components and if we have named exports
|
|
172
|
+
if (defaultExportName && defaultExportName[0] !== defaultExportName[0].toUpperCase() && namedExports.length === 0) {
|
|
155
173
|
return source;
|
|
156
174
|
}
|
|
157
175
|
|
|
@@ -165,26 +183,69 @@ module.exports = function cursorButtonLoader(source) {
|
|
|
165
183
|
// Transform the AST
|
|
166
184
|
let modified = false;
|
|
167
185
|
|
|
168
|
-
// Add the withIdeButton import
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
// Add the withIdeButton import if we have exports to process
|
|
187
|
+
if ((hasDefaultExport && (defaultExportName || isAnonymousComponent)) || namedExports.length > 0) {
|
|
188
|
+
const withIdeButtonImport = t.importDeclaration(
|
|
189
|
+
[t.importSpecifier(t.identifier('withIdeButton'), t.identifier('withIdeButton'))],
|
|
190
|
+
t.stringLiteral(importPath)
|
|
191
|
+
);
|
|
173
192
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
modified = true;
|
|
178
|
-
} else {
|
|
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);
|
|
193
|
+
// Insert import after last existing import or at the beginning
|
|
194
|
+
if (lastImportPath) {
|
|
195
|
+
lastImportPath.insertAfter(withIdeButtonImport);
|
|
182
196
|
modified = true;
|
|
197
|
+
} else {
|
|
198
|
+
// No imports found, add at the beginning
|
|
199
|
+
if (ast.program && ast.program.body && Array.isArray(ast.program.body)) {
|
|
200
|
+
ast.program.body.unshift(withIdeButtonImport);
|
|
201
|
+
modified = true;
|
|
202
|
+
}
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
|
|
206
|
+
// Process named exports
|
|
207
|
+
namedExports.forEach(namedExport => {
|
|
208
|
+
if (namedExport.declarator) {
|
|
209
|
+
// Handle export const Component = () => {}
|
|
210
|
+
const wrappedCall = t.callExpression(
|
|
211
|
+
t.identifier('withIdeButton'),
|
|
212
|
+
[
|
|
213
|
+
namedExport.declarator.init,
|
|
214
|
+
t.stringLiteral(relativePath),
|
|
215
|
+
t.objectExpression([
|
|
216
|
+
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
217
|
+
])
|
|
218
|
+
]
|
|
219
|
+
);
|
|
220
|
+
namedExport.declarator.init = wrappedCall;
|
|
221
|
+
modified = true;
|
|
222
|
+
} else if (namedExport.declaration) {
|
|
223
|
+
// Handle export function Component() {}
|
|
224
|
+
const funcDeclaration = namedExport.declaration;
|
|
225
|
+
const wrappedCall = t.callExpression(
|
|
226
|
+
t.identifier('withIdeButton'),
|
|
227
|
+
[
|
|
228
|
+
t.identifier(namedExport.name),
|
|
229
|
+
t.stringLiteral(relativePath),
|
|
230
|
+
t.objectExpression([
|
|
231
|
+
t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
|
|
232
|
+
])
|
|
233
|
+
]
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Insert the function declaration before the export
|
|
237
|
+
namedExport.path.insertBefore(funcDeclaration);
|
|
238
|
+
|
|
239
|
+
// Replace the export with wrapped call
|
|
240
|
+
namedExport.path.node.declaration = t.variableDeclaration('const', [
|
|
241
|
+
t.variableDeclarator(t.identifier(namedExport.name), wrappedCall)
|
|
242
|
+
]);
|
|
243
|
+
modified = true;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
186
247
|
// Replace the default export with wrapped version
|
|
187
|
-
if (defaultExportPath &&
|
|
248
|
+
if (defaultExportPath && (hasDefaultExport && (defaultExportName || isAnonymousComponent))) {
|
|
188
249
|
let wrappedCall;
|
|
189
250
|
|
|
190
251
|
if (isAnonymousComponent) {
|
package/src/withIdeButton.tsx
CHANGED
|
@@ -35,26 +35,23 @@ const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType =
|
|
|
35
35
|
onClick={handleClick}
|
|
36
36
|
style={{
|
|
37
37
|
position: 'absolute',
|
|
38
|
-
top: '
|
|
39
|
-
right: '
|
|
38
|
+
top: '4px',
|
|
39
|
+
right: '4px',
|
|
40
|
+
width: '10px',
|
|
41
|
+
height: '10px',
|
|
40
42
|
background: '#007acc',
|
|
41
|
-
color: 'white',
|
|
42
43
|
border: 'none',
|
|
43
|
-
borderRadius: '
|
|
44
|
-
padding:
|
|
45
|
-
fontSize: '12px',
|
|
44
|
+
borderRadius: '50%',
|
|
45
|
+
padding: 0,
|
|
46
46
|
cursor: 'pointer',
|
|
47
47
|
zIndex: 1000,
|
|
48
|
-
opacity: 0.
|
|
49
|
-
transition: 'opacity 0.2s'
|
|
50
|
-
fontFamily: 'monospace'
|
|
48
|
+
opacity: 0.6,
|
|
49
|
+
transition: 'opacity 0.2s'
|
|
51
50
|
}}
|
|
52
51
|
onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
|
|
53
|
-
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.
|
|
52
|
+
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.6')}
|
|
54
53
|
title={`Open ${filePath} in ${ideType.toUpperCase()}`}
|
|
55
|
-
|
|
56
|
-
📝
|
|
57
|
-
</button>
|
|
54
|
+
/>
|
|
58
55
|
);
|
|
59
56
|
};
|
|
60
57
|
|