@keak/sdk 2.0.3 → 2.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keak/sdk",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Production-ready A/B testing and experimentation SDK for React applications with visual editing, source mapping, and real-time variant testing",
5
5
  "author": "Keak Team",
6
6
  "homepage": "https://www.keak.com/",
@@ -75,7 +75,10 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
+ "@babel/core": "^7.26.0",
78
79
  "@babel/parser": "^7.26.3",
80
+ "@babel/preset-react": "^7.26.3",
81
+ "@babel/preset-typescript": "^7.26.0",
79
82
  "@babel/traverse": "^7.26.5",
80
83
  "@babel/types": "^7.26.5",
81
84
  "clsx": "^2.1.1",
@@ -91,6 +94,7 @@
91
94
  "@types/node": "^24.3.0",
92
95
  "@types/react": "^18.0.0",
93
96
  "@types/react-dom": "^18.3.7",
97
+ "@vitejs/plugin-react": "^5.1.2",
94
98
  "autoprefixer": "^10.4.0",
95
99
  "chokidar-cli": "^3.0.0",
96
100
  "concurrently": "^8.2.2",
@@ -1,41 +1,93 @@
1
1
  /**
2
2
  * Keak Next.js Plugin
3
- * Simplified version - injects source location attributes in development only
4
- *
3
+ * Injects source location attributes into JSX elements in development mode
4
+ *
5
+ * This enables Keak's element selection feature to map DOM elements back to source code.
6
+ *
5
7
  * Usage in next.config.js:
6
- * const { withKeak } = require('@keak/sdk/next');
8
+ * const { withKeak } = require('@keak/sdk/plugins/next');
7
9
  * module.exports = withKeak({ ... });
10
+ *
11
+ * IMPORTANT: This plugin only works with webpack, not Turbopack.
12
+ * Make sure your dev script does NOT include --turbopack flag.
8
13
  */
9
14
 
10
15
  const path = require('path');
11
16
  const fs = require('fs');
12
17
 
