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/README.md +181 -16
- package/lib/loader.js +122 -42
- package/lib/plugin.js +0 -3
- package/lib/withIdeButton.js +71 -24
- package/lib/withIdeButton.tsx +78 -28
- package/package.json +2 -2
- package/src/__tests__/loader.test.js +338 -2
- package/src/loader.js +122 -37
- package/src/withIdeButton.tsx +78 -28
- package/lib/__tests__/loader.test.js +0 -480
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
|
-
|
|
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
|
+
// 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 &&
|
|
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
|
|
233
|
-
if (!isAnonymousComponent &&
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
package/src/withIdeButton.tsx
CHANGED
|
@@ -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
|
-
|
|
17
|
-
case '
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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: '
|
|
39
|
-
right: '
|
|
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: '
|
|
44
|
-
padding:
|
|
45
|
-
fontSize: '12px',
|
|
98
|
+
borderRadius: '50%',
|
|
99
|
+
padding: 0,
|
|
46
100
|
cursor: 'pointer',
|
|
47
101
|
zIndex: 1000,
|
|
48
|
-
opacity: 0.
|
|
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.
|
|
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
|
+
}
|