@keak/sdk 2.0.3 → 2.0.5
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 +71 -14
- package/src/plugins/webpack-loader-babel/index.js +146 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keak/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
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,41 +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
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
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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: /\.
|
|
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:
|
|
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
|
|
5
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
}
|