draply-dev 1.0.1 → 1.0.3

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.
Files changed (3) hide show
  1. package/bin/cli.js +214 -147
  2. package/package.json +1 -1
  3. package/src/overlay.js +944 -903
package/bin/cli.js CHANGED
@@ -2,24 +2,24 @@
2
2
  'use strict';
3
3
 
4
4
  const http = require('http');
5
- const fs = require('fs');
5
+ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const zlib = require('zlib');
8
8
 
9
- const args = process.argv.slice(2);
9
+ const args = process.argv.slice(2);
10
10
  const targetPort = parseInt(args[0]) || 3000;
11
- const proxyPort = parseInt(args[1]) || 4000;
11
+ const proxyPort = parseInt(args[1]) || 4000;
12
12
  const targetHost = args[2] || 'localhost';
13
13
 
14
- const OVERLAY_JS = fs.readFileSync(path.join(__dirname, '../src/overlay.js'), 'utf8');
14
+ const OVERLAY_JS = fs.readFileSync(path.join(__dirname, '../src/overlay.js'), 'utf8');
15
15
  // Unique marker that does NOT appear inside overlay.js itself
16
- const MARKER = 'data-ps-done="1"';
17
- const INJECT_TAG = `\n<link rel="stylesheet" href="/draply.css">\n<script ${MARKER}>\n${OVERLAY_JS}\n</script>`;
16
+ const MARKER = 'data-ps-done="1"';
17
+ const INJECT_TAG = `\n<link rel="stylesheet" href="/draply.css">\n<script ${MARKER}>\n${OVERLAY_JS}\n</script>`;
18
18
 
19
19
  function injectOverlay(html) {
20
20
  if (html.includes(MARKER)) return html;
21
- if (html.includes('</body>')) return html.replace(/<\/body>/i, INJECT_TAG + '\n</body>');
22
- if (html.includes('</html>')) return html.replace(/<\/html>/i, INJECT_TAG + '\n</html>');
21
+ if (html.includes('</body>')) return html.replace(/<\/body>/i, INJECT_TAG + '\n</body>');
22
+ if (html.includes('</html>')) return html.replace(/<\/html>/i, INJECT_TAG + '\n</html>');
23
23
  return html + INJECT_TAG;
24
24
  }
25
25
 
@@ -27,158 +27,214 @@ function decode(headers, chunks) {
27
27
  const enc = headers['content-encoding'] || '';
28
28
  const buf = Buffer.concat(chunks);
29
29
  return new Promise((res, rej) => {
30
- if (enc === 'gzip') return zlib.gunzip(buf, (e,d) => e ? rej(e) : res(d.toString('utf8')));
31
- if (enc === 'deflate') return zlib.inflate(buf, (e,d) => e ? rej(e) : res(d.toString('utf8')));
32
- if (enc === 'br') return zlib.brotliDecompress(buf, (e,d) => e ? rej(e) : res(d.toString('utf8')));
30
+ if (enc === 'gzip') return zlib.gunzip(buf, (e, d) => e ? rej(e) : res(d.toString('utf8')));
31
+ if (enc === 'deflate') return zlib.inflate(buf, (e, d) => e ? rej(e) : res(d.toString('utf8')));
32
+ if (enc === 'br') return zlib.brotliDecompress(buf, (e, d) => e ? rej(e) : res(d.toString('utf8')));
33
33
  res(buf.toString('utf8'));
34
34
  });
35
35
  }
36
36
 
37
37
  // Создаём пустой draply.css при старте если его нет
38
- const projectRoot = process.cwd();
38
+ const projectRoot = process.cwd();
39
39
  const overridesPath = path.join(projectRoot, 'draply.css');
40
40
  if (!fs.existsSync(overridesPath)) {
41
41
  fs.writeFileSync(overridesPath, '/* draply */\n', 'utf8');
42
42
  }
