draply-dev 1.0.1 → 1.0.4
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 +220 -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,220 @@ 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, fallback to index.html)
|
|
166
111
|
for (const ch of (changes || [])) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
112
|
+
let targetFile = ch.exactFile;
|
|
113
|
+
if (!targetFile || !fs.existsSync(targetFile)) {
|
|
114
|
+
const fallbackHTML = path.join(process.cwd(), 'index.html');
|
|
115
|
+
if (fs.existsSync(fallbackHTML)) {
|
|
116
|
+
targetFile = fallbackHTML;
|
|
117
|
+
} else {
|
|
118
|
+
results.push({ selector: ch.selector, ok: false, reason: 'Source file not linked' });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!fileMap.has(targetFile)) {
|
|
123
|
+
fileMap.set(targetFile, { content: fs.readFileSync(targetFile, 'utf8'), items: [] });
|
|
124
|
+
}
|
|
125
|
+
fileMap.get(targetFile).items.push(ch);
|
|
176
126
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
127
|
+
|
|
128
|
+
// 2. Call AI per file using XML Patches
|
|
129
|
+
for (const [filePath, { content, items }] of fileMap) {
|
|
130
|
+
|
|
131
|
+
let changesBlock = '';
|
|
132
|
+
const lines = content.split('\n');
|
|
133
|
+
let classNamesToFind = items.map(c => {
|
|
134
|
+
const parts = c.selector.split('.');
|
|
135
|
+
return parts.length > 1 ? parts.pop() : '';
|
|
136
|
+
}).filter(Boolean);
|
|
137
|
+
|
|
138
|
+
let ctxStart = 0, ctxEnd = lines.length - 1;
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
if (classNamesToFind.some(cls => lines[i].includes(cls))) {
|
|
141
|
+
ctxStart = Math.max(0, i - 25);
|
|
142
|
+
ctxEnd = Math.min(lines.length - 1, i + 25);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const snippet = lines.slice(ctxStart, ctxEnd + 1).join('\n');
|
|
146
|
+
|
|
147
|
+
items.forEach(item => {
|
|
148
|
+
const propsStr = Object.entries(item.props).map(([k, v]) => ` ${k}: ${v}`).join('\n');
|
|
149
|
+
changesBlock += `\nTarget Selector: ${item.selector}\nChanges:\n${propsStr}\n`;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const prompt = `You are a strict code editor applying style changes to a file.
|
|
153
|
+
|
|
154
|
+
FILE: ${path.basename(filePath)} (lines ${ctxStart + 1}-${ctxEnd + 1})
|
|
155
|
+
\`\`\`
|
|
156
|
+
${snippet}
|
|
157
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
CHANGES (Apply to HTML/React elements matching selectors):
|
|
160
|
+
${changesBlock}
|
|
161
|
+
|
|
162
|
+
IMPORTANT: Return your changes wrapped in <patch> tags containing <search> and <replace> blocks. DO NOT use JSON.
|
|
163
|
+
|
|
164
|
+
Rules:
|
|
165
|
+
- The content inside <search> must be an EXACT substring from the file snippet above.
|
|
166
|
+
- Update HTML/JSX inline styles or Tailwind classes appropriately.
|
|
167
|
+
- REPLACE old style values if they exist, DO NOT duplicate keys!
|
|
168
|
+
|
|
169
|
+
Example response:
|
|
170
|
+
<patch>
|
|
171
|
+
<search>
|
|
172
|
+
<button className="btn" style={{ padding: '10px' }}>
|
|
173
|
+
</search>
|
|
174
|
+
<replace>
|
|
175
|
+
<button className="btn" style={{ padding: '10px', color: '#ff0000' }}>
|
|
176
|
+
</replace>
|
|
177
|
+
</patch>
|
|
178
|
+
|
|
179
|
+
Return ONLY the patch blocks.`;
|
|
180
|
+
|
|
181
|
+
let apiResult = '';
|
|
182
|
+
try {
|
|
183
|
+
// Groq call
|
|
184
|
+
const groqBody = JSON.stringify({
|
|
185
|
+
model: 'llama-3.3-70b-versatile',
|
|
186
|
+
messages: [{ role: 'user', content: prompt }],
|
|
187
|
+
temperature: 0.1, max_tokens: 8192
|
|
188
|
+
});
|
|
189
|
+
apiResult = await new Promise((resolve, reject) => {
|
|
190
|
+
const https = require('https');
|
|
191
|
+
const req = https.request({
|
|
192
|
+
hostname: 'api.groq.com', path: '/openai/v1/chat/completions',
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Length': Buffer.byteLength(groqBody) }
|
|
195
|
+
}, res => {
|
|
196
|
+
const chunks = [];
|
|
197
|
+
res.on('data', c => chunks.push(c));
|
|
198
|
+
res.on('end', () => {
|
|
199
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
200
|
+
if (data.error) reject(new Error(data.error.message));
|
|
201
|
+
else resolve(data.choices?.[0]?.message?.content || '');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
req.on('error', reject); req.write(groqBody); req.end();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const patches = [];
|
|
208
|
+
const patchRegex = /<search>([\s\S]*?)<\/search>[\s\S]*?<replace>([\s\S]*?)<\/replace>/g;
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = patchRegex.exec(apiResult)) !== null) {
|
|
211
|
+
let s = match[1], r = match[2];
|
|
212
|
+
if (s.startsWith('\n')) s = s.substring(1);
|
|
213
|
+
if (s.endsWith('\n')) s = s.substring(0, s.length - 1);
|
|
214
|
+
if (r.startsWith('\n')) r = r.substring(1);
|
|
215
|
+
if (r.endsWith('\n')) r = r.substring(0, r.length - 1);
|
|
216
|
+
if (s.trim()) patches.push({ search: s, replace: r });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let newContent = content;
|
|
220
|
+
let applied = 0;
|
|
221
|
+
for (const patch of patches) {
|
|
222
|
+
if (newContent.includes(patch.search)) {
|
|
223
|
+
newContent = newContent.replace(patch.search, patch.replace);
|
|
224
|
+
applied++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (applied > 0) {
|
|
229
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
230
|
+
console.log(` \x1b[32m✓\x1b[0m Modified ${path.basename(filePath)}`);
|
|
231
|
+
items.forEach(item => results.push({ selector: item.selector, ok: true }));
|
|
232
|
+
} else {
|
|
233
|
+
items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI patch failed to match' }));
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
items.forEach(item => results.push({ selector: item.selector, ok: false, reason: e.message }));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const applied = results.filter(r => r.ok).length;
|
|
180
241
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
181
|
-
res.end(JSON.stringify({ ok: true }));
|
|
242
|
+
res.end(JSON.stringify({ ok: true, applied, total: results.length, results }));
|
|
243
|
+
|
|
182
244
|
} catch (e) {
|
|
183
245
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
184
246
|
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
@@ -188,14 +250,25 @@ ${props}
|
|
|
188
250
|
}
|
|
189
251
|
|
|
190
252
|
// ── Draply: Serve CSS ──────────────────────────────────────────────────────
|
|
191
|
-
if (req.url === '/draply.css') {
|
|
253
|
+
if (req.url.split('?')[0] === '/draply.css') {
|
|
254
|
+
const isModule = req.headers['sec-fetch-dest'] === 'script' || req.url.includes('import');
|
|
192
255
|
try {
|
|
193
256
|
const css = fs.readFileSync(overridesPath, 'utf8');
|
|
194
|
-
|
|
195
|
-
|
|
257
|
+
if (isModule) {
|
|
258
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript', 'cache-control': 'no-store' });
|
|
259
|
+
res.end(`const style = document.createElement('style');\nstyle.setAttribute('data-draply-module', '1');\nstyle.textContent = ${JSON.stringify(css)};\ndocument.head.appendChild(style);\nexport default {};`);
|
|
260
|
+
} else {
|
|
261
|
+
res.writeHead(200, { 'Content-Type': 'text/css', 'cache-control': 'no-store' });
|
|
262
|
+
res.end(css);
|
|
263
|
+
}
|
|
196
264
|
} catch (e) {
|
|
197
|
-
|
|
198
|
-
|
|
265
|
+
if (isModule) {
|
|
266
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
267
|
+
res.end('export default {};');
|
|
268
|
+
} else {
|
|
269
|
+
res.writeHead(200, { 'Content-Type': 'text/css' });
|
|
270
|
+
res.end('');
|
|
271
|
+
}
|
|
199
272
|
}
|
|
200
273
|
return;
|
|
201
274
|
}
|
|
@@ -205,10 +278,10 @@ ${props}
|
|
|
205
278
|
// ── Proxy to dev server ────────────────────────────────────────────────────
|
|
206
279
|
const opts = {
|
|
207
280
|
hostname: targetHost,
|
|
208
|
-
port:
|
|
209
|
-
path:
|
|
210
|
-
method:
|
|
211
|
-
headers:
|
|
281
|
+
port: targetPort,
|
|
282
|
+
path: req.url,
|
|
283
|
+
method: req.method,
|
|
284
|
+
headers: { ...req.headers, host: `${targetHost}:${targetPort}`, 'accept-encoding': 'identity' },
|
|
212
285
|
};
|
|
213
286
|
|
|
214
287
|
const pReq = http.request(opts, pRes => {
|
|
@@ -218,15 +291,15 @@ ${props}
|
|
|
218
291
|
pRes.on('data', c => chunks.push(c));
|
|
219
292
|
pRes.on('end', async () => {
|
|
220
293
|
try {
|
|
221
|
-
const html
|
|
222
|
-
const out
|
|
294
|
+
const html = await decode(pRes.headers, chunks);
|
|
295
|
+
const out = Buffer.from(injectOverlay(html), 'utf8');
|
|
223
296
|
const headers = { ...pRes.headers };
|
|
224
297
|
delete headers['content-encoding'];
|
|
225
298
|
headers['content-length'] = out.length;
|
|
226
|
-
headers['cache-control']
|
|
299
|
+
headers['cache-control'] = 'no-store';
|
|
227
300
|
res.writeHead(pRes.statusCode, headers);
|
|
228
301
|
res.end(out);
|
|
229
|
-
} catch(e) {
|
|
302
|
+
} catch (e) {
|
|
230
303
|
res.writeHead(500); res.end('Draply error: ' + e.message);
|
|
231
304
|
}
|
|
232
305
|
});
|
|
@@ -237,7 +310,7 @@ ${props}
|
|
|
237
310
|
});
|
|
238
311
|
|
|
239
312
|
pReq.on('error', () => {
|
|
240
|
-
res.writeHead(502, {'Content-Type':'text/html'});
|
|
313
|
+
res.writeHead(502, { 'Content-Type': 'text/html' });
|
|
241
314
|
res.end(`<!DOCTYPE html><html><body style="background:#0a0a0f;color:#e8e8f0;font-family:monospace;padding:60px;text-align:center">
|
|
242
315
|
<h2 style="color:#ff6b6b">⚠ Не могу достучаться до ${targetHost}:${targetPort}</h2>
|
|
243
316
|
<p style="color:#555;margin-top:16px">Убедись что dev сервер запущен, потом обнови страницу</p>
|