draply-dev 1.2.0 → 1.2.1
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/bin/cli.js +11 -481
- package/package.json +1 -1
- package/src/draply-features.js +223 -775
- package/src/overlay.js +3 -15
package/bin/cli.js
CHANGED
|
@@ -35,12 +35,11 @@ function decode(headers, chunks) {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// Store CSS in hidden .draply/ folder to keep user's project clean
|
|
39
39
|
const projectRoot = process.cwd();
|
|
40
40
|
const draplyDir = path.join(projectRoot, '.draply');
|
|
41
41
|
if (!fs.existsSync(draplyDir)) {
|
|
42
42
|
fs.mkdirSync(draplyDir, { recursive: true });
|
|
43
|
-
// Добавляем .draply в .gitignore если он есть
|
|
44
43
|
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
45
44
|
if (fs.existsSync(gitignorePath)) {
|
|
46
45
|
const gi = fs.readFileSync(gitignorePath, 'utf8');
|
|
@@ -54,7 +53,6 @@ if (!fs.existsSync(overridesPath)) {
|
|
|
54
53
|
fs.writeFileSync(overridesPath, '/* draply */\n', 'utf8');
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
58
56
|
const server = http.createServer((req, res) => {
|
|
59
57
|
|
|
60
58
|
// CORS preflight
|
|
@@ -63,28 +61,13 @@ const server = http.createServer((req, res) => {
|
|
|
63
61
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
64
62
|
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
65
63
|
|
|
66
|
-
// ── Draply: Save
|
|
64
|
+
// ── Draply: Save changes to CSS ─────────────────────────────────────────────
|
|
67
65
|
if (req.url === '/draply-save' && req.method === 'POST') {
|
|
68
66
|
let body = '';
|
|
69
67
|
req.on('data', c => body += c);
|
|
70
68
|
req.on('end', () => {
|
|
71
69
|
try {
|
|
72
70
|
const { changes } = JSON.parse(body);
|
|
73
|
-
const projectInfo = detectProject(projectRoot);
|
|
74
|
-
const results = [];
|
|
75
|
-
|
|
76
|
-
for (const ch of (changes || [])) {
|
|
77
|
-
if (!ch.selector || !ch.props) continue;
|
|
78
|
-
const result = autoApplyChange(projectRoot, ch.selector, ch.props, projectInfo);
|
|
79
|
-
results.push(result);
|
|
80
|
-
if (result.applied) {
|
|
81
|
-
console.log(` \x1b[32m✓\x1b[0m Applied: ${result.file} (${result.strategy})`);
|
|
82
|
-
} else {
|
|
83
|
-
console.log(` \x1b[33m⚠\x1b[0m Fallback CSS: ${ch.selector.split('>').pop().trim()}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Also save CSS override as backup
|
|
88
71
|
const lines = [];
|
|
89
72
|
for (const ch of (changes || [])) {
|
|
90
73
|
if (!ch.selector) continue;
|
|
@@ -96,11 +79,9 @@ const server = http.createServer((req, res) => {
|
|
|
96
79
|
}
|
|
97
80
|
const css = '/* draply — ' + new Date().toLocaleString('ru-RU') + ' */\n\n' + lines.join('\n\n') + '\n';
|
|
98
81
|
fs.writeFileSync(overridesPath, css, 'utf8');
|
|
99
|
-
|
|
100
|
-
const applied = results.filter(r => r.applied).length;
|
|
101
|
-
const total = results.length;
|
|
82
|
+
console.log(` \x1b[32m✓\x1b[0m Saved ${changes.length} changes to .draply/overrides.css`);
|
|
102
83
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
103
|
-
res.end(JSON.stringify({ ok: true
|
|
84
|
+
res.end(JSON.stringify({ ok: true }));
|
|
104
85
|
} catch (e) {
|
|
105
86
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
106
87
|
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
@@ -133,32 +114,6 @@ const server = http.createServer((req, res) => {
|
|
|
133
114
|
return;
|
|
134
115
|
}
|
|
135
116
|
|
|
136
|
-
// ── Draply: Project Info endpoint ──────────────────────────────────────────
|
|
137
|
-
if (req.url === '/draply-project-info' && req.method === 'GET') {
|
|
138
|
-
const info = detectProject(projectRoot);
|
|
139
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
140
|
-
res.end(JSON.stringify(info));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ── Draply: Find Source endpoint ──────────────────────────────────────────
|
|
145
|
-
if (req.url === '/draply-find-source' && req.method === 'POST') {
|
|
146
|
-
let body = '';
|
|
147
|
-
req.on('data', c => body += c);
|
|
148
|
-
req.on('end', () => {
|
|
149
|
-
try {
|
|
150
|
-
const { selector, tagName, className } = JSON.parse(body);
|
|
151
|
-
const result = findSourceFile(projectRoot, { selector, tagName, className });
|
|
152
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
153
|
-
res.end(JSON.stringify(result));
|
|
154
|
-
} catch (e) {
|
|
155
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
156
|
-
res.end(JSON.stringify({ found: false, error: e.message }));
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
117
|
// ── Proxy to dev server ────────────────────────────────────────────────────
|
|
163
118
|
const opts = {
|
|
164
119
|
hostname: targetHost,
|
|
@@ -196,8 +151,8 @@ const server = http.createServer((req, res) => {
|
|
|
196
151
|
pReq.on('error', () => {
|
|
197
152
|
res.writeHead(502, { 'Content-Type': 'text/html' });
|
|
198
153
|
res.end(`<!DOCTYPE html><html><body style="background:#0a0a0f;color:#e8e8f0;font-family:monospace;padding:60px;text-align:center">
|
|
199
|
-
<h2 style="color:#ff6b6b">⚠
|
|
200
|
-
<p style="color:#555;margin-top:16px"
|
|
154
|
+
<h2 style="color:#ff6b6b">⚠ Can't reach ${targetHost}:${targetPort}</h2>
|
|
155
|
+
<p style="color:#555;margin-top:16px">Make sure your dev server is running, then refresh</p>
|
|
201
156
|
<script>setTimeout(()=>location.reload(), 2000)</script>
|
|
202
157
|
</body></html>`);
|
|
203
158
|
});
|
|
@@ -206,435 +161,10 @@ const server = http.createServer((req, res) => {
|
|
|
206
161
|
});
|
|
207
162
|
|
|
208
163
|
server.listen(proxyPort, () => {
|
|
209
|
-
console.log('\n \x1b[32m●\x1b[0m Draply
|
|
210
|
-
console.log(`
|
|
211
|
-
console.log(`
|
|
212
|
-
console.log(` \x1b[90mCtrl+C
|
|
164
|
+
console.log('\n \x1b[32m●\x1b[0m Draply running\n');
|
|
165
|
+
console.log(` Your project → \x1b[36mhttp://${targetHost}:${targetPort}\x1b[0m`);
|
|
166
|
+
console.log(` Open this → \x1b[33mhttp://localhost:${proxyPort}\x1b[0m \x1b[32m← go here!\x1b[0m\n`);
|
|
167
|
+
console.log(` \x1b[90mCtrl+C to stop\x1b[0m\n`);
|
|
213
168
|
});
|
|
214
169
|
|
|
215
|
-
process.on('SIGINT', () => { console.log('\n \x1b[90mDraply
|
|
216
|
-
|
|
217
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
218
|
-
// PROJECT DETECTION
|
|
219
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
220
|
-
function detectProject(root) {
|
|
221
|
-
const result = { framework: 'unknown', cssStrategy: 'unknown', root };
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
const pkgPath = path.join(root, 'package.json');
|
|
225
|
-
if (!fs.existsSync(pkgPath)) return result;
|
|
226
|
-
|
|
227
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
228
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
229
|
-
|
|
230
|
-
// Detect framework
|
|
231
|
-
if (allDeps['next']) result.framework = 'next';
|
|
232
|
-
else if (allDeps['react']) result.framework = 'react';
|
|
233
|
-
else if (allDeps['nuxt']) result.framework = 'nuxt';
|
|
234
|
-
else if (allDeps['vue']) result.framework = 'vue';
|
|
235
|
-
else if (allDeps['@angular/core']) result.framework = 'angular';
|
|
236
|
-
else if (allDeps['svelte']) result.framework = 'svelte';
|
|
237
|
-
else if (allDeps['vite']) result.framework = 'vite';
|
|
238
|
-
|
|
239
|
-
// Detect CSS strategy
|
|
240
|
-
if (allDeps['tailwindcss']) result.cssStrategy = 'tailwind';
|
|
241
|
-
else if (allDeps['styled-components']) result.cssStrategy = 'styled-components';
|
|
242
|
-
else if (allDeps['@emotion/react'] || allDeps['@emotion/styled']) result.cssStrategy = 'emotion';
|
|
243
|
-
else if (allDeps['sass'] || allDeps['node-sass']) result.cssStrategy = 'sass';
|
|
244
|
-
else {
|
|
245
|
-
// Check for CSS modules (usually enabled by default in React/Next)
|
|
246
|
-
if (['react', 'next'].includes(result.framework)) {
|
|
247
|
-
result.cssStrategy = 'css-modules';
|
|
248
|
-
} else {
|
|
249
|
-
result.cssStrategy = 'external';
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} catch { /* ignore */ }
|
|
253
|
-
|
|
254
|
-
return result;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
258
|
-
// SOURCE FILE FINDER
|
|
259
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
260
|
-
function findSourceFile(root, { selector, tagName, className }) {
|
|
261
|
-
const result = { found: false, file: null, line: null, hint: '' };
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const srcDirs = ['src', 'app', 'pages', 'components', 'lib'];
|
|
265
|
-
const extensions = ['.tsx', '.jsx', '.vue', '.svelte', '.js', '.ts'];
|
|
266
|
-
const searchTerm = className || tagName || '';
|
|
267
|
-
|
|
268
|
-
if (!searchTerm) {
|
|
269
|
-
result.hint = 'Select an element with a class name for better results.';
|
|
270
|
-
return result;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
for (const dir of srcDirs) {
|
|
274
|
-
const dirPath = path.join(root, dir);
|
|
275
|
-
if (!fs.existsSync(dirPath)) continue;
|
|
276
|
-
|
|
277
|
-
const files = walkDir(dirPath, extensions);
|
|
278
|
-
for (const file of files) {
|
|
279
|
-
try {
|
|
280
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
281
|
-
const lines = content.split('\n');
|
|
282
|
-
for (let i = 0; i < lines.length; i++) {
|
|
283
|
-
if (lines[i].includes(searchTerm)) {
|
|
284
|
-
result.found = true;
|
|
285
|
-
result.file = path.relative(root, file);
|
|
286
|
-
result.line = i + 1;
|
|
287
|
-
result.hint = `Found "${searchTerm}" at ${result.file}:${result.line}`;
|
|
288
|
-
return result;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
} catch { /* skip unreadable files */ }
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
result.hint = `Could not find "${searchTerm}" in source files.`;
|
|
296
|
-
} catch (e) {
|
|
297
|
-
result.hint = 'Error searching source files: ' + e.message;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return result;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function walkDir(dir, extensions, results = []) {
|
|
304
|
-
try {
|
|
305
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
306
|
-
for (const entry of entries) {
|
|
307
|
-
const fullPath = path.join(dir, entry.name);
|
|
308
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.next' || entry.name === 'dist') continue;
|
|
309
|
-
if (entry.isDirectory()) {
|
|
310
|
-
walkDir(fullPath, extensions, results);
|
|
311
|
-
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
312
|
-
results.push(fullPath);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
} catch { /* ignore */ }
|
|
316
|
-
return results;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
320
|
-
// AUTO-APPLY CHANGES TO SOURCE FILES
|
|
321
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
322
|
-
|
|
323
|
-
function autoApplyChange(root, selector, props, projectInfo) {
|
|
324
|
-
const result = { selector, applied: false, file: null, strategy: null, reason: '' };
|
|
325
|
-
|
|
326
|
-
// 1. Extract class name from selector
|
|
327
|
-
const className = extractClassName(selector);
|
|
328
|
-
if (!className) {
|
|
329
|
-
result.reason = 'No class name in selector';
|
|
330
|
-
return result;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// 2. Try: find existing CSS rule and modify it
|
|
334
|
-
const cssResult = findAndModifyCSSRule(root, className, props);
|
|
335
|
-
if (cssResult.applied) {
|
|
336
|
-
result.applied = true;
|
|
337
|
-
result.file = path.relative(root, cssResult.file);
|
|
338
|
-
result.strategy = 'css-modify';
|
|
339
|
-
return result;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// 3. Try: find JSX component using this class → find its CSS import → add rule there
|
|
343
|
-
const importResult = findCSSImportAndAppend(root, className, props);
|
|
344
|
-
if (importResult.applied) {
|
|
345
|
-
result.applied = true;
|
|
346
|
-
result.file = path.relative(root, importResult.file);
|
|
347
|
-
result.strategy = 'css-append';
|
|
348
|
-
return result;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// 4. Try: modify inline style in JSX
|
|
352
|
-
const inlineResult = modifyInlineStyle(root, className, props);
|
|
353
|
-
if (inlineResult.applied) {
|
|
354
|
-
result.applied = true;
|
|
355
|
-
result.file = path.relative(root, inlineResult.file);
|
|
356
|
-
result.strategy = 'inline-style';
|
|
357
|
-
return result;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// 5. Fallback: find any CSS file in project and append
|
|
361
|
-
const fallbackResult = appendToAnyCSSFile(root, className, props);
|
|
362
|
-
if (fallbackResult.applied) {
|
|
363
|
-
result.applied = true;
|
|
364
|
-
result.file = path.relative(root, fallbackResult.file);
|
|
365
|
-
result.strategy = 'css-fallback';
|
|
366
|
-
return result;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
result.reason = 'Could not find target file for ' + className;
|
|
370
|
-
return result;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Extract the most specific class name from a CSS selector
|
|
374
|
-
function extractClassName(selector) {
|
|
375
|
-
// Selector like: #root > div > section.nexus-hero > h1.title
|
|
376
|
-
const parts = selector.split('>').map(s => s.trim());
|
|
377
|
-
// Try from the end — most specific first
|
|
378
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
379
|
-
const classMatch = parts[i].match(/\.([a-zA-Z][a-zA-Z0-9_-]*)/);
|
|
380
|
-
if (classMatch) return classMatch[1];
|
|
381
|
-
}
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Strategy 1: Find existing CSS rule in .css/.scss files and modify it
|
|
386
|
-
function findAndModifyCSSRule(root, className, props) {
|
|
387
|
-
const cssFiles = walkDir(root, ['.css', '.scss', '.sass', '.less']);
|
|
388
|
-
const ruleRegex = new RegExp(
|
|
389
|
-
`(\\.${escapeRegex(className)}\\s*\\{)([^}]*)(\\})`, 's'
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
for (const file of cssFiles) {
|
|
393
|
-
// Skip node_modules, .draply, etc
|
|
394
|
-
const rel = path.relative(root, file);
|
|
395
|
-
if (rel.includes('node_modules') || rel.includes('.draply')) continue;
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
let content = fs.readFileSync(file, 'utf8');
|
|
399
|
-
const match = content.match(ruleRegex);
|
|
400
|
-
if (!match) continue;
|
|
401
|
-
|
|
402
|
-
// Found the rule — modify it
|
|
403
|
-
const existingBlock = match[2];
|
|
404
|
-
let newBlock = existingBlock;
|
|
405
|
-
|
|
406
|
-
for (const [prop, val] of Object.entries(props)) {
|
|
407
|
-
const propRegex = new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g');
|
|
408
|
-
if (propRegex.test(newBlock)) {
|
|
409
|
-
// Property exists — update value
|
|
410
|
-
newBlock = newBlock.replace(
|
|
411
|
-
new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g'),
|
|
412
|
-
`$1${val}$3`
|
|
413
|
-
);
|
|
414
|
-
} else {
|
|
415
|
-
// Property doesn't exist — add it
|
|
416
|
-
newBlock = newBlock.trimEnd() + `\n ${prop}: ${val};\n`;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
content = content.replace(ruleRegex, `$1${newBlock}$3`);
|
|
421
|
-
fs.writeFileSync(file, content, 'utf8');
|
|
422
|
-
|
|
423
|
-
return { applied: true, file };
|
|
424
|
-
} catch { /* skip */ }
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return { applied: false };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Strategy 2: Find the JSX component using className, find its CSS import, append rule
|
|
431
|
-
function findCSSImportAndAppend(root, className, props) {
|
|
432
|
-
const jsxFiles = walkDir(root, ['.jsx', '.tsx', '.js', '.ts']);
|
|
433
|
-
|
|
434
|
-
for (const jsxFile of jsxFiles) {
|
|
435
|
-
const rel = path.relative(root, jsxFile);
|
|
436
|
-
if (rel.includes('node_modules')) continue;
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
const content = fs.readFileSync(jsxFile, 'utf8');
|
|
440
|
-
|
|
441
|
-
// Check if this component uses the className
|
|
442
|
-
if (!content.includes(className)) continue;
|
|
443
|
-
|
|
444
|
-
// Find CSS imports in this file
|
|
445
|
-
// Matches: import './styles.css' or import styles from './styles.module.css' or require('./styles.css')
|
|
446
|
-
const importRegex = /(?:import\s+(?:\w+\s+from\s+)?['"]([^'"]+\.(?:css|scss|sass|less))['"]|require\(['"]([^'"]+\.(?:css|scss|sass|less))['"]\))/g;
|
|
447
|
-
let importMatch;
|
|
448
|
-
const cssImports = [];
|
|
449
|
-
|
|
450
|
-
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
451
|
-
const importPath = importMatch[1] || importMatch[2];
|
|
452
|
-
const fullCSSPath = path.resolve(path.dirname(jsxFile), importPath);
|
|
453
|
-
if (fs.existsSync(fullCSSPath)) {
|
|
454
|
-
cssImports.push(fullCSSPath);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (cssImports.length === 0) continue;
|
|
459
|
-
|
|
460
|
-
// Append rule to the first CSS import
|
|
461
|
-
const targetCSS = cssImports[0];
|
|
462
|
-
const newRule = `\n/* Draply: .${className} */\n.${className} {\n${formatProps(props)}\n}\n`;
|
|
463
|
-
|
|
464
|
-
let cssContent = fs.readFileSync(targetCSS, 'utf8');
|
|
465
|
-
|
|
466
|
-
// Check if rule already exists (might have been added before)
|
|
467
|
-
if (cssContent.includes(`.${className}`)) {
|
|
468
|
-
// Modify existing rule instead
|
|
469
|
-
return findAndModifyCSSRuleSingle(targetCSS, className, props);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
cssContent += newRule;
|
|
473
|
-
fs.writeFileSync(targetCSS, cssContent, 'utf8');
|
|
474
|
-
|
|
475
|
-
return { applied: true, file: targetCSS };
|
|
476
|
-
} catch { /* skip */ }
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return { applied: false };
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Helper: modify rule in a specific file
|
|
483
|
-
function findAndModifyCSSRuleSingle(file, className, props) {
|
|
484
|
-
const ruleRegex = new RegExp(
|
|
485
|
-
`(\\.${escapeRegex(className)}\\s*\\{)([^}]*)(\\})`, 's'
|
|
486
|
-
);
|
|
487
|
-
try {
|
|
488
|
-
let content = fs.readFileSync(file, 'utf8');
|
|
489
|
-
const match = content.match(ruleRegex);
|
|
490
|
-
if (!match) return { applied: false };
|
|
491
|
-
|
|
492
|
-
let newBlock = match[2];
|
|
493
|
-
for (const [prop, val] of Object.entries(props)) {
|
|
494
|
-
const propRegex = new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g');
|
|
495
|
-
if (propRegex.test(newBlock)) {
|
|
496
|
-
newBlock = newBlock.replace(
|
|
497
|
-
new RegExp(`(${escapeRegex(prop)}\\s*:\\s*)([^;]+)(;)`, 'g'),
|
|
498
|
-
`$1${val}$3`
|
|
499
|
-
);
|
|
500
|
-
} else {
|
|
501
|
-
newBlock = newBlock.trimEnd() + `\n ${prop}: ${val};\n`;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
content = content.replace(ruleRegex, `$1${newBlock}$3`);
|
|
505
|
-
fs.writeFileSync(file, content, 'utf8');
|
|
506
|
-
return { applied: true, file };
|
|
507
|
-
} catch { return { applied: false }; }
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Strategy 3: Add inline style to JSX element
|
|
511
|
-
function modifyInlineStyle(root, className, props) {
|
|
512
|
-
const jsxFiles = walkDir(root, ['.jsx', '.tsx']);
|
|
513
|
-
|
|
514
|
-
for (const file of jsxFiles) {
|
|
515
|
-
const rel = path.relative(root, file);
|
|
516
|
-
if (rel.includes('node_modules')) continue;
|
|
517
|
-
|
|
518
|
-
try {
|
|
519
|
-
let content = fs.readFileSync(file, 'utf8');
|
|
520
|
-
if (!content.includes(className)) continue;
|
|
521
|
-
|
|
522
|
-
// Find element with this className
|
|
523
|
-
// Pattern: className="...nexus-hero..." or className={'...nexus-hero...'}
|
|
524
|
-
const elementRegex = new RegExp(
|
|
525
|
-
`(<[a-zA-Z][a-zA-Z0-9]*[^>]*className=["'{][^"'}]*${escapeRegex(className)}[^"'}]*["'}])([^>]*>|\\s*/>)`,
|
|
526
|
-
's'
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
const match = content.match(elementRegex);
|
|
530
|
-
if (!match) continue;
|
|
531
|
-
|
|
532
|
-
const fullTag = match[0];
|
|
533
|
-
const jsxProps = propsToJSXStyle(props);
|
|
534
|
-
|
|
535
|
-
// Check if element already has a style prop
|
|
536
|
-
if (fullTag.includes('style=')) {
|
|
537
|
-
// Modify existing style prop — add/update properties
|
|
538
|
-
const styleRegex = /style=\{\{([^}]*)\}\}/;
|
|
539
|
-
const styleMatch = fullTag.match(styleRegex);
|
|
540
|
-
if (styleMatch) {
|
|
541
|
-
let existingStyles = styleMatch[1];
|
|
542
|
-
for (const [camelProp, val] of Object.entries(jsxProps)) {
|
|
543
|
-
const propRegex = new RegExp(`${camelProp}\\s*:\\s*['"][^'"]*['"]`, 'g');
|
|
544
|
-
if (propRegex.test(existingStyles)) {
|
|
545
|
-
existingStyles = existingStyles.replace(propRegex, `${camelProp}: '${val}'`);
|
|
546
|
-
} else {
|
|
547
|
-
existingStyles = existingStyles.trim();
|
|
548
|
-
if (existingStyles && !existingStyles.endsWith(',')) existingStyles += ',';
|
|
549
|
-
existingStyles += ` ${camelProp}: '${val}'`;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
const newTag = fullTag.replace(styleRegex, `style={{${existingStyles}}}`);
|
|
553
|
-
content = content.replace(fullTag, newTag);
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
// Add new style prop
|
|
557
|
-
const styleStr = Object.entries(jsxProps)
|
|
558
|
-
.map(([k, v]) => `${k}: '${v}'`)
|
|
559
|
-
.join(', ');
|
|
560
|
-
const insertion = ` style={{${styleStr}}}`;
|
|
561
|
-
// Insert before the closing > or />
|
|
562
|
-
const newTag = fullTag.replace(match[2], insertion + match[2]);
|
|
563
|
-
content = content.replace(fullTag, newTag);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
fs.writeFileSync(file, content, 'utf8');
|
|
567
|
-
return { applied: true, file };
|
|
568
|
-
} catch { /* skip */ }
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return { applied: false };
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Strategy 4: Find any CSS file in the project and append the rule
|
|
575
|
-
function appendToAnyCSSFile(root, className, props) {
|
|
576
|
-
// Priority: index.css, App.css, globals.css, main.css, styles.css
|
|
577
|
-
const candidates = [
|
|
578
|
-
'src/index.css', 'src/App.css', 'src/app/globals.css', 'src/globals.css',
|
|
579
|
-
'src/main.css', 'src/styles.css', 'styles/globals.css', 'app/globals.css',
|
|
580
|
-
'src/styles/globals.css'
|
|
581
|
-
];
|
|
582
|
-
|
|
583
|
-
for (const candidate of candidates) {
|
|
584
|
-
const fullPath = path.join(root, candidate);
|
|
585
|
-
if (fs.existsSync(fullPath)) {
|
|
586
|
-
try {
|
|
587
|
-
let content = fs.readFileSync(fullPath, 'utf8');
|
|
588
|
-
// Check if rule already exists
|
|
589
|
-
if (content.includes(`.${className}`)) {
|
|
590
|
-
return findAndModifyCSSRuleSingle(fullPath, className, props);
|
|
591
|
-
}
|
|
592
|
-
const newRule = `\n/* Draply: .${className} */\n.${className} {\n${formatProps(props)}\n}\n`;
|
|
593
|
-
content += newRule;
|
|
594
|
-
fs.writeFileSync(fullPath, content, 'utf8');
|
|
595
|
-
return { applied: true, file: fullPath };
|
|
596
|
-
} catch { /* skip */ }
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Last resort: find ANY css file
|
|
601
|
-
const allCSS = walkDir(root, ['.css']);
|
|
602
|
-
for (const file of allCSS) {
|
|
603
|
-
const rel = path.relative(root, file);
|
|
604
|
-
if (rel.includes('node_modules') || rel.includes('.draply') || rel.includes('.next')) continue;
|
|
605
|
-
try {
|
|
606
|
-
let content = fs.readFileSync(file, 'utf8');
|
|
607
|
-
if (content.includes(`.${className}`)) {
|
|
608
|
-
return findAndModifyCSSRuleSingle(file, className, props);
|
|
609
|
-
}
|
|
610
|
-
const newRule = `\n/* Draply: .${className} */\n.${className} {\n${formatProps(props)}\n}\n`;
|
|
611
|
-
content += newRule;
|
|
612
|
-
fs.writeFileSync(file, content, 'utf8');
|
|
613
|
-
return { applied: true, file };
|
|
614
|
-
} catch { /* skip */ }
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return { applied: false };
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
621
|
-
|
|
622
|
-
function escapeRegex(str) {
|
|
623
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function formatProps(props) {
|
|
627
|
-
return Object.entries(props).map(([k, v]) => ` ${k}: ${v};`).join('\n');
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function camelCase(str) {
|
|
631
|
-
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function propsToJSXStyle(props) {
|
|
635
|
-
const result = {};
|
|
636
|
-
for (const [prop, val] of Object.entries(props)) {
|
|
637
|
-
result[camelCase(prop)] = val;
|
|
638
|
-
}
|
|
639
|
-
return result;
|
|
640
|
-
}
|
|
170
|
+
process.on('SIGINT', () => { console.log('\n \x1b[90mDraply stopped\x1b[0m\n'); process.exit(0); });
|