43
- let cssInjected = false; // флаг: CSS уже подключён к проекту?
44
-
45
- // ── Авто-определение фреймворка и подключение draply.css ──────────────────
46
- function autoInjectCSS() {
47
- if (cssInjected) return;
48
- cssInjected = true;
49
-
50
- const has = f => fs.existsSync(path.join(projectRoot, f));
51
- const read = f => fs.readFileSync(path.join(projectRoot, f), 'utf8');
52
- const write = (f, c) => fs.writeFileSync(path.join(projectRoot, f), c, 'utf8');
53
- const already = (content) => content.includes('draply.css');
54
-
55
- try {
56
- // Next.js App Router
57
- for (const f of ['app/layout.tsx', 'app/layout.jsx', 'app/layout.js']) {
58
- if (has(f)) {
59
- let src = read(f);
60
- if (already(src)) return;
61
- src = `import '../draply.css';\n` + src;
62
- write(f, src);
63
- console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
64
- return;
65
- }
66
- }
67
43
 
68
- // Next.js Pages Router
69
- for (const f of ['pages/_app.tsx', 'pages/_app.jsx', 'pages/_app.js']) {
70
- if (has(f)) {
71
- let src = read(f);
72
- if (already(src)) return;
73
- src = `import '../draply.css';\n` + src;
74
- write(f, src);
75
- console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
76
- return;
77
- }
78
- }
79
44
 
80
- // Nuxt
81
- for (const f of ['nuxt.config.ts', 'nuxt.config.js']) {
82
- if (has(f)) {
83
- let src = read(f);
84
- if (already(src)) return;
85
- if (src.includes('css:')) {
86
- src = src.replace(/css:\s*\[/, "css: ['./draply.css', ");
87
- } else {
88
- src = src.replace(/export default defineNuxtConfig\(\{/, "export default defineNuxtConfig({\n css: ['./draply.css'],");
89
- }
90
- write(f, src);
91
- console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
92
- return;
93
- }
94
- }
45
+ const server = http.createServer((req, res) => {
95
46
 
96
- // Vue (Vite)
97
- if (has('src/App.vue')) {
98
- for (const f of ['src/main.ts', 'src/main.js']) {
99
- if (has(f)) {
100
- let src = read(f);
101
- if (already(src)) return;
102
- src = `import '../draply.css'\n` + src;
103
- write(f, src);
104
- console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
105
- return;
106
- }
107
- }
108
- }
47
+ // CORS preflight
48
+ res.setHeader('Access-Control-Allow-Origin', '*');
49
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
50
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
51
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
109
52
 
110
- // React (Vite) src/main.tsx / src/main.jsx
111
- for (const f of ['src/main.tsx', 'src/main.jsx', 'src/main.js', 'src/main.ts', 'src/index.tsx', 'src/index.jsx', 'src/index.js']) {
112
- if (has(f)) {
113
- let src = read(f);
114
- if (already(src)) return;
115
- src = `import '../draply.css'\n` + src;
116
- write(f, src);
117
- console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
118
- return;
119
- }
120
- }
53
+ // ── Config endpoint (get/set API key) ───────────────────────────────────────
54
+ const configPath = path.join(process.cwd(), 'draply.config.json');
55
+ function loadConfig() { try { return JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { return {}; } }
56
+ function saveConfig(cfg) { fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf8'); }
121
57
 
122
- // CRA public/index.html
123
- if (has('public/index.html')) {
124
- let src = read('public/index.html');
125
- if (already(src)) return;
126
- src = src.replace(/<\/head>/i, ' <link rel="stylesheet" href="%PUBLIC_URL%/draply.css">\n </head>');
127
- write('public/index.html', src);
128
- // Копируем draply.css в public/
129
- fs.copyFileSync(overridesPath, path.join(projectRoot, 'public/draply.css'));
130
- console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → public/index.html`);
58
+ if (req.url === '/draply-config') {
59
+ if (req.method === 'GET') {
60
+ const cfg = loadConfig();
61
+ res.writeHead(200, { 'Content-Type': 'application/json' });
62
+ res.end(JSON.stringify({ hasKey: !!cfg.apiKey, provider: cfg.provider || 'groq' }));
131
63
  return;
132
64
  }
133
-
134
- // Plain HTML / Vite — index.html в корне
135
- if (has('index.html')) {
136
- let src = read('index.html');
137
- if (already(src)) return;
138
- src = src.replace(/<\/head>/i, ' <link rel="stylesheet" href="./draply.css">\n </head>');
139
- write('index.html', src);
140
- console.log(` \x1b[32m✓\x1b[0m Подключил draply.css index.html`);
65
+ if (req.method === 'POST') {
66
+ let body = '';
67
+ req.on('data', c => body += c);
68
+ req.on('end', () => {
69
+ try {
70
+ const cfg = { ...loadConfig(), ...JSON.parse(body) };
71
+ saveConfig(cfg);
72
+ res.writeHead(200, { 'Content-Type': 'application/json' });
73
+ res.end(JSON.stringify({ ok: true }));
74
+ } catch (e) {
75
+ res.writeHead(500); res.end(JSON.stringify({ ok: false, error: e.message }));
76
+ }
77
+ });
141
78
  return;
142
79
  }
143
-
144
- console.log(` \x1b[33m⚠\x1b[0m Не удалось определить фреймворк — добавь draply.css вручную`);
145
- } catch (err) {
146
- console.log(` \x1b[33m⚠\x1b[0m Ошибка авто-подключения: ${err.message}`);
147
80
  }
148
- }
149
-
150
- const server = http.createServer((req, res) => {
151
-
152
- // CORS preflight
153
- res.setHeader('Access-Control-Allow-Origin', '*');
154
- res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
155
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
156
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
157
81
 
158
- // ── Draply: Save endpoint ──────────────────────────────────────────────────
159
- if (req.url === '/draply-save' && req.method === 'POST') {
82
+ // ── AI Apply endpoint ───────────────────────────────────────────────────────
83
+ if ((req.url === '/draply-ai-apply' || req.url === '/draply-save') && req.method === 'POST') {
160
84
  let body = '';
161
85
  req.on('data', c => body += c);
162
- req.on('end', () => {
86
+ req.on('end', async () => {
163
87
  try {
164
88
  const { changes } = JSON.parse(body);
165
- const lines = [];
89
+ const cfg = loadConfig();
90
+
91
+ // If no config, fallback to saving standard CSS
92
+ if (!cfg.apiKey) {
93
+ const lines = [];
94
+ for (const ch of (changes || [])) {
95
+ if (!ch.selector) continue;
96
+ const props = Object.entries(ch.props).map(([k, v]) => ` ${k}: ${v};`).join('\n');
97
+ const label = ch.selector.split('>').pop().trim();
98
+ lines.push(`/* ${label} */\n${ch.selector} {\n${props}\n}`);
99
+ }
100
+ const css = '/* draply */\n\n' + lines.join('\n\n') + '\n';
101
+ fs.writeFileSync(overridesPath, css, 'utf8');
102
+ res.writeHead(200, { 'Content-Type': 'application/json' });
103
+ res.end(JSON.stringify({ ok: true, fallback: true }));
104
+ return;
105
+ }
106
+
107
+ const results = [];
108
+ const fileMap = new Map();
109
+
110
+ // 1. Group by exact file (thanks to React Fiber)
166
111
  for (const ch of (changes || [])) {
167
- if (!ch.selector) continue;
168
- const props = Object.entries(ch.props)
169
- .map(([k, v]) => ` ${k}: ${v};`)
170
- .join('\n');
171
- const label = ch.selector.split('>').pop().trim();
172
- lines.push(`/* ${label} */
173
- ${ch.selector} {
174
- ${props}
175
- }`);
112
+ if (!ch.exactFile || !fs.existsSync(ch.exactFile)) {
113
+ results.push({ selector: ch.selector, ok: false, reason: 'Source file not linked' });
114
+ continue;
115
+ }
116
+ if (!fileMap.has(ch.exactFile)) {
117
+ fileMap.set(ch.exactFile, { content: fs.readFileSync(ch.exactFile, 'utf8'), items: [] });
118
+ }
119
+ fileMap.get(ch.exactFile).items.push(ch);
176
120
  }
177
- const css = '/* draply — ' + new Date().toLocaleString('ru-RU') + ' */\n\n' + lines.join('\n\n') + '\n';
178
- fs.writeFileSync(overridesPath, css, 'utf8');
179
- autoInjectCSS();
121
+
122
+ // 2. Call AI per file using XML Patches
123
+ for (const [filePath, { content, items }] of fileMap) {
124
+
125
+ let changesBlock = '';
126
+ const lines = content.split('\n');
127
+ let classNamesToFind = items.map(c => {
128
+ const parts = c.selector.split('.');
129
+ return parts.length > 1 ? parts.pop() : '';
130
+ }).filter(Boolean);
131
+
132
+ let ctxStart = 0, ctxEnd = lines.length - 1;
133
+ for (let i = 0; i < lines.length; i++) {
134
+ if (classNamesToFind.some(cls => lines[i].includes(cls))) {
135
+ ctxStart = Math.max(0, i - 25);
136
+ ctxEnd = Math.min(lines.length - 1, i + 25);
137
+ }
138
+ }
139
+ const snippet = lines.slice(ctxStart, ctxEnd + 1).join('\n');
140
+
141
+ items.forEach(item => {
142
+ const propsStr = Object.entries(item.props).map(([k, v]) => ` ${k}: ${v}`).join('\n');
143
+ changesBlock += `\nTarget Selector: ${item.selector}\nChanges:\n${propsStr}\n`;
144
+ });
145
+
146
+ const prompt = `You are a strict code editor applying style changes to a file.
147
+
148
+ FILE: ${path.basename(filePath)} (lines ${ctxStart + 1}-${ctxEnd + 1})
149
+ \`\`\`
150
+ ${snippet}
151
+ \`\`\`
152
+
153
+ CHANGES (Apply to React elements matching selectors):
154
+ ${changesBlock}
155
+
156
+ IMPORTANT: Return your changes wrapped in <patch> tags containing <search> and <replace> blocks. DO NOT use JSON.
157
+
158
+ Rules:
159
+ - The content inside <search> must be an EXACT substring from the file snippet above.
160
+ - Update JSX inline styles or Tailwind classes appropriately.
161
+ - REPLACE old style values if they exist, DO NOT duplicate keys!
162
+
163
+ Example response:
164
+ <patch>
165
+ <search>
166
+ <button className="btn" style={{ padding: '10px' }}>
167
+ </search>
168
+ <replace>
169
+ <button className="btn" style={{ padding: '10px', color: '#ff0000' }}>
170
+ </replace>
171
+ </patch>
172
+
173
+ Return ONLY the patch blocks.`;
174
+
175
+ let apiResult = '';
176
+ try {
177
+ // Groq call
178
+ const groqBody = JSON.stringify({
179
+ model: 'llama-3.3-70b-versatile',
180
+ messages: [{ role: 'user', content: prompt }],
181
+ temperature: 0.1, max_tokens: 8192
182
+ });
183
+ apiResult = await new Promise((resolve, reject) => {
184
+ const https = require('https');
185
+ const req = https.request({
186
+ hostname: 'api.groq.com', path: '/openai/v1/chat/completions',
187
+ method: 'POST',
188
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Length': Buffer.byteLength(groqBody) }
189
+ }, res => {
190
+ const chunks = [];
191
+ res.on('data', c => chunks.push(c));
192
+ res.on('end', () => {
193
+ const data = JSON.parse(Buffer.concat(chunks).toString());
194
+ if (data.error) reject(new Error(data.error.message));
195
+ else resolve(data.choices?.[0]?.message?.content || '');
196
+ });
197
+ });
198
+ req.on('error', reject); req.write(groqBody); req.end();
199
+ });
200
+
201
+ const patches = [];
202
+ const patchRegex = /<search>([\s\S]*?)<\/search>[\s\S]*?<replace>([\s\S]*?)<\/replace>/g;
203
+ let match;
204
+ while ((match = patchRegex.exec(apiResult)) !== null) {
205
+ let s = match[1], r = match[2];
206
+ if (s.startsWith('\n')) s = s.substring(1);
207
+ if (s.endsWith('\n')) s = s.substring(0, s.length - 1);
208
+ if (r.startsWith('\n')) r = r.substring(1);
209
+ if (r.endsWith('\n')) r = r.substring(0, r.length - 1);
210
+ if (s.trim()) patches.push({ search: s, replace: r });
211
+ }
212
+
213
+ let newContent = content;
214
+ let applied = 0;
215
+ for (const patch of patches) {
216
+ if (newContent.includes(patch.search)) {
217
+ newContent = newContent.replace(patch.search, patch.replace);
218
+ applied++;
219
+ }
220
+ }
221
+
222
+ if (applied > 0) {
223
+ fs.writeFileSync(filePath, newContent, 'utf8');
224
+ console.log(` \x1b[32m✓\x1b[0m Modified ${path.basename(filePath)}`);
225
+ items.forEach(item => results.push({ selector: item.selector, ok: true }));
226
+ } else {
227
+ items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI patch failed to match' }));
228
+ }
229
+ } catch (e) {
230
+ items.forEach(item => results.push({ selector: item.selector, ok: false, reason: e.message }));
231
+ }
232
+ }
233
+
234
+ const applied = results.filter(r => r.ok).length;
180
235
  res.writeHead(200, { 'Content-Type': 'application/json' });
181
- res.end(JSON.stringify({ ok: true }));
236
+ res.end(JSON.stringify({ ok: true, applied, total: results.length, results }));
237
+
182
238
  } catch (e) {
183
239
  res.writeHead(500, { 'Content-Type': 'application/json' });
184
240
  res.end(JSON.stringify({ ok: false, error: e.message }));
@@ -188,14 +244,25 @@ ${props}
188
244
  }
189
245
 
190
246
  // ── Draply: Serve CSS ──────────────────────────────────────────────────────
191
- if (req.url === '/draply.css') {
247
+ if (req.url.split('?')[0] === '/draply.css') {
248
+ const isModule = req.headers['sec-fetch-dest'] === 'script' || req.url.includes('import');
192
249
  try {
193
250
  const css = fs.readFileSync(overridesPath, 'utf8');
194
- res.writeHead(200, { 'Content-Type': 'text/css', 'cache-control': 'no-store' });
195
- res.end(css);
251
+ if (isModule) {
252
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'cache-control': 'no-store' });
253
+ res.end(`const style = document.createElement('style');\nstyle.setAttribute('data-draply-module', '1');\nstyle.textContent = ${JSON.stringify(css)};\ndocument.head.appendChild(style);\nexport default {};`);
254
+ } else {
255
+ res.writeHead(200, { 'Content-Type': 'text/css', 'cache-control': 'no-store' });
256
+ res.end(css);
257
+ }
196
258
  } catch (e) {
197
- res.writeHead(200, { 'Content-Type': 'text/css' });
198
- res.end('');
259
+ if (isModule) {
260
+ res.writeHead(200, { 'Content-Type': 'application/javascript' });
261
+ res.end('export default {};');
262
+ } else {
263
+ res.writeHead(200, { 'Content-Type': 'text/css' });
264
+ res.end('');
265
+ }
199
266
  }
200
267
  return;
201
268
  }
@@ -205,10 +272,10 @@ ${props}
205
272
  // ── Proxy to dev server ────────────────────────────────────────────────────
206
273
  const opts = {
207
274
  hostname: targetHost,
208
- port: targetPort,
209
- path: req.url,
210
- method: req.method,
211
- headers: { ...req.headers, host: `${targetHost}:${targetPort}`, 'accept-encoding': 'identity' },
275
+ port: targetPort,
276
+ path: req.url,
277
+ method: req.method,
278
+ headers: { ...req.headers, host: `${targetHost}:${targetPort}`, 'accept-encoding': 'identity' },
212
279
  };
213
280
 
214
281
  const pReq = http.request(opts, pRes => {
@@ -218,15 +285,15 @@ ${props}
218
285
  pRes.on('data', c => chunks.push(c));
219
286
  pRes.on('end', async () => {
220
287
  try {
221
- const html = await decode(pRes.headers, chunks);
222
- const out = Buffer.from(injectOverlay(html), 'utf8');
288
+ const html = await decode(pRes.headers, chunks);
289
+ const out = Buffer.from(injectOverlay(html), 'utf8');
223
290
  const headers = { ...pRes.headers };
224
291
  delete headers['content-encoding'];
225
292
  headers['content-length'] = out.length;
226
- headers['cache-control'] = 'no-store';
293
+ headers['cache-control'] = 'no-store';
227
294
  res.writeHead(pRes.statusCode, headers);
228
295
  res.end(out);
229
- } catch(e) {
296
+ } catch (e) {
230
297
  res.writeHead(500); res.end('Draply error: ' + e.message);
231
298
  }
232
299
  });
@@ -237,7 +304,7 @@ ${props}
237
304
  });
238
305
 
239
306
  pReq.on('error', () => {
240
- res.writeHead(502, {'Content-Type':'text/html'});
307
+ res.writeHead(502, { 'Content-Type': 'text/html' });
241
308
  res.end(`<!DOCTYPE html><html><body style="background:#0a0a0f;color:#e8e8f0;font-family:monospace;padding:60px;text-align:center">
242
309
  <h2 style="color:#ff6b6b">⚠ Не могу достучаться до ${targetHost}:${targetPort}</h2>
243
310
  <p style="color:#555;margin-top:16px">Убедись что dev сервер запущен, потом обнови страницу</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draply-dev",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Visual overlay for any frontend project — move, resize, restyle live in the browser, save to CSS",
5
5
  "author": "Arman",
6
6
  "type": "commonjs",