@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 +5 -1
- package/src/plugins/next.cjs +76 -13
- package/src/plugins/webpack-loader-babel/index.js +163 -100
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keak/sdk",
|
|
3
|
-
"version": "2.0.
|
|
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",
|
package/src/plugins/next.cjs
CHANGED
|
@@ -1,35 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keak Next.js Plugin
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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: /\.
|
|
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:
|
|
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
|
|
3
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
//
|
|
25
|
-
if (!/\.(jsx|tsx
|
|
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
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
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
|
}
|