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/README.md +175 -10
- package/lib/__tests__/loader.test.js +480 -0
- package/lib/index.ts +8 -0
- package/lib/loader.js +222 -33
- package/lib/plugin.js +26 -2
- package/lib/withIdeButton.tsx +97 -0
- package/package.json +9 -2
- package/src/__tests__/loader.test.js +480 -0
- package/src/loader.js +222 -33
- package/src/plugin.js +25 -1
package/src/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
|
|
26
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
153
|
+
// Skip this check for anonymous components
|
|
154
|
+
if (defaultExportName && defaultExportName[0] !== defaultExportName[0].toUpperCase()) {
|
|
48
155
|
return source;
|
|
49
156
|
}
|
|
50
157
|
|
|
51
|
-
//
|
|
52
|
-
|
|
158
|
+
// If withIdeButton is already imported, don't process
|
|
159
|
+
if (hasWithIdeButtonImport) {
|
|
160
|
+
return source;
|
|
161
|
+
}
|
|
53
162
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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/src/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,18 +33,29 @@ 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:
|
|
58
|
+
include: includeDirectories,
|
|
38
59
|
use: [
|
|
39
60
|
{
|
|
40
61
|
loader: require.resolve('./loader.js'),
|
|
@@ -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
|