@keak/sdk 1.0.5 → 1.0.7

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.
@@ -28,9 +28,11 @@ function withKeak(nextConfig = {}) {
28
28
  console.log('[Keak] webpack() called with options:', { dev: options.dev, isServer: options.isServer });
29
29
  const { dev, isServer } = options;
30
30
 
31
- // Only inject in development mode and client-side
32
- if (dev && !isServer) {
33
- console.log('[Keak] Configuring Babel loader for source injection...');
31
+ // Only inject in development mode (both client and server)
32
+ // Server components also need attributes for source mapping
33
+ if (dev) {
34
+ console.log('[Keak] Configuring Keak webpack loader for source injection...');
35
+ console.log('[Keak] DEBUG: dev=', dev, 'isServer=', isServer, 'processing:', isServer ? 'server build' : 'client build');
34
36
 
35
37
  // Helper function to resolve modules from the project's node_modules
36
38
  const resolveFromProject = (moduleName) => {
@@ -46,89 +48,195 @@ function withKeak(nextConfig = {}) {
46
48
  }
47
49
  };
48
50
 
49
- // Try to find and modify existing babel-loader configurations
50
- let modifiedExistingLoader = false;
51
-
52
- config.module.rules.forEach(rule => {
53
- // Handle rules with use array
54
- if (rule.use && Array.isArray(rule.use)) {
55
- rule.use.forEach(loader => {
56
- if (typeof loader === 'object' && loader.loader && loader.loader.includes('babel-loader')) {
57
- // Found babel-loader, add our plugin if it's not already there
58
- if (!loader.options) loader.options = {};
59
- if (!loader.options.plugins) loader.options.plugins = [];
60
-
61
- // Check if our plugin is already in the list
62
- const hasKeakPlugin = loader.options.plugins.some(plugin =>
63
- (Array.isArray(plugin) && plugin[0] && plugin[0].includes('babel-source-inject')) ||
64
- (typeof plugin === 'string' && plugin.includes('babel-source-inject'))
65
- );
66
-
67
- if (!hasKeakPlugin) {
68
- loader.options.plugins.push([
69
- require.resolve('./babel-source-inject.cjs'),
70
- { includeElementIndex: true }
71
- ]);
72
- console.log('[Keak] Added source injection plugin to existing babel-loader');
73
- modifiedExistingLoader = true;
74
- }
75
- }
76
- });
51
+ // Ensure module.rules exists
52
+ config.module = config.module || {};
53
+ config.module.rules = config.module.rules || [];
54
+
55
+ // Add Keak loader to run pre-SWC for client code
56
+ // The loader resolves from .keak/webpack-loader-babel which is copied by keak-code during project setup.
57
+ const fs = require('fs');
58
+ const pathModule = require('path');
59
+
60
+ console.log('[Keak] About to resolve loader path...');
61
+ console.log('[Keak] Current working directory:', process.cwd());
62
+
63
+ // Try to find the loader in the project's .keak directory (copied by keak-code setup)
64
+ // Using the Babel-based loader that runs BEFORE SWC (pre-compilation)
65
+ const projectKeakLoader = pathModule.resolve(process.cwd(), '.keak/webpack-loader-babel/index.js');
66
+
67
+ let keakLoaderPath;
68
+ let loaderSource = 'unknown';
69
+
70
+ // Check if loader exists in project .keak directory
71
+ try {
72
+ if (fs.existsSync(projectKeakLoader)) {
73
+ console.log('[Keak] Found loader in project .keak directory');
74
+ keakLoaderPath = projectKeakLoader;
75
+ loaderSource = 'project';
76
+ } else {
77
+ // Fallback to project loader path even if it doesn't exist yet
78
+ console.warn('[Keak] Loader not found, using fallback path:', projectKeakLoader);
79
+ keakLoaderPath = projectKeakLoader;
80
+ loaderSource = 'fallback';
77
81
  }
82
+ } catch (pathError) {
83
+ console.error('[Keak] Error resolving loader path:', pathError.message);
84
+ keakLoaderPath = projectKeakLoader;
85
+ loaderSource = 'error';
86
+ }
78
87
 
79
- // Handle rules with use object
80
- if (rule.use && typeof rule.use === 'object' && !Array.isArray(rule.use)) {
81
- const loader = rule.use;
82
- if (loader.loader && loader.loader.includes('babel-loader')) {
83
- if (!loader.options) loader.options = {};
84
- if (!loader.options.plugins) loader.options.plugins = [];
85
-
86
- const hasKeakPlugin = loader.options.plugins.some(plugin =>
87
- (Array.isArray(plugin) && plugin[0] && plugin[0].includes('babel-source-inject')) ||
88
- (typeof plugin === 'string' && plugin.includes('babel-source-inject'))
89
- );
90
-
91
- if (!hasKeakPlugin) {
92
- loader.options.plugins.push([
93
- require.resolve('./babel-source-inject.cjs'),
94
- { includeElementIndex: true }
95
- ]);
96
- console.log('[Keak] Added source injection plugin to existing babel-loader (object form)');
97
- modifiedExistingLoader = true;
98
- }
99
- }
88
+ console.log('[Keak] Loader path resolved:', keakLoaderPath, '(source:', loaderSource, ')');
89
+ console.log('[Keak] About to inspect webpack rules...');
90
+ console.log('[Keak] DEBUG: config exists?', !!config);
91
+ console.log('[Keak] DEBUG: config.module exists?', !!config.module);
92
+ console.log('[Keak] DEBUG: config.module.rules exists?', !!(config.module && config.module.rules));
93
+ console.log('[Keak] DEBUG: config.module.rules is array?', Array.isArray(config.module && config.module.rules));
94
+
95
+ // Wrap everything in try-catch to catch any errors
96
+ try {
97
+ console.log('[Keak] DEBUG: Entering try block');
98
+ console.log('[Keak] DEBUG: config.module exists?', !!config.module);
99
+ console.log('[Keak] DEBUG: config.module.rules exists?', !!(config.module && config.module.rules));
100
+ console.log('[Keak] Total webpack rules:', config.module && config.module.rules ? config.module.rules.length : 'undefined');
101
+
102
+ if (!config.module || !config.module.rules) {
103
+ console.error('[Keak] ❌ ERROR: config.module.rules is undefined!');
104
+ return config;
105
+ }
106
+
107
+ // Log first few rules to understand structure
108
+ try {
109
+ console.log('[Keak] First few rules structure:');
110
+ config.module.rules.slice(0, 5).forEach((r, i) => {
111
+ console.log(`[Keak] Rule ${i}:`, {
112
+ hasOneOf: Array.isArray(r.oneOf),
113
+ oneOfLength: Array.isArray(r.oneOf) ? r.oneOf.length : 'N/A',
114
+ test: r.test ? r.test.toString().substring(0, 50) : 'no test',
115
+ hasUse: !!r.use,
116
+ useType: r.use ? (Array.isArray(r.use) ? 'array' : typeof r.use) : 'none'
117
+ });
118
+ });
119
+ } catch (e) {
120
+ console.log('[Keak] Error logging rules:', e.message);
100
121
  }
101
- });
102
122
 
103
- // If no existing babel-loader was modified, add a new rule
104
- if (!modifiedExistingLoader) {
105
- console.log('[Keak] No existing babel-loader found, adding new Babel loader with source injection plugin');
106
- // PREPEND the rule so it matches before Next.js's built-in rules
107
- config.module.rules.unshift({
108
- test: /\.(jsx|tsx)$/,
109
- exclude: /node_modules/,
110
- use: {
111
- loader: resolveFromProject('babel-loader'),
112
- options: {
113
- presets: [
114
- [resolveFromProject('@babel/preset-react'), {
115
- runtime: 'automatic'
116
- }],
117
- resolveFromProject('@babel/preset-typescript')
118
- ],
119
- plugins: [
120
- [
121
- require.resolve('./babel-source-inject.cjs'),
123
+ // Based on Next.js webpack structure: Find the oneOf array and add our loader
124
+ // Next.js uses oneOf arrays to process files - we need to add our loader there
125
+ let loaderAdded = false;
126
+
127
+ console.log('[Keak] Attempting Strategy 1: Finding oneOf rule...');
128
+ // Strategy 1: Find the oneOf rule (recommended approach from web search)
129
+ const oneOfRule = config.module.rules.find((rule) => Array.isArray(rule.oneOf));
130
+ console.log('[Keak] Strategy 1 result:', oneOfRule ? 'Found oneOf rule' : 'No oneOf rule found');
131
+
132
+ if (oneOfRule && oneOfRule.oneOf) {
133
+ console.log(`[Keak] ✅ Found oneOf array with ${oneOfRule.oneOf.length} entries`);
134
+
135
+ // Add our loader to the BEGINNING of oneOf array so it runs BEFORE SWC
136
+ // This transforms raw JSX to inject attributes before compilation
137
+ const preRule = {
138
+ test: /\.[jt]sx?$/,
139
+ exclude: [
140
+ /node_modules/,
141
+ /\.next\//,
142
+ /webpack/,
143
+ /next\/dist/
144
+ // NOTE: Removed /\.server\.[jt]sx?$/ exclusion to process server components
145
+ ],
146
+ enforce: 'pre', // Run BEFORE SWC (pre-compilation)
147
+ use: [
148
+ {
149
+ loader: keakLoaderPath,
150
+ options: {
151
+ projectRoot: process.cwd(),
152
+ includeElementIndex: true,
153
+ devWarnings: dev,
154
+ skipPatterns: [
155
+ // NOTE: Removed /(\.server\.[jt]sx?$)/ to process server components
156
+ /webpack/,
157
+ /\.next\//,
158
+ /next\/dist/
159
+ ]
160
+ }
161
+ }
162
+ ]
163
+ };
164
+
165
+ // Add to the BEGINNING of oneOf array (runs before SWC processes files)
166
+ oneOfRule.oneOf.unshift(preRule);
167
+
168
+ console.log(`[Keak] ✅ Added pre-compilation loader to oneOf array (now ${oneOfRule.oneOf.length} entries, runs before SWC)`);
169
+ loaderAdded = true;
170
+ } else {
171
+ console.log('[Keak] No oneOf array found, trying alternative approaches...');
172
+
173
+ // Strategy 2: Look for rules with oneOf property (nested)
174
+ for (let i = 0; i < config.module.rules.length; i++) {
175
+ const rule = config.module.rules[i];
176
+ if (rule.oneOf && Array.isArray(rule.oneOf)) {
177
+ console.log(`[Keak] Found oneOf in rule ${i} with ${rule.oneOf.length} entries`);
178
+
179
+ // Add pre-compilation loader to the BEGINNING of this oneOf array
180
+ rule.oneOf.unshift({
181
+ test: /\.[jt]sx?$/,
182
+ exclude: [/node_modules/, /\.next\//, /webpack/, /next\/dist/],
183
+ enforce: 'pre', // Run BEFORE SWC
184
+ use: [
122
185
  {
123
- includeElementIndex: true
186
+ loader: keakLoaderPath,
187
+ options: {
188
+ projectRoot: process.cwd(),
189
+ includeElementIndex: true,
190
+ devWarnings: dev,
191
+ skipPatterns: [] // Process all files including server components
192
+ }
124
193
  }
125
194
  ]
126
- ],
127
- cacheDirectory: true
195
+ });
196
+ console.log(`[Keak] ✅ Added pre-compilation loader to oneOf array (now ${rule.oneOf.length} entries)`);
197
+ loaderAdded = true;
198
+ break;
128
199
  }
129
200
  }
130
- });
201
+ }
202
+
203
+ // Strategy 3: Fallback - Add as new rule with enforce: 'pre' at top level
204
+ // This is less ideal but should still work
205
+ if (!loaderAdded) {
206
+ console.log('[Keak] Fallback: Adding pre-compilation loader rule to top-level rules');
207
+ config.module.rules.unshift({
208
+ test: /\.[jt]sx?$/,
209
+ exclude: [/node_modules/, /\.next\//, /webpack/, /next\/dist/],
210
+ enforce: 'pre', // Run BEFORE SWC
211
+ use: [
212
+ {
213
+ loader: keakLoaderPath,
214
+ options: {
215
+ projectRoot: process.cwd(),
216
+ includeElementIndex: true,
217
+ devWarnings: dev,
218
+ skipPatterns: [] // Process all files including server components
219
+ }
220
+ }
221
+ ]
222
+ });
223
+ console.log('[Keak] ✅ Added pre-compilation loader rule with enforce:pre (fallback)');
224
+ loaderAdded = true;
225
+ }
226
+
227
+ if (!loaderAdded) {
228
+ console.error('[Keak] ❌ ERROR: Failed to add loader using any strategy!');
229
+ } else {
230
+ console.log('[Keak] ✅ Loader successfully added to webpack config');
231
+ }
232
+ } catch (error) {
233
+ console.error('[Keak] ❌ ERROR in loader injection block:', error);
234
+ console.error('[Keak] Error message:', error.message);
235
+ console.error('[Keak] Error stack:', error.stack);
236
+ // Don't throw - let webpack continue even if loader injection fails
131
237
  }
238
+ } else {
239
+ console.log('[Keak] Skipping loader injection - dev:', dev, 'isServer:', isServer);
132
240
  }
133
241
 
134
242
  // Call the original webpack config function if it exists
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Webpack loader that uses Babel to inject Keak source attributes into JSX
3
+ * This runs BEFORE SWC compilation to add data-keak-src, data-keak-component, and data-keak-idx attributes
4
+ */
5
+
6
+ const path = require('path');
7
+ const babelParser = require('@babel/parser');
8
+ const babelTraverse = require('@babel/traverse').default;
9
+ const babelGenerator = require('@babel/generator').default;
10
+ const t = require('@babel/types');
11
+
12
+ let elementIndexMap = new Map(); // Track element index per file
13
+
14
+ module.exports = function keakLoaderBabel(source) {
15
+ // Make loader cacheable for better performance
16
+ this.cacheable(true);
17
+
18
+ const resourcePath = this.resourcePath;
19
+ const options = this.getOptions() || {};
20
+ const { projectRoot = process.cwd(), includeElementIndex = true, devWarnings = false } = options;
21
+
22
+ // Skip if not a JSX/TSX file
23
+ if (!/\.(jsx|tsx|js|ts)$/.test(resourcePath)) {
24
+ return source;
25
+ }
26
+
27
+ // Skip files based on skip patterns
28
+ const skipPatterns = options.skipPatterns || [];
29
+ if (skipPatterns.some(pattern => {
30
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
31
+ return regex.test(resourcePath);
32
+ })) {
33
+ return source;
34
+ }
35
+
36
+ try {
37
+ console.log(`[keak-loader-pre] Processing ${path.relative(projectRoot || process.cwd(), resourcePath)} with Babel AST`);
38
+
39
+ // Reset element index for this file
40
+ if (includeElementIndex) {
41
+ elementIndexMap.set(resourcePath, 0);
42
+ }
43
+
44
+ // Parse the source code
45
+ const ast = babelParser.parse(source, {
46
+ sourceType: 'module',
47
+ plugins: [
48
+ 'jsx',
49
+ 'typescript',
50
+ 'decorators-legacy',
51
+ 'classProperties',
52
+ 'objectRestSpread',
53
+ 'functionBind',
54
+ 'exportDefaultFrom',
55
+ 'exportNamespaceFrom',
56
+ 'dynamicImport',
57
+ 'nullishCoalescingOperator',
58
+ 'optionalChaining',
59
+ 'topLevelAwait'
60
+ ]
61
+ });
62
+
63
+ let elementCount = 0;
64
+ let skipFile = false;
65
+
66
+ // First pass: Check for next/font imports (Babel/SWC conflict)
67
+ babelTraverse(ast, {
68
+ ImportDeclaration(importPath) {
69
+ const source = importPath.node.source.value;
70
+ if (source.includes('next/font') || source.includes('@next/font')) {
71
+ skipFile = true;
72
+ importPath.stop();
73
+ }
74
+ }
75
+ });
76
+
77
+ if (skipFile) {
78
+ if (devWarnings) {
79
+ console.log(`[keak-loader-pre] ⚠️ Skipping ${path.relative(projectRoot || process.cwd(), resourcePath)} (contains next/font)`);
80
+ }
81
+ return source;
82
+ }
83
+
84
+ // Second pass: Transform JSX elements
85
+ babelTraverse(ast, {
86
+ JSXOpeningElement(nodePath) {
87
+ const node = nodePath.node;
88
+
89
+ // Skip if element already has keak source attribute
90
+ const hasKeakSrc = node.attributes.some(attr =>
91
+ t.isJSXAttribute(attr) &&
92
+ t.isJSXIdentifier(attr.name) &&
93
+ attr.name.name === 'data-keak-src'
94
+ );
95
+
96
+ if (hasKeakSrc) {
97
+ return;
98
+ }
99
+
100
+ // Get source location
101
+ const loc = node.loc;
102
+ if (!loc) return;
103
+
104
+ // Create relative path from project root
105
+ const relativePath = path.relative(projectRoot || process.cwd(), resourcePath).replace(/\\/g, '/');
106
+
107
+ // Create source location string: "file:line:column"
108
+ const sourceLocation = `${relativePath}:${loc.start.line}:${loc.start.column}`;
109
+
110
+ // Get element name
111
+ let tagName = '';
112
+ let componentName = '';
113
+
114
+ if (t.isJSXIdentifier(node.name)) {
115
+ tagName = node.name.name;
116
+ // Try to find component name from parent scope
117
+ const parentFunction = nodePath.getFunctionParent();
118
+ if (parentFunction && parentFunction.isFunctionDeclaration()) {
119
+ componentName = parentFunction.node.id?.name || '';
120
+ } else if (parentFunction && parentFunction.isArrowFunctionExpression()) {
121
+ const parentVar = parentFunction.getFunctionParent();
122
+ if (parentVar && parentVar.isVariableDeclarator() && t.isIdentifier(parentVar.node.id)) {
123
+ componentName = parentVar.node.id.name;
124
+ }
125
+ }
126
+ } else if (t.isJSXMemberExpression(node.name)) {
127
+ tagName = node.name.property.name;
128
+ }
129
+
130
+ // Only instrument DOM elements (lowercase) or all elements if configured
131
+ const isDOMElement = /^[a-z]/.test(tagName);
132
+
133
+ if (!isDOMElement && !options.instrumentComponents) {
134
+ return;
135
+ }
136
+
137
+ // Get element index if enabled
138
+ let elementIdx = null;
139
+ if (includeElementIndex) {
140
+ const currentIndex = elementIndexMap.get(resourcePath) || 0;
141
+ elementIdx = currentIndex;
142
+ elementIndexMap.set(resourcePath, currentIndex + 1);
143
+ }
144
+
145
+ // Create attributes to inject
146
+ const attributesToAdd = [
147
+ t.jsxAttribute(
148
+ t.jsxIdentifier('data-keak-src'),
149
+ t.stringLiteral(sourceLocation)
150
+ )
151
+ ];
152
+
153
+ // Add component name if available
154
+ if (componentName) {
155
+ attributesToAdd.push(
156
+ t.jsxAttribute(
157
+ t.jsxIdentifier('data-keak-component'),
158
+ t.stringLiteral(componentName)
159
+ )
160
+ );
161
+ }
162
+
163
+ // Add element index if enabled
164
+ if (elementIdx !== null) {
165
+ attributesToAdd.push(
166
+ t.jsxAttribute(
167
+ t.jsxIdentifier('data-keak-idx'),
168
+ t.stringLiteral(String(elementIdx))
169
+ )
170
+ );
171
+ }
172
+
173
+ // Add attributes to the node
174
+ node.attributes.push(...attributesToAdd);
175
+ elementCount++;
176
+ }
177
+ });
178
+
179
+ // Generate the transformed code
180
+ const output = babelGenerator(ast, {
181
+ retainLines: false,
182
+ compact: false,
183
+ sourceMaps: false
184
+ }, source);
185
+
186
+ if (elementCount > 0) {
187
+ console.log(`[keak-loader-pre] ✓ Injected attributes into ${elementCount} elements in ${path.relative(projectRoot || process.cwd(), resourcePath)}`);
188
+ }
189
+
190
+ return output.code;
191
+ } catch (error) {
192
+ // If transformation fails, return original source
193
+ console.error(`[keak-loader-pre] ❌ Error processing ${resourcePath}:`, error.message);
194
+ if (devWarnings) {
195
+ console.error('[keak-loader-pre] Stack:', error.stack);
196
+ }
197
+ return source;
198
+ }
199
+ };