@keak/sdk 2.0.2 → 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.2",
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,35 +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
- const { dev } = options;
40
+ const { dev, isServer } = options;
41
+ const projectRoot = process.cwd();
42
+
43
+ console.log('[Keak Plugin] webpack called - dev:', dev, 'isServer:', isServer);
18
44
 
19
45
  // Only inject loader in development mode
20
46
  if (!dev) {
21
- // Call original webpack config if it exists
47
+ console.log('[Keak Plugin] Skipping - not in development mode');
22
48
  if (typeof nextConfig.webpack === 'function') {
23
49
  return nextConfig.webpack(config, options);
24
50
  }
25
51
  return config;
26
52
  }
27
53
 
28
- // Resolve loader path from project .keak directory or fallback
29
- const projectKeakLoader = path.resolve(process.cwd(), '.keak/webpack-loader-babel/index.js');
30
- const keakLoaderPath = fs.existsSync(projectKeakLoader)
31
- ? projectKeakLoader
32
- : 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;
73
+
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
+ }
33
91
 
34
92
  // Ensure config.module.rules exists
35
93
  if (!config.module || !config.module.rules) {
@@ -38,20 +96,23 @@ function withKeak(nextConfig = {}) {
38
96
  }
39
97
 
40
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
41
101
  const keakRule = {
42
- test: /\.[jt]sx?$/,
102
+ test: /\.(jsx|tsx)$/, // Only JSX/TSX files (not .js/.ts)
43
103
  exclude: [
44
104
  /node_modules/,
45
105
  /\.next\//,
46
106
  /webpack/,
47
- /next\/dist/
107
+ /next\/dist/,
108
+ /\.keak\//, // Don't process our own loader files
48
109
  ],
49
110
  enforce: 'pre', // Run BEFORE SWC compilation
50
111
  use: [
51
112
  {
52
113
  loader: keakLoaderPath,
53
114
  options: {
54
- projectRoot: process.cwd()
115
+ projectRoot: projectRoot
55
116
  }
56
117
  }
57
118
  ]
@@ -62,9 +123,11 @@ function withKeak(nextConfig = {}) {
62
123
  if (oneOfRule && oneOfRule.oneOf) {
63
124
  // Add to beginning so it runs before SWC
64
125
  oneOfRule.oneOf.unshift(keakRule);
126
+ console.log('[Keak Plugin] ✅ Injected loader into webpack oneOf rules');
65
127
  } else {
66
128
  // Fallback: Add as top-level rule with enforce: 'pre'
67
129
  config.module.rules.unshift(keakRule);
130
+ console.log('[Keak Plugin] ✅ Injected loader as top-level webpack rule');
68
131
  }
69
132
 
70
133
  // Call original webpack config if it exists
@@ -1,124 +1,187 @@
1
1
  /**
2
- * Webpack loader that uses Babel to inject Keak source attributes into JSX
3
- * Simplified version - runs BEFORE SWC compilation
2
+ * Webpack loader that injects Keak source attributes into JSX
3
+ *
4
+ * Uses Babel AST transformation to safely inject data-keak-src attributes
5
+ * into JSX elements without corrupting TypeScript generics or other syntax.
6
+ *
4
7
  * Only runs in development mode
5
8
  */
6
9
 
7
10
  const path = require('path');
8
- const babelParser = require('@babel/parser');
9
- const babelTraverse = require('@babel/traverse').default;
10
- const babelGenerator = require('@babel/generator').default;
11
- const t = require('@babel/types');
12
11
 
13
- module.exports = function keakLoaderBabel(source) {
14
- this.cacheable(true);
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
+ }
15
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
+
88
+ module.exports = function keakSourceLoader(source) {
89
+ // Mark as cacheable
90
+ this.cacheable(true);
91
+
16
92
  const resourcePath = this.resourcePath;
17
93
  const options = this.getOptions() || {};
18
-
94
+
19
95
  // Skip in production
20
96
  if (process.env.NODE_ENV === 'production' && !options.forceProduction) {
21
97
  return source;
22
98
  }
23
-
24
- // Skip if not a JSX/TSX file
25
- if (!/\.(jsx|tsx|js|ts)$/.test(resourcePath)) {
99
+
100
+ // Only process JSX/TSX files
101
+ if (!/\.(jsx|tsx)$/.test(resourcePath)) {
26
102
  return source;
27
103
  }
28
-
104
+
105
+ // Skip node_modules
106
+ if (resourcePath.includes('node_modules')) {
107
+ return source;
108
+ }
109
+
110
+ // Skip files that use next/font (known to conflict with Babel processing)
111
+ if (source.includes('next/font') || source.includes('@next/font')) {
112
+ return source;
113
+ }
114
+
115
+ // Skip if already processed
116
+ if (source.includes('data-keak-src=')) {
117
+ return source;
118
+ }
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
+
29
125
  try {
30
- // Parse the source code
31
- const ast = babelParser.parse(source, {
32
- sourceType: 'module',
33
- plugins: [
34
- 'jsx',
35
- 'typescript',
36
- 'decorators-legacy',
37
- 'classProperties',
38
- 'objectRestSpread',
39
- 'dynamicImport',
40
- 'nullishCoalescingOperator',
41
- 'optionalChaining'
42
- ]
43
- });
44
-
45
- let skipFile = false;
46
-
47
- // Check for next/font imports (Babel/SWC conflict)
48
- babelTraverse(ast, {
49
- ImportDeclaration(importPath) {
50
- const source = importPath.node.source.value;
51
- if (source.includes('next/font') || source.includes('@next/font')) {
52
- skipFile = true;
53
- importPath.stop();
54
- }
55
- }
56
- });
57
-
58
- if (skipFile) {
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');
59
133
  return source;
60
134
  }
61
-
62
- // Transform JSX elements
63
- babelTraverse(ast, {
64
- JSXOpeningElement(nodePath) {
65
- const node = nodePath.node;
66
-
67
- // Skip if already has keak attributes
68
- const hasKeakAttr = node.attributes.some(attr =>
69
- t.isJSXAttribute(attr) &&
70
- t.isJSXIdentifier(attr.name) &&
71
- (attr.name.name === 'data-keak-file' || attr.name.name === 'data-keak-src')
72
- );
73
-
74
- if (hasKeakAttr || !node.loc) {
75
- return;
76
- }
77
-
78
- // Smart relative path detection
79
- let sourceFile;
80
- const srcParts = resourcePath.split('/src/');
81
- if (srcParts.length > 1) {
82
- sourceFile = 'src/' + srcParts[srcParts.length - 1];
83
- } else {
84
- const appParts = resourcePath.split('/app/');
85
- if (appParts.length > 1) {
86
- sourceFile = 'app/' + appParts[appParts.length - 1];
87
- } else {
88
- const compParts = resourcePath.split('/components/');
89
- if (compParts.length > 1) {
90
- sourceFile = 'components/' + compParts[compParts.length - 1];
91
- } else {
92
- sourceFile = path.relative(process.cwd(), resourcePath);
93
- }
94
- }
95
- }
96
-
97
- const { line } = node.loc.start;
98
-
99
- // Add data-keak-file and data-keak-line attributes
100
- node.attributes.push(
101
- t.jsxAttribute(
102
- t.jsxIdentifier('data-keak-file'),
103
- t.stringLiteral(sourceFile)
104
- ),
105
- t.jsxAttribute(
106
- t.jsxIdentifier('data-keak-line'),
107
- t.stringLiteral(String(line))
108
- )
109
- );
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
110
150
  }
151
+ }
152
+
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
159
+ }
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,
111
175
  });
112
-
113
- // Generate transformed code
114
- const output = babelGenerator(ast, {
115
- retainLines: false,
116
- compact: false
117
- }, source);
118
-
119
- return output.code;
176
+
177
+ if (result && result.code) {
178
+ return result.code;
179
+ }
180
+
181
+ // If transformation failed, return original
182
+ return source;
120
183
  } catch (error) {
121
- // Return original source on error
184
+ // Log error but don't fail the build - return original source
122
185
  console.error(`[keak-loader] Error processing ${resourcePath}:`, error.message);
123
186
  return source;
124
187
  }