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.
- package/bin/cli.js +214 -147
- package/package.json +1 -1
- 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
|
|
5
|
+
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const zlib = require('zlib');
|
|
8
8
|
|
|
9
|
-
const args
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
10
|
const targetPort = parseInt(args[0]) || 3000;
|
|
11
|
-
const proxyPort
|
|
11
|
+
const proxyPort = parseInt(args[1]) || 4000;
|
|
12
12
|
const targetHost = args[2] || 'localhost';
|
|
13
13
|
|
|
14
|
-
const OVERLAY_JS
|
|
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
|
|
17
|
-
const INJECT_TAG
|
|
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>'))
|
|
22
|
-
if (html.includes('</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')
|
|
31
|
-
if (enc === 'deflate') return zlib.inflate(buf,
|
|
32
|
-
if (enc === 'br')
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
// ──
|
|
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
|
|
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.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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:
|
|
209
|
-
path:
|
|
210
|
-
method:
|
|
211
|
-
headers:
|
|
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
|
|
222
|
-
const out
|
|
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']
|
|
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>
|