nextjs-ide-helper 1.1.3 → 1.3.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/lib/loader.js CHANGED
@@ -1,4 +1,9 @@
1
1
  const path = require('path');
2
+ const { parse } = require('@babel/parser');
3
+ const traverse = require('@babel/traverse').default;
4
+ const generate = require('@babel/generator').default;
5
+ const t = require('@babel/types');
6
+ const { minimatch } = require('minimatch');
2
7
 
3
8
  /**
4
9
  * Webpack loader that automatically wraps React components with cursor buttons
@@ -9,6 +14,8 @@ module.exports = function cursorButtonLoader(source) {
9
14
  const filename = this.resourcePath;
10
15
  const options = this.getOptions() || {};
11
16
 
17
+ console.log('🔧 Loader called for file:', filename);
18
+
12
19
  const {
13
20
  componentPaths = ['src/components'],
14
21
  projectRoot = process.cwd(),
@@ -18,62 +25,244 @@ module.exports = function cursorButtonLoader(source) {
18
25
 
19
26
  // Only process if enabled
20
27
  if (!enabled) {
28
+ console.log('🔧 Loader disabled, returning original source');
21
29
  return source;
22
30
  }
23
31
 
24
32
  // Only process files in specified component directories
25
- const shouldProcess = componentPaths.some(componentPath =>
26
- filename.includes(path.resolve(projectRoot, componentPath))
27
- );
33
+ const relativePath = path.relative(projectRoot, filename);
34
+ console.log('🔧 Processing file:', relativePath, 'against paths:', componentPaths);
35
+
36
+ const shouldProcess = componentPaths.some(componentPath => {
37
+ console.log('🔧 Checking path:', componentPath);
38
+ // Check if componentPath contains glob patterns
39
+ if (componentPath.includes('*')) {
40
+ const matches = minimatch(relativePath, componentPath);
41
+ console.log('🔧 Glob match result:', matches, 'for pattern:', componentPath);
42
+ return matches;
43
+ } else {
44
+ // Fallback to the original behavior for non-glob patterns
45
+ const matches = filename.includes(path.resolve(projectRoot, componentPath));
46
+ console.log('🔧 Direct path match result:', matches);
47
+ return matches;
48
+ }
49
+ });
50
+
51
+ console.log('🔧 Should process:', shouldProcess);
28
52
 
29
53
  if (!shouldProcess || (!filename.endsWith('.tsx') && !filename.endsWith('.jsx'))) {
54
+ console.log('🔧 File does not match criteria, skipping');
30
55
  return source;
31
56
  }
32
57
 
33
- // Check if it's already wrapped
58
+ // Check if it's already wrapped using simple string check for performance
34
59
  if (source.includes('withIdeButton')) {
60
+ console.log('🔧 File already contains withIdeButton, skipping');
35
61
  return source;
36
62
  }
37
63
 
38
- // Check if it has a default export that looks like a React component
39
- const defaultExportMatch = source.match(/export\s+default\s+(\w+)/);
40
- if (!defaultExportMatch) {
64
+ console.log('🔧 Proceeding to transform file:', relativePath);
65
+
66
+ let ast;
67
+ try {
68
+ // Parse the source code into an AST
69
+ ast = parse(source, {
70
+ sourceType: 'module',
71
+ plugins: [
72
+ 'jsx',
73
+ 'typescript',
74
+ 'decorators-legacy',
75
+ 'classProperties',
76
+ 'objectRestSpread',
77
+ 'asyncGenerators',
78
+ 'functionBind',
79
+ 'exportDefaultFrom',
80
+ 'exportNamespaceFrom',
81
+ 'dynamicImport',
82
+ 'nullishCoalescingOperator',
83
+ 'optionalChaining'
84
+ ]
85
+ });
86
+ } catch (error) {
87
+ // If parsing fails, return original source
88
+ return source;
89
+ }
90
+
91
+ let hasDefaultExport = false;
92
+ let defaultExportName = null;
93
+ let isAnonymousComponent = false;
94
+ let hasWithIdeButtonImport = false;
95
+ let lastImportPath = null;
96
+ let defaultExportPath = null;
97
+
98
+ // Traverse the AST to analyze the code
99
+ traverse(ast, {
100
+ ImportDeclaration(path) {
101
+ lastImportPath = path;
102
+
103
+ // Check if withIdeButton is already imported
104
+ if (path.node.source.value === importPath) {
105
+ path.node.specifiers.forEach(spec => {
106
+ if (t.isImportSpecifier(spec) && spec.imported.name === 'withIdeButton') {
107
+ hasWithIdeButtonImport = true;
108
+ }
109
+ });
110
+ }
111
+ },
112
+
113
+ ExportDefaultDeclaration(path) {
114
+ hasDefaultExport = true;
115
+ defaultExportPath = path;
116
+
117
+ if (t.isIdentifier(path.node.declaration)) {
118
+ // export default ComponentName
119
+ defaultExportName = path.node.declaration.name;
120
+ } else if (t.isFunctionDeclaration(path.node.declaration)) {
121
+ if (path.node.declaration.id) {
122
+ // export default function ComponentName() {}
123
+ defaultExportName = path.node.declaration.id.name;
124
+ } else {
125
+ // export default function() {} - anonymous function
126
+ isAnonymousComponent = true;
127
+ }
128
+ } else if (t.isClassDeclaration(path.node.declaration) && path.node.declaration.id) {
129
+ // export default class ComponentName extends ... {}
130
+ defaultExportName = path.node.declaration.id.name;
131
+ } else if (t.isArrowFunctionExpression(path.node.declaration)) {
132
+ // export default () => {} - anonymous arrow function
133
+ isAnonymousComponent = true;
134
+ } else if (t.isVariableDeclaration(path.node.declaration)) {
135
+ // export default const ComponentName = ...
136
+ const declarator = path.node.declaration.declarations[0];
137
+ if (t.isIdentifier(declarator.id)) {
138
+ defaultExportName = declarator.id.name;
139
+ }
140
+ }
141
+
142
+ // Stop traversal once we find the default export
143
+ path.stop();
144
+ }
145
+ });
146
+
147
+ // Check if we should process this file
148
+ if (!hasDefaultExport || (!defaultExportName && !isAnonymousComponent)) {
41
149
  return source;
42
150
  }
43
151
 
44
- const componentName = defaultExportMatch[1];
45
-
46
152
  // Check if component name starts with uppercase (React component convention)
47
- if (!componentName || componentName[0] !== componentName[0].toUpperCase()) {
153
+ // Skip this check for anonymous components
154
+ if (defaultExportName && defaultExportName[0] !== defaultExportName[0].toUpperCase()) {
48
155
  return source;
49
156
  }
50
157
 
51
- // Get relative path from project root
52
- const relativePath = path.relative(projectRoot, filename);
158
+ // If withIdeButton is already imported, don't process
159
+ if (hasWithIdeButtonImport) {
160
+ return source;
161
+ }
53
162
 
54
- // Find the last import to insert our import after it
55
- const imports = source.match(/^import.*$/gm) || [];
56
-
57
- let modifiedSource;
58
-
59
- if (imports.length === 0) {
60
- // No imports found, add at top
61
- const importStatement = `import { withIdeButton } from '${importPath}';\n`;
62
- const wrappedExport = `export default withIdeButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
63
- modifiedSource = importStatement + source.replace(/export\s+default\s+\w+;?/, wrappedExport);
163
+ // relativePath already declared above for glob matching
164
+
165
+ // Transform the AST
166
+ let modified = false;
167
+
168
+ // Add the withIdeButton import
169
+ const withIdeButtonImport = t.importDeclaration(
170
+ [t.importSpecifier(t.identifier('withIdeButton'), t.identifier('withIdeButton'))],
171
+ t.stringLiteral(importPath)
172
+ );
173
+
174
+ // Insert import after last existing import or at the beginning
175
+ if (lastImportPath) {
176
+ lastImportPath.insertAfter(withIdeButtonImport);
177
+ modified = true;
64
178
  } else {
65
- const lastImportIndex = source.lastIndexOf(imports[imports.length - 1]) + imports[imports.length - 1].length;
66
-
67
- // Add import statement
68
- const importStatement = `\nimport { withIdeButton } from '${importPath}';`;
69
-
70
- // Replace the export
71
- const wrappedExport = `export default withIdeButton(${componentName}, '${relativePath}', { projectRoot: '${projectRoot}' });`;
179
+ // No imports found, add at the beginning
180
+ if (ast.program && ast.program.body && Array.isArray(ast.program.body)) {
181
+ ast.program.body.unshift(withIdeButtonImport);
182
+ modified = true;
183
+ }
184
+ }
185
+
186
+ // Replace the default export with wrapped version
187
+ if (defaultExportPath && modified) {
188
+ let wrappedCall;
72
189
 
73
- modifiedSource = source.slice(0, lastImportIndex) +
74
- importStatement +
75
- source.slice(lastImportIndex).replace(/export\s+default\s+\w+;?/, wrappedExport);
190
+ if (isAnonymousComponent) {
191
+ // For anonymous components, wrap the original function/arrow function
192
+ let componentExpression;
193
+
194
+ if (t.isFunctionDeclaration(defaultExportPath.node.declaration)) {
195
+ // Convert anonymous function declaration to function expression
196
+ componentExpression = t.functionExpression(
197
+ null, // id is null for anonymous
198
+ defaultExportPath.node.declaration.params,
199
+ defaultExportPath.node.declaration.body,
200
+ defaultExportPath.node.declaration.generator,
201
+ defaultExportPath.node.declaration.async
202
+ );
203
+ } else {
204
+ // For arrow functions and other expressions, use directly
205
+ componentExpression = defaultExportPath.node.declaration;
206
+ }
207
+
208
+ wrappedCall = t.callExpression(
209
+ t.identifier('withIdeButton'),
210
+ [
211
+ componentExpression,
212
+ t.stringLiteral(relativePath),
213
+ t.objectExpression([
214
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
215
+ ])
216
+ ]
217
+ );
218
+ } else {
219
+ // For named components, wrap with the component name identifier
220
+ wrappedCall = t.callExpression(
221
+ t.identifier('withIdeButton'),
222
+ [
223
+ t.identifier(defaultExportName),
224
+ t.stringLiteral(relativePath),
225
+ t.objectExpression([
226
+ t.objectProperty(t.identifier('projectRoot'), t.stringLiteral(projectRoot))
227
+ ])
228
+ ]
229
+ );
230
+ }
231
+
232
+ // If the export is a named function or class declaration, we need to handle it differently
233
+ if (!isAnonymousComponent &&
234
+ (t.isFunctionDeclaration(defaultExportPath.node.declaration) ||
235
+ t.isClassDeclaration(defaultExportPath.node.declaration))) {
236
+ // Insert the function/class declaration before the export
237
+ const declaration = defaultExportPath.node.declaration;
238
+ defaultExportPath.insertBefore(declaration);
239
+
240
+ // Replace the export declaration with the wrapped call
241
+ defaultExportPath.node.declaration = wrappedCall;
242
+ } else {
243
+ // For other cases (identifier, variable declaration, anonymous functions), just replace the declaration
244
+ defaultExportPath.node.declaration = wrappedCall;
245
+ }
76
246
  }
77
247
 
78
- return modifiedSource;
248
+ // Generate the transformed code
249
+ if (modified) {
250
+ try {
251
+ const result = generate(ast, {
252
+ retainLines: false,
253
+ compact: false,
254
+ jsescOption: {
255
+ quotes: 'single'
256
+ }
257
+ });
258
+ return result.code;
259
+ } catch (error) {
260
+ // If generation fails, return original source
261
+ return source;
262
+ }
263
+ }
264
+
265
+ // If we reach here, nothing was modified
266
+
267
+ return source;
79
268
  };
package/lib/plugin.js CHANGED
@@ -9,6 +9,16 @@ const path = require('path');
9
9
  * @property {boolean} [enabled=process.env.NODE_ENV === 'development'] - Enable/disable the plugin
10
10
  */