18
+ /**
19
+ * Check if Babel dependencies are available
20
+ * The loader needs @babel/core to transform JSX
21
+ */
22
+ function checkBabelAvailable(projectRoot) {
23
+ const babelCorePath = path.join(projectRoot, 'node_modules', '@babel', 'core');
24
+ const babelReactPath = path.join(projectRoot, 'node_modules', '@babel', 'preset-react');
25
+
26
+ const hasBabelCore = fs.existsSync(babelCorePath);
27
+ const hasBabelReact = fs.existsSync(babelReactPath);
28
+
29
+ return {
30
+ available: hasBabelCore && hasBabelReact,
31
+ hasBabelCore,
32
+ hasBabelReact
33
+ };
34
+ }
35
+
13
36
  function withKeak(nextConfig = {}) {
14
37
  return {
15
38
  ...nextConfig,
16
39
  webpack(config, options) {
17
40
  const { dev, isServer } = options;
41
+ const projectRoot = process.cwd();
18
42
 
19
43
  console.log('[Keak Plugin] webpack called - dev:', dev, 'isServer:', isServer);
20
44
 
21
45
  // Only inject loader in development mode
22
46
  if (!dev) {
23
47
  console.log('[Keak Plugin] Skipping - not in development mode');
24
- // Call original webpack config if it exists
25
48
  if (typeof nextConfig.webpack === 'function') {
26
49
  return nextConfig.webpack(config, options);
27
50
  }
28
51
  return config;
29
52
  }
30
53
 
31
- // Resolve loader path from project .keak directory or fallback
32
- const projectKeakLoader = path.resolve(process.cwd(), '.keak/webpack-loader-babel/index.js');
33
- const keakLoaderPath = fs.existsSync(projectKeakLoader)
34
- ? projectKeakLoader
35
- : path.resolve(__dirname, 'webpack-loader-babel/index.js');
54
+ // Check if Babel is available (required for AST transformation)
55
+ const babelCheck = checkBabelAvailable(projectRoot);
56
+ if (!babelCheck.available) {
57
+ console.warn('[Keak Plugin] ⚠️ Babel not fully available for source mapping');
58
+ if (!babelCheck.hasBabelCore) {
59
+ console.warn('[Keak Plugin] Missing: @babel/core');
60
+ }
61
+ if (!babelCheck.hasBabelReact) {
62
+ console.warn('[Keak Plugin] Missing: @babel/preset-react');
63
+ }
64
+ console.warn('[Keak Plugin] Source mapping may not work. Run: npm install @babel/core @babel/preset-react');
65
+ // Continue anyway - the loader will gracefully handle missing deps
66
+ }
67
+
68
+ // Resolve loader path from project .keak directory or fallback to SDK
69
+ const projectKeakLoader = path.resolve(projectRoot, '.keak/webpack-loader-babel/index.js');
70
+ const sdkLoaderPath = path.resolve(__dirname, 'webpack-loader-babel/index.js');
71
+
72
+ let keakLoaderPath = null;
36
73
 
37
- console.log('[Keak Plugin] Using loader path:', keakLoaderPath);
38
- console.log('[Keak Plugin] Loader exists:', fs.existsSync(keakLoaderPath));
74
+ // Prefer project-local loader (copied during setup)
75
+ if (fs.existsSync(projectKeakLoader)) {
76
+ keakLoaderPath = projectKeakLoader;
77
+ console.log('[Keak Plugin] Using project loader:', projectKeakLoader);
78
+ } else if (fs.existsSync(sdkLoaderPath)) {
79
+ keakLoaderPath = sdkLoaderPath;
80
+ console.log('[Keak Plugin] Using SDK loader:', sdkLoaderPath);
81
+ } else {
82
+ console.warn('[Keak Plugin] ⚠️ No loader found! Source mapping will not work.');
83
+ console.warn('[Keak Plugin] Expected at:', projectKeakLoader);
84
+ console.warn('[Keak Plugin] Or at:', sdkLoaderPath);
85
+ // Continue without loader - don't break the build
86
+ if (typeof nextConfig.webpack === 'function') {
87
+ return nextConfig.webpack(config, options);
88
+ }
89
+ return config;
90
+ }
39
91
 
40
92
  // Ensure config.module.rules exists
41
93
  if (!config.module || !config.module.rules) {
@@ -44,20 +96,23 @@ function withKeak(nextConfig = {}) {
44
96
  }
45
97
 
46
98
  // Create the loader rule
99
+ // IMPORTANT: Only match .jsx and .tsx files to avoid processing non-JSX code
100
+ // This prevents issues with TypeScript generics being misinterpreted as JSX
47
101
  const keakRule = {
48
- test: /\.[jt]sx?$/,
102
+ test: /\.(jsx|tsx)$/, // Only JSX/TSX files (not .js/.ts)
49
103
  exclude: [
50
104
  /node_modules/,
51
105
  /\.next\//,
52
106
  /webpack/,
53
- /next\/dist/
107
+ /next\/dist/,
108
+ /\.keak\//, // Don't process our own loader files
54
109
  ],
55
110
  enforce: 'pre', // Run BEFORE SWC compilation
56
111
  use: [
57
112
  {
58
113
  loader: keakLoaderPath,
59
114
  options: {
60
- projectRoot: process.cwd()
115
+ projectRoot: projectRoot
61
116
  }
62
117
  }
63
118
  ]
@@ -68,9 +123,11 @@ function withKeak(nextConfig = {}) {
68
123
  if (oneOfRule && oneOfRule.oneOf) {
69
124
  // Add to beginning so it runs before SWC
70
125
  oneOfRule.oneOf.unshift(keakRule);
126
+ console.log('[Keak Plugin] ✅ Injected loader into webpack oneOf rules');
71
127
  } else {
72
128
  // Fallback: Add as top-level rule with enforce: 'pre'
73
129
  config.module.rules.unshift(keakRule);
130
+ console.log('[Keak Plugin] ✅ Injected loader as top-level webpack rule');
74
131
  }
75
132
 
76
133
  // Call original webpack config if it exists
@@ -1,109 +1,187 @@
1
1
  /**
2
2
  * Webpack loader that injects Keak source attributes into JSX
3
3
  *
4
- * Uses regex-based injection instead of full Babel AST transformation
5
- * to avoid interfering with React's module resolution in Next.js 15
4
+ * Uses Babel AST transformation to safely inject data-keak-src attributes
5
+ * into JSX elements without corrupting TypeScript generics or other syntax.
6
6
  *
7
7
  * Only runs in development mode
8
8
  */
9
9
 
10
10
  const path = require('path');
11
11
 
12
+ /**
13
+ * Create the Babel plugin for injecting source attributes
14
+ */
15
+ function createKeakSourcePlugin(sourceFile) {
16
+ return function keakSourcePlugin({ types: t }) {
17
+ return {
18
+ name: 'keak-source-injector',
19
+ visitor: {
20
+ JSXOpeningElement(path) {
21
+ // Skip if already has data-keak-src attribute
22
+ const hasKeakAttr = path.node.attributes.some(
23
+ attr => attr.type === 'JSXAttribute' &&
24
+ attr.name &&
25
+ attr.name.name &&
26
+ attr.name.name.toString().startsWith('data-keak-')
27
+ );
28
+
29
+ if (hasKeakAttr) {
30
+ return;
31
+ }
32
+
33
+ // Get source location from Babel's AST
34
+ const loc = path.node.loc;
35
+ if (!loc || !loc.start) {
36
+ return;
37
+ }
38
+
39
+ const lineNumber = loc.start.line;
40
+ const columnNumber = loc.start.column + 1; // Babel columns are 0-indexed
41
+
42
+ // Create the data-keak-src attribute value
43
+ const srcValue = `${sourceFile}:${lineNumber}:${columnNumber}`;
44
+
45
+ // Add the attribute to the JSX element
46
+ path.node.attributes.push(
47
+ t.jsxAttribute(
48
+ t.jsxIdentifier('data-keak-src'),
49
+ t.stringLiteral(srcValue)
50
+ )
51
+ );
52
+ }
53
+ }
54
+ };
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Extract a relative source file path from an absolute path
60
+ */
61
+ function getRelativeSourcePath(absolutePath, projectRoot) {
62
+ // Try to extract from common directory structures
63
+ const srcParts = absolutePath.split('/src/');
64
+ if (srcParts.length > 1) {
65
+ return 'src/' + srcParts[srcParts.length - 1];
66
+ }
67
+
68
+ const appParts = absolutePath.split('/app/');
69
+ if (appParts.length > 1) {
70
+ return 'app/' + appParts[appParts.length - 1];
71
+ }
72
+
73
+ const compParts = absolutePath.split('/components/');
74
+ if (compParts.length > 1) {
75
+ return 'components/' + compParts[compParts.length - 1];
76
+ }
77
+
78
+ const pagesParts = absolutePath.split('/pages/');
79
+ if (pagesParts.length > 1) {
80
+ return 'pages/' + pagesParts[pagesParts.length - 1];
81
+ }
82
+
83
+ // Fallback: use path relative to project root or cwd
84
+ const root = projectRoot || process.cwd();
85
+ return path.relative(root, absolutePath);
86
+ }
87
+
12
88
  module.exports = function keakSourceLoader(source) {
89
+ // Mark as cacheable
13
90
  this.cacheable(true);
14
-
91
+
15
92
  const resourcePath = this.resourcePath;
16
93
  const options = this.getOptions() || {};
17
-
94
+
18
95
  // Skip in production
19
96
  if (process.env.NODE_ENV === 'production' && !options.forceProduction) {
20
97
  return source;
21
98
  }
22
-
23
- // Skip if not a JSX/TSX file
99
+
100
+ // Only process JSX/TSX files
24
101
  if (!/\.(jsx|tsx)$/.test(resourcePath)) {
25
102
  return source;
26
103
  }
27
-
104
+
28
105
  // Skip node_modules
29
106
  if (resourcePath.includes('node_modules')) {
30
107
  return source;
31
108
  }
32
-
109
+
33
110
  // Skip files that use next/font (known to conflict with Babel processing)
34
111
  if (source.includes('next/font') || source.includes('@next/font')) {
35
112
  return source;
36
113
  }
37
-
114
+
38
115
  // Skip if already processed
39
116
  if (source.includes('data-keak-src=')) {
40
117
  return source;
41
118
  }
42
-
119
+
120
+ // Skip files with no JSX (quick check to avoid unnecessary Babel parsing)
121
+ if (!source.includes('<') || !source.includes('>')) {
122
+ return source;
123
+ }
124
+
43
125
  try {
44
- // Smart relative path detection
45
- let sourceFile;
46
- const srcParts = resourcePath.split('/src/');
47
- if (srcParts.length > 1) {
48
- sourceFile = 'src/' + srcParts[srcParts.length - 1];
49
- } else {
50
- const appParts = resourcePath.split('/app/');
51
- if (appParts.length > 1) {
52
- sourceFile = 'app/' + appParts[appParts.length - 1];
53
- } else {
54
- const compParts = resourcePath.split('/components/');
55
- if (compParts.length > 1) {
56
- sourceFile = 'components/' + compParts[compParts.length - 1];
57
- } else {
58
- sourceFile = path.relative(process.cwd(), resourcePath);
59
- }
126
+ // Try to require @babel/core - it should be available via @keak/sdk dependencies
127
+ let babel;
128
+ try {
129
+ babel = require('@babel/core');
130
+ } catch (e) {
131
+ // Babel not available, return original source
132
+ console.warn('[keak-loader] @babel/core not found, skipping transformation');
133
+ return source;
134
+ }
135
+
136
+ // Get relative source path for the attribute
137
+ const sourceFile = getRelativeSourcePath(resourcePath, options.projectRoot);
138
+
139
+ // Determine if this is TypeScript
140
+ const isTypeScript = /\.tsx?$/.test(resourcePath);
141
+
142
+ // Configure Babel presets based on file type
143
+ const presets = [];
144
+ if (isTypeScript) {
145
+ try {
146
+ require.resolve('@babel/preset-typescript');
147
+ presets.push(['@babel/preset-typescript', { isTSX: true, allExtensions: true }]);
148
+ } catch (e) {
149
+ // TypeScript preset not available, try parsing as JavaScript
60
150
  }
61
151
  }
62
-
63
- // Track the current line number
64
- const lines = source.split('\n');
65
- const processedLines = [];
66
152
 
67
- for (let i = 0; i < lines.length; i++) {
68
- const line = lines[i];
69
- const lineNumber = i + 1;
70
-
71
- // Match JSX opening tags: <ComponentName or <div, etc.
72
- // But not self-closing tags that are already complete, or fragments
73
- const processedLine = line.replace(
74
- // Match: <TagName followed by space, newline, or >
75
- // But NOT: </tag, <>, <Fragment
76
- /<([A-Z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9-]*)(\s|>)/g,
77
- (match, tagName, after) => {
78
- // Skip fragments and closing tags
79
- if (tagName === 'Fragment' || tagName === '') {
80
- return match;
81
- }
82
-
83
- // Skip if this looks like it already has data-keak attributes
84
- if (line.includes('data-keak-')) {
85
- return match;
86
- }
87
-
88
- // Inject the attribute
89
- const attr = ` data-keak-src="${sourceFile}:${lineNumber}:1"`;
90
-
91
- if (after === '>') {
92
- // <tag> -> <tag data-keak-src="...">
93
- return `<${tagName}${attr}>`;
94
- } else {
95
- // <tag -> <tag data-keak-src="..."
96
- return `<${tagName}${attr}${after}`;
97
- }
98
- }
99
- );
100
-
101
- processedLines.push(processedLine);
153
+ // Always include React preset for JSX
154
+ try {
155
+ require.resolve('@babel/preset-react');
156
+ presets.push(['@babel/preset-react', { runtime: 'automatic' }]);
157
+ } catch (e) {
158
+ // React preset not available
102
159
  }
103
-
104
- return processedLines.join('\n');
160
+
161
+ // Transform the source using Babel
162
+ const result = babel.transformSync(source, {
163
+ filename: resourcePath,
164
+ presets: presets,
165
+ plugins: [createKeakSourcePlugin(sourceFile)],
166
+ // Important: preserve the original code structure
167
+ retainLines: true,
168
+ // Don't generate source maps (webpack handles this)
169
+ sourceMaps: false,
170
+ // Parse as module
171
+ sourceType: 'module',
172
+ // Don't run other config files
173
+ configFile: false,
174
+ babelrc: false,
175
+ });
176
+
177
+ if (result && result.code) {
178
+ return result.code;
179
+ }
180
+
181
+ // If transformation failed, return original
182
+ return source;
105
183
  } catch (error) {
106
- // Return original source on error
184
+ // Log error but don't fail the build - return original source
107
185
  console.error(`[keak-loader] Error processing ${resourcePath}:`, error.message);
108
186
  return source;
109
187
  }