@rahul_ur/devlink-bridge 1.0.0
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/dist/bridge.d.ts +78 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +361 -0
- package/dist/bridge.js.map +1 -0
- package/dist/cache.d.ts +23 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +92 -0
- package/dist/cache.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist-cjs/bridge.js +364 -0
- package/dist-cjs/cache.js +95 -0
- package/dist-cjs/index.js +7 -0
- package/dist-cjs/types.js +3 -0
- package/package.json +77 -0
- package/server.mjs +234 -0
- package/setup-devlink.mjs +388 -0
package/server.mjs
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { watch } from 'fs';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
|
|
7
|
+
const PORT = 7100;
|
|
8
|
+
const WORKSPACE = process.cwd();
|
|
9
|
+
const watchers = new Map();
|
|
10
|
+
|
|
11
|
+
// ─── Security: resolve path inside workspace ──────────────────────────────────
|
|
12
|
+
function safeResolvePath(filePath) {
|
|
13
|
+
const resolved = path.resolve(filePath);
|
|
14
|
+
// Allow absolute paths anywhere on the machine (needed for cross-project use)
|
|
15
|
+
// but log them for awareness
|
|
16
|
+
return resolved;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Ignored directories for readDir ─────────────────────────────────────────
|
|
20
|
+
const IGNORE = new Set([
|
|
21
|
+
'node_modules', '.git', 'dist', 'build', '.next',
|
|
22
|
+
'coverage', '.DS_Store', 'thumbs.db', '.turbo',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
async function readDir(dirPath, recursive) {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = [];
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (IGNORE.has(entry.name)) continue;
|
|
36
|
+
const fullPath = path.join(dirPath, entry.name).replace(/\\/g, '/');
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
result.push({
|
|
39
|
+
name: entry.name,
|
|
40
|
+
path: fullPath,
|
|
41
|
+
type: 'directory',
|
|
42
|
+
children: recursive ? await readDir(path.join(dirPath, entry.name), true) : undefined,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
result.push({ name: entry.name, path: fullPath, type: 'file' });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result.sort((a, b) => {
|
|
49
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
50
|
+
return a.name.localeCompare(b.name);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── WebSocket server ─────────────────────────────────────────────────────────
|
|
55
|
+
const wss = new WebSocketServer({ host: '127.0.0.1', port: PORT });
|
|
56
|
+
|
|
57
|
+
wss.on('connection', (ws) => {
|
|
58
|
+
console.log('[devlink] client connected');
|
|
59
|
+
|
|
60
|
+
ws.on('message', async (raw) => {
|
|
61
|
+
let msg;
|
|
62
|
+
try {
|
|
63
|
+
msg = JSON.parse(raw.toString());
|
|
64
|
+
} catch {
|
|
65
|
+
console.error('[devlink] invalid JSON message');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (msg.type) {
|
|
70
|
+
|
|
71
|
+
// ── Handshake ────────────────────────────────────────────────────────────
|
|
72
|
+
case 'handshake': {
|
|
73
|
+
ws.send(JSON.stringify({
|
|
74
|
+
type: 'handshakeAck',
|
|
75
|
+
protocolVersion: '1.0',
|
|
76
|
+
workspaceRoot: WORKSPACE.replace(/\\/g, '/'),
|
|
77
|
+
}));
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Read file ────────────────────────────────────────────────────────────
|
|
82
|
+
case 'readFile': {
|
|
83
|
+
try {
|
|
84
|
+
const resolved = safeResolvePath(msg.path);
|
|
85
|
+
const content = await fs.readFile(resolved, 'utf-8');
|
|
86
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: content }));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Write file ───────────────────────────────────────────────────────────
|
|
94
|
+
case 'writeFile': {
|
|
95
|
+
try {
|
|
96
|
+
const resolved = safeResolvePath(msg.path);
|
|
97
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
98
|
+
await fs.writeFile(resolved, msg.content, 'utf-8');
|
|
99
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: null }));
|
|
100
|
+
// Push back with writeId so browser can dedup its own save
|
|
101
|
+
ws.send(JSON.stringify({
|
|
102
|
+
type: 'fileChanged',
|
|
103
|
+
path: msg.path,
|
|
104
|
+
content: msg.content,
|
|
105
|
+
writeId: msg.writeId,
|
|
106
|
+
}));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Read directory ───────────────────────────────────────────────────────
|
|
114
|
+
case 'readDir': {
|
|
115
|
+
try {
|
|
116
|
+
const resolved = safeResolvePath(msg.path);
|
|
117
|
+
const entries = await readDir(resolved, msg.recursive ?? false);
|
|
118
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: entries }));
|
|
119
|
+
} catch (e) {
|
|
120
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Watch file ───────────────────────────────────────────────────────────
|
|
126
|
+
case 'watchFile': {
|
|
127
|
+
const filePath = msg.path;
|
|
128
|
+
if (!watchers.has(filePath)) {
|
|
129
|
+
try {
|
|
130
|
+
const watcher = watch(filePath, async () => {
|
|
131
|
+
try {
|
|
132
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
133
|
+
if (ws.readyState === ws.OPEN) {
|
|
134
|
+
ws.send(JSON.stringify({ type: 'fileChanged', path: filePath, content }));
|
|
135
|
+
}
|
|
136
|
+
} catch { /* file deleted */ }
|
|
137
|
+
});
|
|
138
|
+
watchers.set(filePath, watcher);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: null }));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Unwatch file ─────────────────────────────────────────────────────────
|
|
149
|
+
case 'unwatchFile': {
|
|
150
|
+
const watcher = watchers.get(msg.path);
|
|
151
|
+
if (watcher) {
|
|
152
|
+
watcher.close();
|
|
153
|
+
watchers.delete(msg.path);
|
|
154
|
+
}
|
|
155
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: null }));
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Execute shell command ─────────────────────────────────────────────────
|
|
160
|
+
// Used by terminal tab and AI codex commands
|
|
161
|
+
case 'execCommand': {
|
|
162
|
+
const { command, cwd } = msg;
|
|
163
|
+
|
|
164
|
+
// Safety: block obviously destructive commands
|
|
165
|
+
const blocked = ['rm -rf /', 'format c:', 'del /f /s /q c:\\'];
|
|
166
|
+
const lower = (command || '').toLowerCase();
|
|
167
|
+
if (blocked.some(b => lower.includes(b))) {
|
|
168
|
+
ws.send(JSON.stringify({
|
|
169
|
+
type: 'response', id: msg.id, ok: true,
|
|
170
|
+
result: { stdout: '', stderr: 'Command blocked by devlink safety rules.', code: 1 },
|
|
171
|
+
}));
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const resolvedCwd = cwd ? safeResolvePath(cwd) : WORKSPACE;
|
|
176
|
+
|
|
177
|
+
exec(command, { cwd: resolvedCwd, timeout: 60000 }, (err, stdout, stderr) => {
|
|
178
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
179
|
+
ws.send(JSON.stringify({
|
|
180
|
+
type: 'response', id: msg.id, ok: true,
|
|
181
|
+
result: {
|
|
182
|
+
stdout: stdout || '',
|
|
183
|
+
stderr: stderr || '',
|
|
184
|
+
error: err?.message || null,
|
|
185
|
+
code: err?.code ?? 0,
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── AI autocomplete ───────────────────────────────────────────────────────
|
|
193
|
+
// Stub — returns empty so Monaco doesn't error.
|
|
194
|
+
// Replace with your model adapter call here.
|
|
195
|
+
case 'ai-autocomplete': {
|
|
196
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: '' }));
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Ping / pong ───────────────────────────────────────────────────────────
|
|
201
|
+
case 'ping': {
|
|
202
|
+
ws.send(JSON.stringify({ type: 'pong', id: msg.id }));
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
default: {
|
|
207
|
+
console.warn('[devlink] unknown message type:', msg.type);
|
|
208
|
+
ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: `Unknown type: ${msg.type}` }));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
ws.on('close', () => {
|
|
214
|
+
console.log('[devlink] client disconnected');
|
|
215
|
+
// Clean up watchers for this client
|
|
216
|
+
for (const [filePath, watcher] of watchers) {
|
|
217
|
+
watcher.close();
|
|
218
|
+
watchers.delete(filePath);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
ws.on('error', (e) => {
|
|
223
|
+
console.error('[devlink] client error:', e.message);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
wss.on('error', (e) => {
|
|
228
|
+
console.error('[devlink] server error:', e.message);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
console.log(`[devlink] bridge running on ws://127.0.0.1:${PORT}`);
|
|
232
|
+
console.log(`[devlink] workspace: ${WORKSPACE}`);
|
|
233
|
+
console.log(`[devlink] execCommand handler: enabled`);
|
|
234
|
+
console.log(`[devlink] ai-autocomplete handler: stub (replace with model adapter)`);
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const bridgePackageRoot = path.dirname(__filename);
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
10
|
+
|
|
11
|
+
const log = (msg) => console.log(`[devlink setup] ${msg}`);
|
|
12
|
+
const warn = (msg) => console.warn(`[devlink setup] ⚠ ${msg}`);
|
|
13
|
+
const ok = (msg) => console.log(`[devlink setup] ✓ ${msg}`);
|
|
14
|
+
|
|
15
|
+
function readJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
16
|
+
function writeJson(p,v) { fs.writeFileSync(p, `${JSON.stringify(v, null, 2)}\n`); }
|
|
17
|
+
function exists(p) { return fs.existsSync(p); }
|
|
18
|
+
|
|
19
|
+
function maybeRelativeDep(targetPath) {
|
|
20
|
+
if (!exists(targetPath)) return null;
|
|
21
|
+
return `file:${path.relative(projectRoot, targetPath).replace(/\\/g, '/')}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Detect framework ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function detectFramework(pkg) {
|
|
27
|
+
const allDeps = {
|
|
28
|
+
...pkg.dependencies,
|
|
29
|
+
...pkg.devDependencies,
|
|
30
|
+
};
|
|
31
|
+
if (allDeps['react-scripts']) return 'cra';
|
|
32
|
+
if (allDeps['next']) return 'next';
|
|
33
|
+
if (allDeps['vite']) return 'vite';
|
|
34
|
+
if (allDeps['@vitejs/plugin-react']) return 'vite';
|
|
35
|
+
return 'unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Update package.json ──────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function updatePackageJson() {
|
|
41
|
+
if (!exists(packageJsonPath)) {
|
|
42
|
+
throw new Error(`package.json not found in ${projectRoot}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pkg = readJson(packageJsonPath);
|
|
46
|
+
pkg.scripts = pkg.scripts || {};
|
|
47
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
48
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
49
|
+
|
|
50
|
+
let changed = false;
|
|
51
|
+
|
|
52
|
+
// Add bridge scripts
|
|
53
|
+
if (!pkg.scripts['devlink:bridge']) {
|
|
54
|
+
pkg.scripts['devlink:bridge'] = 'node node_modules/devlink-bridge/server.mjs';
|
|
55
|
+
changed = true; log('added script devlink:bridge');
|
|
56
|
+
}
|
|
57
|
+
if (!pkg.scripts['devlink:setup']) {
|
|
58
|
+
pkg.scripts['devlink:setup'] = 'node node_modules/devlink-bridge/setup-devlink.mjs';
|
|
59
|
+
changed = true; log('added script devlink:setup');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const alphaRoot = path.resolve(bridgePackageRoot, '..');
|
|
63
|
+
const deps = {
|
|
64
|
+
'devlink-bridge': maybeRelativeDep(bridgePackageRoot),
|
|
65
|
+
'devlink-studio': maybeRelativeDep(path.join(alphaRoot, 'devlink-v2', 'packages', 'devlink-studio')),
|
|
66
|
+
'devlink-babel-plugin': maybeRelativeDep(path.join(alphaRoot, 'devlink-babel-plugin')),
|
|
67
|
+
'devlink-vite-plugin': maybeRelativeDep(path.join(alphaRoot, 'devlink-vite-plugin')),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (!pkg.dependencies['devlink-bridge'] && deps['devlink-bridge']) {
|
|
71
|
+
pkg.dependencies['devlink-bridge'] = deps['devlink-bridge'];
|
|
72
|
+
changed = true; log('added dependency devlink-bridge');
|
|
73
|
+
}
|
|
74
|
+
if (!pkg.dependencies['devlink-studio'] && deps['devlink-studio']) {
|
|
75
|
+
pkg.dependencies['devlink-studio'] = deps['devlink-studio'];
|
|
76
|
+
changed = true; log('added dependency devlink-studio');
|
|
77
|
+
}
|
|
78
|
+
if (!pkg.devDependencies['devlink-babel-plugin'] && deps['devlink-babel-plugin']) {
|
|
79
|
+
pkg.devDependencies['devlink-babel-plugin'] = deps['devlink-babel-plugin'];
|
|
80
|
+
changed = true; log('added devDependency devlink-babel-plugin');
|
|
81
|
+
}
|
|
82
|
+
if (!pkg.devDependencies['devlink-vite-plugin'] && deps['devlink-vite-plugin']) {
|
|
83
|
+
pkg.devDependencies['devlink-vite-plugin'] = deps['devlink-vite-plugin'];
|
|
84
|
+
changed = true; log('added devDependency devlink-vite-plugin');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (changed) writeJson(packageJsonPath, pkg);
|
|
88
|
+
else log('package.json already up to date');
|
|
89
|
+
|
|
90
|
+
return pkg;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── CRA setup ───────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function setupCra() {
|
|
96
|
+
const configPath = path.join(projectRoot, 'node_modules', 'react-scripts', 'config', 'webpack.config.js');
|
|
97
|
+
if (!exists(configPath)) {
|
|
98
|
+
warn('react-scripts webpack config not found. Run npm install first, then npm run devlink:setup again.');
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let src = fs.readFileSync(configPath, 'utf8');
|
|
103
|
+
let changed = false;
|
|
104
|
+
|
|
105
|
+
if (!src.includes("require.resolve('devlink-babel-plugin')")) {
|
|
106
|
+
const marker = "const createEnvironmentHash = require('./webpack/persistentCache/createEnvironmentHash');";
|
|
107
|
+
if (src.includes(marker)) {
|
|
108
|
+
src = src.replace(marker, `${marker}\nconst devlinkBabelPlugin = require.resolve('devlink-babel-plugin');`);
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!src.includes('devlinkBabelPlugin,')) {
|
|
114
|
+
const reactRefreshLine = " isEnvDevelopment &&\n shouldUseReactRefresh &&\n require.resolve('react-refresh/babel'),";
|
|
115
|
+
if (src.includes(reactRefreshLine)) {
|
|
116
|
+
const block =
|
|
117
|
+
" isEnvDevelopment && [\n" +
|
|
118
|
+
" devlinkBabelPlugin,\n" +
|
|
119
|
+
" { root: paths.appPath, envs: ['development'] },\n" +
|
|
120
|
+
" ],\n";
|
|
121
|
+
src = src.replace(reactRefreshLine, `${block}${reactRefreshLine}`);
|
|
122
|
+
changed = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (changed) { fs.writeFileSync(configPath, src); ok('patched CRA webpack config'); }
|
|
127
|
+
else { ok('CRA webpack config already patched'); }
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Vite setup ───────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function setupVite() {
|
|
134
|
+
// Find vite config — could be .ts, .js, .mts, .mjs
|
|
135
|
+
const candidates = [
|
|
136
|
+
'vite.config.ts', 'vite.config.js',
|
|
137
|
+
'vite.config.mts', 'vite.config.mjs',
|
|
138
|
+
];
|
|
139
|
+
const configFile = candidates.find(f => exists(path.join(projectRoot, f)));
|
|
140
|
+
|
|
141
|
+
if (!configFile) {
|
|
142
|
+
warn('No vite.config file found. Creating vite.config.ts with devlink config...');
|
|
143
|
+
createViteConfig('vite.config.ts');
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const configPath = path.join(projectRoot, configFile);
|
|
148
|
+
let src = fs.readFileSync(configPath, 'utf8');
|
|
149
|
+
|
|
150
|
+
if (src.includes('devlink-babel-plugin') || src.includes('devlink-vite-plugin')) {
|
|
151
|
+
ok('vite.config already has devlink config');
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Patch existing vite config
|
|
156
|
+
const isTs = configFile.endsWith('.ts') || configFile.endsWith('.mts');
|
|
157
|
+
|
|
158
|
+
// Add import at top
|
|
159
|
+
const importLine = `import { devlinkBabelConfig } from 'devlink-vite-plugin';\n`;
|
|
160
|
+
if (!src.includes("from 'devlink-vite-plugin'")) {
|
|
161
|
+
// Insert after last existing import
|
|
162
|
+
const lastImportIdx = src.lastIndexOf('\nimport ');
|
|
163
|
+
const insertAt = lastImportIdx === -1 ? 0 : src.indexOf('\n', lastImportIdx) + 1;
|
|
164
|
+
src = src.slice(0, insertAt) + importLine + src.slice(insertAt);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Patch react() plugin call to add babel config
|
|
168
|
+
// Handles: react() and react({ ... })
|
|
169
|
+
if (src.includes('react()')) {
|
|
170
|
+
src = src.replace(
|
|
171
|
+
'react()',
|
|
172
|
+
`react({\n babel: {\n plugins: [devlinkBabelConfig({ root: process.cwd() })].filter(Boolean),\n },\n })`
|
|
173
|
+
);
|
|
174
|
+
ok('patched react() in vite.config');
|
|
175
|
+
} else if (src.match(/react\(\s*\{/)) {
|
|
176
|
+
// react({ already has options — add babel inside
|
|
177
|
+
src = src.replace(
|
|
178
|
+
/react\(\s*\{/,
|
|
179
|
+
`react({\n babel: {\n plugins: [devlinkBabelConfig({ root: process.cwd() })].filter(Boolean),\n },\n `
|
|
180
|
+
);
|
|
181
|
+
ok('patched react({ in vite.config');
|
|
182
|
+
} else {
|
|
183
|
+
// Can't auto-patch safely — append comment instructions
|
|
184
|
+
src += `\n// TODO: add devlink babel plugin to your react() config:\n`;
|
|
185
|
+
src += `// import { devlinkBabelConfig } from 'devlink-vite-plugin'\n`;
|
|
186
|
+
src += `// react({ babel: { plugins: [devlinkBabelConfig({ root: process.cwd() })] } })\n`;
|
|
187
|
+
warn('Could not auto-patch vite.config. See TODO comment added at bottom of file.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fs.writeFileSync(configPath, src);
|
|
191
|
+
ok(`patched ${configFile}`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createViteConfig(filename) {
|
|
196
|
+
const content = `import { defineConfig } from 'vite';
|
|
197
|
+
import react from '@vitejs/plugin-react';
|
|
198
|
+
import { devlinkBabelConfig } from 'devlink-vite-plugin';
|
|
199
|
+
|
|
200
|
+
export default defineConfig({
|
|
201
|
+
plugins: [
|
|
202
|
+
react({
|
|
203
|
+
babel: {
|
|
204
|
+
plugins: [
|
|
205
|
+
devlinkBabelConfig({ root: process.cwd() }),
|
|
206
|
+
].filter(Boolean),
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
`;
|
|
212
|
+
fs.writeFileSync(path.join(projectRoot, filename), content);
|
|
213
|
+
ok(`created ${filename} with devlink config`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Next.js setup ────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
function setupNext() {
|
|
219
|
+
const candidates = ['next.config.ts', 'next.config.js', 'next.config.mjs'];
|
|
220
|
+
const configFile = candidates.find(f => exists(path.join(projectRoot, f)));
|
|
221
|
+
|
|
222
|
+
if (!configFile) {
|
|
223
|
+
warn('No next.config file found. Creating next.config.js with devlink config...');
|
|
224
|
+
createNextConfig('next.config.js');
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const configPath = path.join(projectRoot, configFile);
|
|
229
|
+
let src = fs.readFileSync(configPath, 'utf8');
|
|
230
|
+
|
|
231
|
+
if (src.includes('devlink-babel-plugin')) {
|
|
232
|
+
ok('next.config already has devlink config');
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Next.js uses .babelrc or babel.config.js to add babel plugins
|
|
237
|
+
// We patch/create .babelrc
|
|
238
|
+
const babelRcPath = path.join(projectRoot, '.babelrc');
|
|
239
|
+
if (exists(babelRcPath)) {
|
|
240
|
+
const babelRc = readJson(babelRcPath);
|
|
241
|
+
const devEnv = babelRc.env?.development || {};
|
|
242
|
+
devEnv.plugins = devEnv.plugins || [];
|
|
243
|
+
if (!devEnv.plugins.some(p => (Array.isArray(p) ? p[0] : p) === 'devlink-babel-plugin')) {
|
|
244
|
+
devEnv.plugins.push(['devlink-babel-plugin', { root: '.', envs: ['development'] }]);
|
|
245
|
+
babelRc.env = babelRc.env || {};
|
|
246
|
+
babelRc.env.development = devEnv;
|
|
247
|
+
writeJson(babelRcPath, babelRc);
|
|
248
|
+
ok('patched .babelrc for Next.js');
|
|
249
|
+
} else {
|
|
250
|
+
ok('.babelrc already has devlink config');
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Create .babelrc
|
|
254
|
+
const babelRc = {
|
|
255
|
+
presets: ['next/babel'],
|
|
256
|
+
env: {
|
|
257
|
+
development: {
|
|
258
|
+
plugins: [
|
|
259
|
+
['devlink-babel-plugin', { root: '.', envs: ['development'] }]
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
writeJson(babelRcPath, babelRc);
|
|
265
|
+
ok('created .babelrc for Next.js with devlink config');
|
|
266
|
+
warn('Adding .babelrc disables Next.js SWC compiler. Only use in development.');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createNextConfig(filename) {
|
|
273
|
+
const content = `/** @type {import('next').NextConfig} */
|
|
274
|
+
const nextConfig = {};
|
|
275
|
+
module.exports = nextConfig;
|
|
276
|
+
`;
|
|
277
|
+
fs.writeFileSync(path.join(projectRoot, filename), content);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Generate devlink.config.ts in user's project ─────────────────────────────
|
|
281
|
+
|
|
282
|
+
function generateDevlinkConfig(framework) {
|
|
283
|
+
const configPath = path.join(projectRoot, 'devlink.config.ts');
|
|
284
|
+
if (exists(configPath)) { ok('devlink.config.ts already exists'); return; }
|
|
285
|
+
|
|
286
|
+
// Try to auto-detect project src folder
|
|
287
|
+
const srcDir = exists(path.join(projectRoot, 'src')) ? './src' : '.';
|
|
288
|
+
const winRoot = projectRoot.replace(/\\/g, '/');
|
|
289
|
+
|
|
290
|
+
const content = `// devlink.config.ts
|
|
291
|
+
// Auto-generated by devlink setup. Edit projectRoot and rootPath for your project.
|
|
292
|
+
|
|
293
|
+
export const devlinkConfig = {
|
|
294
|
+
/** Absolute path to your project root */
|
|
295
|
+
projectRoot: '${winRoot}',
|
|
296
|
+
|
|
297
|
+
/** Path shown in the file tree — usually projectRoot/src */
|
|
298
|
+
rootPath: '${winRoot}/${srcDir.replace('./', '')}',
|
|
299
|
+
|
|
300
|
+
/** Start editor docked to the right side */
|
|
301
|
+
defaultDocked: true,
|
|
302
|
+
|
|
303
|
+
/** Width of the docked editor panel in px */
|
|
304
|
+
dockedWidth: 460,
|
|
305
|
+
};
|
|
306
|
+
`;
|
|
307
|
+
fs.writeFileSync(configPath, content);
|
|
308
|
+
ok('created devlink.config.ts — edit projectRoot if needed');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── Generate main devlink entry patch instructions ───────────────────────────
|
|
312
|
+
|
|
313
|
+
function printMainInstructions(framework) {
|
|
314
|
+
console.log('');
|
|
315
|
+
console.log('─────────────────────────────────────────────────────');
|
|
316
|
+
console.log('[devlink setup] Final step — update your entry file:');
|
|
317
|
+
console.log('─────────────────────────────────────────────────────');
|
|
318
|
+
console.log('');
|
|
319
|
+
console.log('In your main.tsx / main.jsx / _app.tsx:');
|
|
320
|
+
console.log('');
|
|
321
|
+
if (framework === 'next') {
|
|
322
|
+
console.log(` // pages/_app.tsx
|
|
323
|
+
import { DevlinkBridge } from 'devlink-bridge'
|
|
324
|
+
import { DevlinkStudio } from 'devlink-studio'
|
|
325
|
+
import devlinkConfig from '../devlink.config'
|
|
326
|
+
|
|
327
|
+
const bridge = new DevlinkBridge()
|
|
328
|
+
|
|
329
|
+
export default function App({ Component, pageProps }) {
|
|
330
|
+
return (
|
|
331
|
+
<DevlinkStudio bridge={bridge} {...devlinkConfig}>
|
|
332
|
+
<Component {...pageProps} />
|
|
333
|
+
</DevlinkStudio>
|
|
334
|
+
)
|
|
335
|
+
}`);
|
|
336
|
+
} else {
|
|
337
|
+
console.log(` import { DevlinkBridge } from 'devlink-bridge'
|
|
338
|
+
import { DevlinkStudio } from 'devlink-studio'
|
|
339
|
+
import { devlinkConfig } from '../devlink.config'
|
|
340
|
+
|
|
341
|
+
const bridge = new DevlinkBridge()
|
|
342
|
+
|
|
343
|
+
function Root() {
|
|
344
|
+
return (
|
|
345
|
+
<DevlinkStudio bridge={bridge} {...devlinkConfig}>
|
|
346
|
+
<App />
|
|
347
|
+
</DevlinkStudio>
|
|
348
|
+
)
|
|
349
|
+
}`);
|
|
350
|
+
}
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log('Then start the bridge server in a separate terminal:');
|
|
353
|
+
console.log(' npm run devlink:bridge');
|
|
354
|
+
console.log('');
|
|
355
|
+
console.log('─────────────────────────────────────────────────────');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
function main() {
|
|
361
|
+
log(`configuring ${projectRoot}`);
|
|
362
|
+
|
|
363
|
+
const pkg = updatePackageJson();
|
|
364
|
+
const framework = detectFramework(pkg);
|
|
365
|
+
log(`detected framework: ${framework}`);
|
|
366
|
+
|
|
367
|
+
let configured = false;
|
|
368
|
+
if (framework === 'cra') configured = setupCra();
|
|
369
|
+
else if (framework === 'vite') configured = setupVite();
|
|
370
|
+
else if (framework === 'next') configured = setupNext();
|
|
371
|
+
else {
|
|
372
|
+
warn('Could not detect framework (CRA / Vite / Next.js).');
|
|
373
|
+
warn('Add devlink-babel-plugin manually to your build config.');
|
|
374
|
+
warn('See: https://github.com/your-org/devlink#manual-setup');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
generateDevlinkConfig(framework);
|
|
378
|
+
printMainInstructions(framework);
|
|
379
|
+
|
|
380
|
+
ok('setup complete');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
main();
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.error(`[devlink setup] failed: ${err.message}`);
|
|
387
|
+
process.exitCode = 1;
|
|
388
|
+
}
|