nextjs-ide-helper 1.3.4 → 1.5.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/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,75 @@ module.exports = function cursorButtonLoader(source) {
165
183
  // Transform the AST
166
184
  let modified = false;
167
185
 
168
- // Add the withIdeButton import
169
- const withIdeButtonImport = t.importDeclaration(
170
- [t.importSpecifier(t.identifier('withIdeButton'), t.identifier('withIdeButton'))],
171
- t.stringLiteral(importPath)
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
- // Insert import after last existing import or at the beginning
175
- if (lastImportPath) {
176
- lastImportPath.insertAfter(withIdeButtonImport);
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
+ // Convert FunctionDeclaration to FunctionExpression (preserves name for stack traces)
225
+ const funcDecl = namedExport.declaration;
226
+ const funcExpr = t.functionExpression(
227
+ funcDecl.id, // Keep the name for debugging
228
+ funcDecl.params,
229
+ funcDecl.body,
230
+ funcDecl.generator,
231
+ funcDecl.async
232
+ );
233
+
234
+ const wrappedCall = t.callExpression(
235
+ t.identifier('withIdeButton'),
236
+ [
237
+ funcExpr,
238
+ t.stringLiteral(relativePath),
239
+ t.objectExpression([
240
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
241
+ ])
242
+ ]
243
+ );
244
+
245
+ // Replace: export function X() {} -> export const X = withIdeButton(function X() {}, ...)
246
+ namedExport.path.node.declaration = t.variableDeclaration('const', [
247
+ t.variableDeclarator(t.identifier(namedExport.name), wrappedCall)
248
+ ]);
249
+ modified = true;
250
+ }
251
+ });
252
+
186
253
  // Replace the default export with wrapped version
