nextjs-ide-helper 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,8 @@ A Next.js plugin that automatically adds IDE buttons to React components in deve
12
12
  - ⚡ **Hydration Safe**: No SSR/client hydration mismatches
13
13
  - 📱 **TypeScript Support**: Full TypeScript definitions included
14
14
  - 🔌 **Multi-IDE Support**: Supports Cursor, VS Code, WebStorm, and Atom
15
+ - 🎭 **Multiple Component Patterns**: Supports all React component export patterns
16
+ - 🔍 **AST-Based Processing**: Uses robust Abstract Syntax Tree parsing for accurate code transformation
15
17
 
16
18
  ## Installation
17
19
 
@@ -91,16 +93,95 @@ export default withIdeButton(MyComponent, 'src/components/MyComponent.tsx', {
91
93
  });
92
94
  ```
93
95
 
96
+ ## Supported Component Patterns
97
+
98
+ The plugin automatically detects and wraps all types of React component export patterns:
99
+
100
+ ### Named Components
101
+ ```tsx
102
+ // Standard pattern
103
+ const MyComponent = () => <div>Hello</div>;
104
+ export default MyComponent;
105
+
106
+ // Variable declaration
107
+ const MyComponent = function() {
108
+ return <div>Hello</div>;
109
+ };
110
+ export default MyComponent;
111
+ ```
112
+
113
+ ### Direct Export Function Components
114
+ ```tsx
115
+ // Named function
116
+ export default function MyComponent() {
117
+ return <div>Hello</div>;
118
+ }
119
+
120
+ // Anonymous function
121
+ export default function() {
122
+ return <div>Hello</div>;
123
+ }
124
+ ```
125
+
126
+ ### Arrow Function Components
127
+ ```tsx
128
+ // Anonymous arrow function
129
+ export default () => {
130
+ return <div>Hello</div>;
131
+ };
132
+
133
+ // Arrow function with parameters
134
+ export default (props) => {
135
+ return <div>Hello {props.name}</div>;
136
+ };
137
+ ```
138
+
139
+ ### Class Components
140
+ ```tsx
141
+ // Named class
142
+ export default class MyComponent extends React.Component {
143
+ render() {
144
+ return <div>Hello</div>;
145
+ }
146
+ }
147
+
148
+ // TypeScript class
149
+ export default class MyComponent extends Component<Props> {
150
+ render() {
151
+ return <div>Hello</div>;
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### TypeScript Components
157
+ ```tsx
158
+ // Function with TypeScript
159
+ interface Props {
160
+ title: string;
161
+ }
162
+
163
+ export default function MyComponent(props: Props) {
164
+ return <div>{props.title}</div>;
165
+ }
166
+
167
+ // Arrow function with TypeScript
168
+ export default (props: Props) => {
169
+ return <div>{props.title}</div>;
170
+ };
171
+ ```
172
+
94
173
  ## How It Works
95
174
 
96
- 1. **Webpack Loader**: The plugin uses a custom webpack loader to transform your React components at build time
97
- 2. **Automatic Detection**: It scans specified directories for `.tsx` and `.jsx` files
98
- 3. **Smart Wrapping**: Only wraps components that export a default React component
99
- 4. **Development Only**: The IDE buttons only appear in development mode
100
- 5. **Hydration Safe**: Uses client-side state to prevent SSR/hydration mismatches
175
+ 1. **AST-Based Processing**: Uses Babel's Abstract Syntax Tree parser for robust code analysis
176
+ 2. **Webpack Loader**: The plugin uses a custom webpack loader to transform your React components at build time
177
+ 3. **Automatic Detection**: It scans specified directories for `.tsx` and `.jsx` files
178
+ 4. **Smart Wrapping**: Only wraps components that export a default React component (PascalCase names)
179
+ 5. **Development Only**: The IDE buttons only appear in development mode
180
+ 6. **Hydration Safe**: Uses client-side state to prevent SSR/hydration mismatches
101
181
 
102
- ## Example
182
+ ## Examples
103
183
 
184
+ ### Standard Component Pattern
104
185
  Before (your original component):
105
186
  ```tsx
106
187
  // src/components/Button.tsx
@@ -133,7 +214,53 @@ const Button = ({ children, onClick }) => {
133
214
 
134
215
  export default withIdeButton(Button, 'src/components/Button.tsx', {
135
216
  projectRoot: '/path/to/project',
136
- ideType: 'cursor' // defaults to cursor, can be 'vscode', 'webstorm', 'atom'
217
+ ideType: 'cursor'
218
+ });
219
+ ```
220
+
221
+ ### Direct Export Function Component
222
+ Before:
223
+ ```tsx
224
+ // src/components/Header.tsx
225
+ export default function Header() {
226
+ return <header>My App Header</header>;
227
+ }
228
+ ```
229
+
230
+ After (automatically transformed):
231
+ ```tsx
232
+ // src/components/Header.tsx (transformed by the plugin)
233
+ import { withIdeButton } from 'nextjs-ide-plugin/withIdeButton';
234
+
235
+ function Header() {
236
+ return <header>My App Header</header>;
237
+ }
238
+
239
+ export default withIdeButton(Header, 'src/components/Header.tsx', {
240
+ projectRoot: '/path/to/project',
241
+ ideType: 'cursor'
242
+ });
243
+ ```
244
+
245
+ ### Anonymous Component
246
+ Before:
247
+ ```tsx
248
+ // src/components/Footer.tsx
249
+ export default () => {
250
+ return <footer>© 2025 My App</footer>;
251
+ };
252
+ ```
253
+
254
+ After (automatically transformed):
255
+ ```tsx
256
+ // src/components/Footer.tsx (transformed by the plugin)
257
+ import { withIdeButton } from 'nextjs-ide-plugin/withIdeButton';
258
+
259
+ export default withIdeButton(() => {
260
+ return <footer>© 2025 My App</footer>;
261
+ }, 'src/components/Footer.tsx', {
262
+ projectRoot: '/path/to/project',
263
+ ideType: 'cursor'
137
264
  });
138
265
  ```
139
266
 
@@ -174,8 +301,24 @@ MIT
174
301
 
175
302
  ## Changelog
176
303
 
304
+ See [CHANGELOG.md](./CHANGELOG.md) for detailed release notes.
305
+
306
+ ### Recent Releases
307
+
308
+ ### 1.2.0 - Enhanced Component Support
309
+ - Added support for all React component export patterns
310
+ - AST-based code transformation for better reliability
311
+ - Anonymous component support
312
+ - Enhanced TypeScript compatibility
313
+
314
+ ### 1.1.3
315
+ - Updated readme
316
+
317
+ ### 1.1.2
318
+ - Added support for multiple IDEs (Cursor, VS Code, WebStorm, Atom)
319
+
177
320
  ### 1.0.0
178
321
  - Initial release
179
322
  - Automatic component wrapping
180
323
  - TypeScript support
181
- - Hydration-safe implementation
324
+ - Hydration-safe implementation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-ide-helper",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
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",
@@ -11,7 +11,7 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "prepare": "npm run build",
14
- "test": "echo \"Error: no test specified\" && exit 1"
14
+ "test": "jest"
15
15
  },
16
16
  "keywords": [
17
17
  "nextjs",
@@ -33,8 +33,14 @@
33
33
  "react": ">=18.0.0"
34
34
  },
35
35
  "devDependencies": {
36
+ "@babel/generator": "^7.28.0",
37
+ "@babel/parser": "^7.28.0",
38
+ "@babel/traverse": "^7.28.0",
39
+ "@babel/types": "^7.28.2",
40
+ "@types/jest": "^30.0.0",
36
41
  "@types/node": "^20.19.9",
37
42
  "@types/react": "^18.3.23",
43
+ "jest": "^30.0.5",
38
44
  "typescript": "^5.8.3"
39
45
  },
40
46
  "repository": {
@@ -0,0 +1,429 @@
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
+
398
+ describe('options handling', function() {
399
+ it('should use default options when none provided', function() {
400
+ mockContext.getOptions.mockReturnValue(null);
401
+ process.env.NODE_ENV = 'development';
402
+
403
+ const source = `const Button = () => <button>Click</button>;
404
+ export default Button;`;
405
+
406
+ const result = loader.call(mockContext, source);
407
+
408
+ expect(result).toContain('withIdeButton');
409
+ expect(result).toContain("'nextjs-ide-helper'");
410
+ });
411
+
412
+ it('should handle custom project root', function() {
413
+ const customRoot = '/custom/project';
414
+ mockContext.resourcePath = customRoot + '/src/components/Button.tsx';
415
+ mockContext.getOptions.mockReturnValue({
416
+ enabled: true,
417
+ projectRoot: customRoot
418
+ });
419
+
420
+ const source = `const Button = () => <button>Click</button>;
421
+ export default Button;`;
422
+
423
+ const result = loader.call(mockContext, source);
424
+
425
+ expect(result).toMatch(new RegExp("projectRoot:\\s*['\"]" + customRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + "['\"]"));
426
+ expect(result).toContain("'src/components/Button.tsx'");
427
+ });
428
+ });
429
+ });
package/src/loader.js CHANGED
@@ -1,4 +1,8 @@
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');
2
6
 