11
11
 
12
+ /**
13
+ * Extract base directory from a glob pattern
14
+ * @param {string} globPattern - The glob pattern
15
+ * @returns {string} - The base directory
16
+ */
17
+ function extractBaseDirectory(globPattern) {
18
+ // Remove glob patterns (**/ */ etc.) to get the base directory
19
+ return globPattern.replace(/\/\*\*.*$/, '').replace(/\/\*.*$/, '');
20
+ }
21
+
12
22
  /**
13
23
  * NextJS Cursor Helper plugin
14
24
  * @param {CursorHelperOptions} options - Configuration options
@@ -23,21 +33,32 @@ function withCursorHelper(options = {}) {
23
33
  };
24
34
 
25
35
  const config = { ...defaultOptions, ...options };
36
+
37
+ console.log('🔧 Plugin initialized with config:', config);
26
38
 
27
39
  return (nextConfig = {}) => {
28
40
  return {
29
41
  ...nextConfig,
30
42
  webpack: (webpackConfig, context) => {
31
43
  const { dev, isServer } = context;
44
+
45
+ console.log('🔧 Webpack config called:', { dev, isServer, enabled: config.enabled });
32
46
 
33
47
  // Only apply in development and for client-side
34
48
  if (config.enabled && dev && !isServer) {
49
+ // Extract base directories from glob patterns for webpack's include
50
+ const includeDirectories = [...new Set(
51
+ config.componentPaths.map(p => extractBaseDirectory(p))
52
+ )].map(p => path.resolve(config.projectRoot, p));
53
+
54
+ console.log('🔧 Adding webpack rule with include directories:', includeDirectories);
55
+
35
56
  const rule = {
36
57
  test: /\.tsx?$/,
37
- include: config.componentPaths.map(p => path.resolve(config.projectRoot, p)),
58
+ include: includeDirectories,
38
59
  use: [
39
60
  {
40
- loader: require.resolve('./loader'),
61
+ loader: require.resolve('./loader.js'),
41
62
  options: config
42
63
  }
43
64
  ],
@@ -45,6 +66,9 @@ function withCursorHelper(options = {}) {
45
66
  };
46
67
 
47
68
  webpackConfig.module.rules.unshift(rule);
69
+ console.log('🔧 Webpack rule added successfully');
70
+ } else {
71
+ console.log('🔧 Plugin conditions not met, skipping webpack rule');
48
72
  }
49
73
 
50
74
  // Call the existing webpack function if it exists
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+ import React, { useState, useEffect } from 'react';
3
+
4
+ interface IdeButtonProps {
5
+ filePath: string;
6
+ projectRoot?: string;
7
+ ideType?: 'cursor' | 'vscode' | 'webstorm' | 'atom';
8
+ }
9
+
10
+ const IdeButton: React.FC<IdeButtonProps> = ({ filePath, projectRoot, ideType = 'cursor' }) => {
11
+ const getIdeUrl = (ide: string, path: string) => {
12
+ const fullPath = projectRoot ? `${projectRoot}/${path}` : path;
13
+
14
+ switch (ide) {
15
+ case 'cursor':
16
+ return `cursor://file${fullPath}`;
17
+ case 'vscode':
18
+ return `vscode://file${fullPath}`;
19
+ case 'webstorm':
20
+ return `webstorm://open?file=${fullPath}`;
21
+ case 'atom':
22
+ return `atom://open?path=${fullPath}`;
23
+ default:
24
+ return `cursor://file${fullPath}`;
25
+ }
26
+ };
27
+
28
+ const handleClick = () => {
29
+ const url = getIdeUrl(ideType, filePath);
30
+ window.open(url, '_blank');
31
+ };
32
+
33
+ return (
34
+ <button
35
+ onClick={handleClick}
36
+ style={{
37
+ position: 'absolute',
38
+ top: '8px',
39
+ right: '8px',
40
+ background: '#007acc',
41
+ color: 'white',
42
+ border: 'none',
43
+ borderRadius: '4px',
44
+ padding: '4px 8px',
45
+ fontSize: '12px',
46
+ cursor: 'pointer',
47
+ zIndex: 1000,
48
+ opacity: 0.7,
49
+ transition: 'opacity 0.2s',
50
+ fontFamily: 'monospace'
51
+ }}
52
+ onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
53
+ onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.7')}
54
+ title={`Open ${filePath} in ${ideType.toUpperCase()}`}
55
+ >
56
+ 📝
57
+ </button>
58
+ );
59
+ };
60
+
61
+ export interface WithIdeButtonOptions {
62
+ projectRoot?: string;
63
+ enabled?: boolean;
64
+ ideType?: 'cursor' | 'vscode' | 'webstorm' | 'atom';
65
+ }
66
+
67
+ export function withIdeButton<T extends object>(
68
+ WrappedComponent: React.ComponentType<T>,
69
+ filePath: string,
70
+ options: WithIdeButtonOptions = {}
71
+ ) {
72
+ const { projectRoot, enabled = process.env.NODE_ENV === 'development', ideType = 'cursor' } = options;
73
+
74
+ const WithIdeButtonComponent: React.FC<T> = (props) => {
75
+ const [isClient, setIsClient] = useState(false);
76
+
77
+ useEffect(() => {
78
+ setIsClient(true);
79
+ }, []);
80
+
81
+ // In production or when disabled, just return the component without wrapper
82
+ if (!enabled || !isClient) {
83
+ return <WrappedComponent {...props} />;
84
+ }
85
+
86
+ return (
87
+ <div style={{ position: 'relative' }}>
88
+ <IdeButton filePath={filePath} projectRoot={projectRoot} ideType={ideType} />
89
+ <WrappedComponent {...props} />
90
+ </div>
91
+ );
92
+ };
93
+
94
+ WithIdeButtonComponent.displayName = `withIdeButton(${WrappedComponent.displayName || WrappedComponent.name})`;
95
+
96
+ return WithIdeButtonComponent;
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-ide-helper",
3
- "version": "1.1.3",
3
+ "version": "1.3.1",
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,15 @@
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",
44
+ "minimatch": "^10.0.3",
38
45
  "typescript": "^5.8.3"
39
46
  },
40
47
  "repository": {