187
- if (defaultExportPath && modified) {
254
+ if (defaultExportPath && (hasDefaultExport && (defaultExportName || isAnonymousComponent))) {
188
255
  let wrappedCall;
189
256
 
190
257
  if (isAnonymousComponent) {
@@ -229,15 +296,33 @@ module.exports = function cursorButtonLoader(source) {
229
296
  );
230
297
  }
231
298
 
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
299
+ // If the export is a named function declaration, convert to function expression inline
300
+ if (!isAnonymousComponent && t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
301
+ const funcDecl = defaultExportPath.node.declaration;
302
+ const funcExpr = t.functionExpression(
303
+ funcDecl.id, // Keep the name for debugging
304
+ funcDecl.params,
305
+ funcDecl.body,
306
+ funcDecl.generator,
307
+ funcDecl.async
308
+ );
309
+
310
+ // Wrap the function expression directly
311
+ wrappedCall = t.callExpression(
312
+ t.identifier('withIdeButton'),
313
+ [
314
+ funcExpr,
315
+ t.stringLiteral(relativePath),
316
+ t.objectExpression([
317
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
318
+ ])
319
+ ]
320
+ );
321
+ defaultExportPath.node.declaration = wrappedCall;
322
+ } else if (!isAnonymousComponent && t.isClassDeclaration(defaultExportPath.node.declaration)) {
323
+ // For classes, we still need to separate declaration (can't inline class expressions easily)
237
324
  const declaration = defaultExportPath.node.declaration;
238
325
  defaultExportPath.insertBefore(declaration);
239
-
240
- // Replace the export declaration with the wrapped call
241
326
  defaultExportPath.node.declaration = wrappedCall;
242
327
  } else {
243
328
  // For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
@@ -1,6 +1,61 @@
1
1
  'use client';
2
2
  import React, { useState, useEffect } from 'react';
3
3
 
4
+ // Inject global styles once
5
+ let stylesInjected = false;
6
+
7
+ function injectStyles() {
8
+ if (stylesInjected || typeof document === 'undefined') return;
9
+ stylesInjected = true;
10
+
11
+ const style = document.createElement('style');
12
+ style.id = 'ide-helper-styles';
13
+ style.textContent = `
14
+ .ide-helper-dot { display: block; }
15
+ .ide-helper-hidden .ide-helper-dot { display: none; }
16
+ .ide-helper-toggle {
17
+ background: transparent;
18
+ border: 2px solid #007acc !important;
19
+ box-sizing: border-box;
20
+ }
21
+ .ide-helper-hidden .ide-helper-toggle {
22
+ background: #666;
23
+ border-color: #666 !important;
24
+ }
25
+ `;
26
+ document.head.appendChild(style);
27
+ }
28
+
29
+ // Toggle button - renders once via singleton pattern
30
+ let toggleInjected = false;
31
+
32
+ function injectToggle() {
33
+ if (toggleInjected || typeof document === 'undefined') return;
34
+ toggleInjected = true;
35
+
36
+ const btn = document.createElement('button');
37
+ btn.className = 'ide-helper-toggle';
38
+ btn.title = 'Toggle IDE dots (click to show/hide)';
39
+ Object.assign(btn.style, {
40
+ position: 'fixed',
41
+ bottom: '16px',
42
+ right: '16px',
43
+ width: '18px',
44
+ height: '18px',
45
+ border: 'none',
46
+ borderRadius: '50%',
47
+ padding: '0',
48
+ cursor: 'pointer',
49
+ zIndex: '9999',
50
+ opacity: '0.7',
51
+ transition: 'opacity 0.2s'
52
+ });
53
+ btn.onmouseenter = () => btn.style.opacity = '1';
54
+ btn.onmouseleave = () => btn.style.opacity = '0.7';
55
+ btn.onclick = () => document.body.classList.toggle('ide-helper-hidden');
56
+ document.body.appendChild(btn);
57
+ }
58
+
4
59
  interface IdeButtonProps {
5
60
  filePath: string;
6
61
  projectRoot?: string;
@@ -8,53 +63,49 @@ interface IdeButtonProps {
8
63
  }
9
64
 
10
65
  const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
66
+ useEffect(() => {
67
+ injectStyles();
68
+ injectToggle();
69
+ }, []);
70
+
11
71
  const getIdeUrl = (ide: string, path: string) => {
12
72
  const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
13
-
14
73
  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}`;
74
+ case 'cursor': return `cursor://file${fullPath}`;
75
+ case 'vscode': return `vscode://file${fullPath}`;
76
+ case 'webstorm': return `webstorm://open?file=${fullPath}`;
77
+ case 'atom': return `atom://open?path=${fullPath}`;
78
+ default: return `cursor://file${fullPath}`;
25
79
  }
26
80
  };
27
81
 
28
82
  const handleClick = () => {
29
- const url = getIdeUrl(ideType, filePath);
30
- window.open(url, '_blank');
83
+ window.open(getIdeUrl(ideType, filePath), '_blank');
31
84
  };
32
85
 
33
86
  return (
34
87
  <button
88
+ className="ide-helper-dot"
35
89
  onClick={handleClick}
36
90
  style={{
37
91
  position: 'absolute',
38
- top: '8px',
39
- right: '8px',
92
+ top: '4px',
93
+ right: '4px',
94
+ width: '10px',
95
+ height: '10px',
40
96
  background: '#007acc',
41
- color: 'white',
42
97
  border: 'none',
43
- borderRadius: '4px',
44
- padding: '4px 8px',
45
- fontSize: '12px',
98
+ borderRadius: '50%',
99
+ padding: 0,
46
100
  cursor: 'pointer',
47
101
  zIndex: 1000,
48
- opacity: 0.7,
49
- transition: 'opacity 0.2s',
50
- fontFamily: 'monospace'
102
+ opacity: 0.6,
103
+ transition: 'opacity 0.2s'
51
104
  }}
52
105
  onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
53
- onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.7')}
106
+ onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.6')}
54
107
  title={`Open ${filePath} in ${ideType.toUpperCase()}`}
55
- >
56
- 📝
57
- </button>
108
+ />
58
109
  );
59
110
  };
60
111
 
@@ -78,7 +129,6 @@ export function withIdeButton<T extends object>(
78
129
  setIsClient(true);
79
130
  }, []);
80
131
 
81
- // In production or when disabled, just return the component without wrapper
82
132
  if (!enabled || !isClient) {
83
133
  return <WrappedComponent {...props} />;
84
134
  }
@@ -94,4 +144,4 @@ export function withIdeButton<T extends object>(
94
144
  WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
95
145
 
96
146
  return WithIdeButtonComponent;
97
- }
147
+ }