3
7
  /**
4
8
  * Webpack loader that automatically wraps React components with cursor buttons
@@ -30,50 +34,212 @@ module.exports = function cursorButtonLoader(source) {
30
34
  return source;
31
35
  }
32
36
 
33
- // Check if it's already wrapped
37
+ // Check if it's already wrapped using simple string check for performance
34
38
  if (source.includes('withIdeButton')) {
35
39
  return source;
36
40
  }
37
41
 
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) {
42
+ let ast;
43
+ try {
44
+ // Parse the source code into an AST
45
+ ast = parse(source, {
46
+ sourceType: 'module',
47
+ plugins: [
48
+ 'jsx',
49
+ 'typescript',
50
+ 'decorators-legacy',
51
+ 'classProperties',
52
+ 'objectRestSpread',
53
+ 'asyncGenerators',
54
+ 'functionBind',
55
+ 'exportDefaultFrom',
56
+ 'exportNamespaceFrom',
57
+ 'dynamicImport',
58
+ 'nullishCoalescingOperator',
59
+ 'optionalChaining'
60
+ ]
61
+ });
62
+ } catch (error) {
63
+ // If parsing fails, return original source
64
+ return source;
65
+ }
66
+
67
+ let hasDefaultExport = false;
68
+ let defaultExportName = null;
69
+ let isAnonymousComponent = false;
70
+ let hasWithIdeButtonImport = false;
71
+ let lastImportPath = null;
72
+ let defaultExportPath = null;
73
+
74
+ // Traverse the AST to analyze the code
75
+ traverse(ast, {
76
+ ImportDeclaration(path) {
77
+ lastImportPath = path;
78
+
79
+ // Check if withIdeButton is already imported
80
+ if (path.node.source.value === importPath) {
81
+ path.node.specifiers.forEach(spec => {
82
+ if (t.isImportSpecifier(spec) && spec.imported.name === 'withIdeButton') {
83
+ hasWithIdeButtonImport = true;
84
+ }
85
+ });
86
+ }
87
+ },
88
+
89
+ ExportDefaultDeclaration(path) {
90
+ hasDefaultExport = true;
91
+ defaultExportPath = path;
92
+
93
+ if (t.isIdentifier(path.node.declaration)) {
94
+ // export default ComponentName
95
+ defaultExportName = path.node.declaration.name;
96
+ } else if (t.isFunctionDeclaration(path.node.declaration)) {
97
+ if (path.node.declaration.id) {
98
+ // export default function ComponentName() {}
99
+ defaultExportName = path.node.declaration.id.name;
100
+ } else {
101
+ // export default function() {} - anonymous function
102
+ isAnonymousComponent = true;
103
+ }
104
+ } else if (t.isClassDeclaration(path.node.declaration) && path.node.declaration.id) {
105
+ // export default class ComponentName extends ... {}
106
+ defaultExportName = path.node.declaration.id.name;
107
+ } else if (t.isArrowFunctionExpression(path.node.declaration)) {
108
+ // export default () => {} - anonymous arrow function
109
+ isAnonymousComponent = true;
110
+ } else if (t.isVariableDeclaration(path.node.declaration)) {
111
+ // export default const ComponentName = ...
112
+ const declarator = path.node.declaration.declarations[0];
113
+ if (t.isIdentifier(declarator.id)) {
114
+ defaultExportName = declarator.id.name;
115
+ }
116
+ }
117
+
118
+ // Stop traversal once we find the default export
119
+ path.stop();
120
+ }
121
+ });
122
+
123
+ // Check if we should process this file
124
+ if (!hasDefaultExport || (!defaultExportName && !isAnonymousComponent)) {
41
125
  return source;
42
126
  }
43
127
 
44
- const componentName = defaultExportMatch[1];
45
-
46
128
  // Check if component name starts with uppercase (React component convention)
47
- if (!componentName || componentName[0] !== componentName[0].toUpperCase()) {
129
+ // Skip this check for anonymous components
130
+ if (defaultExportName && defaultExportName[0] !== defaultExportName[0].toUpperCase()) {
131
+ return source;
132
+ }
133
+
134
+ // If withIdeButton is already imported, don't process
135
+ if (hasWithIdeButtonImport) {
48
136
  return source;
49
137
  }
50
138
 
51
139
  // Get relative path from project root
52
140
  const relativePath = path.relative(projectRoot, filename);
53
141
 
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);
142
+ // Transform the AST
143
+ let modified = false;
144
+
145
+ // Add the withIdeButton import
146
+ const withIdeButtonImport = t.importDeclaration(
147
+ [t.importSpecifier(t.identifier('withIdeButton'), t.identifier('withIdeButton'))],
148
+ t.stringLiteral(importPath)
149
+ );
150
+
151
+ // Insert import after last existing import or at the beginning
152
+ if (lastImportPath) {
153
+ lastImportPath.insertAfter(withIdeButtonImport);
154
+ modified = true;
64
155
  } 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}' });`;
156
+ // No imports found, add at the beginning
157
+ if (ast.program && ast.program.body && Array.isArray(ast.program.body)) {
158
+ ast.program.body.unshift(withIdeButtonImport);
159
+ modified = true;
160
+ }
161
+ }
162
+
163
+ // Replace the default export with wrapped version
164
+ if (defaultExportPath && modified) {
165
+ let wrappedCall;
72
166
 
73
- modifiedSource = source.slice(0, lastImportIndex) +
74
- importStatement +
75
- source.slice(lastImportIndex).replace(/export\s+default\s+\w+;?/, wrappedExport);
167
+ if (isAnonymousComponent) {
168
+ // For anonymous components, wrap the original function/arrow function
169
+ let componentExpression;
170
+
171
+ if (t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
172
+ // Convert anonymous function declaration to function expression
173
+ componentExpression = t.functionExpression(
174
+ null, // id is null for anonymous
175
+ defaultExportPath.node.declaration.params,
176
+ defaultExportPath.node.declaration.body,
177
+ defaultExportPath.node.declaration.generator,
178
+ defaultExportPath.node.declaration.async
179
+ );
180
+ } else {
181
+ // For arrow functions and other expressions, use directly
182
+ componentExpression = defaultExportPath.node.declaration;
183
+ }
184
+
185
+ wrappedCall = t.callExpression(
186
+ t.identifier('withIdeButton'),
187
+ [
188
+ componentExpression,
189
+ t.stringLiteral(relativePath),
190
+ t.objectExpression([
191
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
192
+ ])
193
+ ]
194
+ );
195
+ } else {
196
+ // For named components, wrap with the component name identifier
197
+ wrappedCall = t.callExpression(
198
+ t.identifier('withIdeButton'),
199
+ [
200
+ t.identifier(defaultExportName),
201
+ t.stringLiteral(relativePath),
202
+ t.objectExpression([
203
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
204
+ ])
205
+ ]
206
+ );
207
+ }
208
+
209
+ // If the export is a named function or class declaration, we need to handle it differently
210
+ if (!isAnonymousComponent &&
211
+ (t.isFunctionDeclaration(defaultExportPath.node.declaration) ||
212
+ t.isClassDeclaration(defaultExportPath.node.declaration))) {
213
+ // Insert the function/class declaration before the export
214
+ const declaration = defaultExportPath.node.declaration;
215
+ defaultExportPath.insertBefore(declaration);
216
+
217
+ // Replace the export declaration with the wrapped call
218
+ defaultExportPath.node.declaration = wrappedCall;
219
+ } else {
220
+ // For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
221
+ defaultExportPath.node.declaration = wrappedCall;
222
+ }
76
223
  }
77
224
 
78
- return modifiedSource;
225
+ // Generate the transformed code
226
+ if (modified) {
227
+ try {
228
+ const result = generate(ast, {
229
+ retainLines: false,
230
+ compact: false,
231
+ jsescOption: {
232
+ quotes: 'single'
233
+ }
234
+ });
235
+ return result.code;
236
+ } catch (error) {
237
+ // If generation fails, return original source
238
+ return source;
239
+ }
240
+ }
241
+
242
+ // If we reach here, nothing was modified
243
+
244
+ return source;
79
245
  };