draply-dev 1.0.1 → 1.1.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/bin/cli.js +178 -134
- package/package.json +1 -1
- package/src/draply-features.js +915 -0
- package/src/overlay.js +912 -903
package/bin/cli.js
CHANGED
|
@@ -2,24 +2,25 @@
|
|
|
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
|
+
const FEATURES_JS = fs.readFileSync(path.join(__dirname, '../src/draply-features.js'), 'utf8');
|
|
15
16
|
// Unique marker that does NOT appear inside overlay.js itself
|
|
16
|
-
const MARKER
|
|
17
|
-
const INJECT_TAG
|
|
17
|
+
const MARKER = 'data-ps-done="1"';
|
|
18
|
+
const INJECT_TAG = `\n<link rel="stylesheet" href="/draply.css">\n<script ${MARKER}>\n${OVERLAY_JS}\n</script>\n<script data-draply-features="1">\n${FEATURES_JS}\n</script>`;
|
|
18
19
|
|
|
19
20
|
function injectOverlay(html) {
|
|
20
21
|
if (html.includes(MARKER)) return html;
|
|
21
|
-
if (html.includes('</body>'))
|
|
22
|
-
if (html.includes('</html>'))
|
|
22
|
+
if (html.includes('</body>')) return html.replace(/<\/body>/i, INJECT_TAG + '\n</body>');
|
|
23
|
+
if (html.includes('</html>')) return html.replace(/<\/html>/i, INJECT_TAG + '\n</html>');
|
|
23
24
|
return html + INJECT_TAG;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -27,125 +28,32 @@ function decode(headers, chunks) {
|
|
|
27
28
|
const enc = headers['content-encoding'] || '';
|
|
28
29
|
const buf = Buffer.concat(chunks);
|
|
29
30
|
return new Promise((res, rej) => {
|
|
30
|
-
if (enc === 'gzip')
|
|
31
|
-
if (enc === 'deflate') return zlib.inflate(buf,
|
|
32
|
-
if (enc === 'br')
|
|
31
|
+
if (enc === 'gzip') return zlib.gunzip(buf, (e, d) => e ? rej(e) : res(d.toString('utf8')));
|
|
32
|
+
if (enc === 'deflate') return zlib.inflate(buf, (e, d) => e ? rej(e) : res(d.toString('utf8')));
|
|
33
|
+
if (enc === 'br') return zlib.brotliDecompress(buf, (e, d) => e ? rej(e) : res(d.toString('utf8')));
|
|
33
34
|
res(buf.toString('utf8'));
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
-
const projectRoot
|
|
39
|
-
const
|
|
38
|
+
// Храним CSS в скрытой папке .draply/ чтобы не засорять проект юзера
|
|
39
|
+
const projectRoot = process.cwd();
|
|
40
|
+
const draplyDir = path.join(projectRoot, '.draply');
|
|
41
|
+
if (!fs.existsSync(draplyDir)) {
|
|
42
|
+
fs.mkdirSync(draplyDir, { recursive: true });
|
|
43
|
+
// Добавляем .draply в .gitignore если он есть
|
|
44
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
45
|
+
if (fs.existsSync(gitignorePath)) {
|
|
46
|
+
const gi = fs.readFileSync(gitignorePath, 'utf8');
|
|
47
|
+
if (!gi.includes('.draply')) {
|
|
48
|
+
fs.appendFileSync(gitignorePath, '\n# Draply temp files\n.draply/\n', 'utf8');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const overridesPath = path.join(draplyDir, 'overrides.css');
|
|
40
53
|
if (!fs.existsSync(overridesPath)) {
|
|
41
54
|
fs.writeFileSync(overridesPath, '/* draply */\n', 'utf8');
|
|
42
55
|
}
|
|
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
56
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
95
|
-
|
|
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
|
-
}
|
|
109
|
-
|
|
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
|
-
}
|
|
121
|
-
|
|
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`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
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`);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
console.log(` \x1b[33m⚠\x1b[0m Не удалось определить фреймворк — добавь draply.css вручную`);
|
|
145
|
-
} catch (err) {
|
|
146
|
-
console.log(` \x1b[33m⚠\x1b[0m Ошибка авто-подключения: ${err.message}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
57
|
|
|
150
58
|
const server = http.createServer((req, res) => {
|
|
151
59
|
|
|
@@ -176,7 +84,6 @@ ${props}
|
|
|
176
84
|
}
|
|
177
85
|
const css = '/* draply — ' + new Date().toLocaleString('ru-RU') + ' */\n\n' + lines.join('\n\n') + '\n';
|
|
178
86
|
fs.writeFileSync(overridesPath, css, 'utf8');
|
|
179
|
-
autoInjectCSS();
|
|
180
87
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
181
88
|
res.end(JSON.stringify({ ok: true }));
|
|
182
89
|
} catch (e) {
|
|
@@ -188,27 +95,62 @@ ${props}
|
|
|
188
95
|
}
|
|
189
96
|
|
|
190
97
|
// ── Draply: Serve CSS ──────────────────────────────────────────────────────
|
|
191
|
-
if (req.url === '/draply.css') {
|
|
98
|
+
if (req.url.split('?')[0] === '/draply.css') {
|
|
99
|
+
const isModule = req.headers['sec-fetch-dest'] === 'script' || req.url.includes('import');
|
|
192
100
|
try {
|
|
193
101
|
const css = fs.readFileSync(overridesPath, 'utf8');
|
|
194
|
-
|
|
195
|
-
|
|
102
|
+
if (isModule) {
|
|
103
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript', 'cache-control': 'no-store' });
|
|
104
|
+
res.end(`const style = document.createElement('style');\nstyle.setAttribute('data-draply-module', '1');\nstyle.textContent = ${JSON.stringify(css)};\ndocument.head.appendChild(style);\nexport default {};`);
|
|
105
|
+
} else {
|
|
106
|
+
res.writeHead(200, { 'Content-Type': 'text/css', 'cache-control': 'no-store' });
|
|
107
|
+
res.end(css);
|
|
108
|
+
}
|
|
196
109
|
} catch (e) {
|
|
197
|
-
|
|
198
|
-
|
|
110
|
+
if (isModule) {
|
|
111
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
112
|
+
res.end('export default {};');
|
|
113
|
+
} else {
|
|
114
|
+
res.writeHead(200, { 'Content-Type': 'text/css' });
|
|
115
|
+
res.end('');
|
|
116
|
+
}
|
|
199
117
|
}
|
|
200
118
|
return;
|
|
201
119
|
}
|
|
202
120
|
|
|
121
|
+
// ── Draply: Project Info endpoint ──────────────────────────────────────────
|
|
122
|
+
if (req.url === '/draply-project-info' && req.method === 'GET') {
|
|
123
|
+
const info = detectProject(projectRoot);
|
|
124
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
125
|
+
res.end(JSON.stringify(info));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
203
128
|
|
|
129
|
+
// ── Draply: Find Source endpoint ──────────────────────────────────────────
|
|
130
|
+
if (req.url === '/draply-find-source' && req.method === 'POST') {
|
|
131
|
+
let body = '';
|
|
132
|
+
req.on('data', c => body += c);
|
|
133
|
+
req.on('end', () => {
|
|
134
|
+
try {
|
|
135
|
+
const { selector, tagName, className } = JSON.parse(body);
|
|
136
|
+
const result = findSourceFile(projectRoot, { selector, tagName, className });
|
|
137
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
138
|
+
res.end(JSON.stringify(result));
|
|
139
|
+
} catch (e) {
|
|
140
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
141
|
+
res.end(JSON.stringify({ found: false, error: e.message }));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
204
146
|
|
|
205
147
|
// ── Proxy to dev server ────────────────────────────────────────────────────
|
|
206
148
|
const opts = {
|
|
207
149
|
hostname: targetHost,
|
|
208
|
-
port:
|
|
209
|
-
path:
|
|
210
|
-
method:
|
|
211
|
-
headers:
|
|
150
|
+
port: targetPort,
|
|
151
|
+
path: req.url,
|
|
152
|
+
method: req.method,
|
|
153
|
+
headers: { ...req.headers, host: `${targetHost}:${targetPort}`, 'accept-encoding': 'identity' },
|
|
212
154
|
};
|
|
213
155
|
|
|
214
156
|
const pReq = http.request(opts, pRes => {
|
|
@@ -218,15 +160,15 @@ ${props}
|
|
|
218
160
|
pRes.on('data', c => chunks.push(c));
|
|
219
161
|
pRes.on('end', async () => {
|
|
220
162
|
try {
|
|
221
|
-
const html
|
|
222
|
-
const out
|
|
163
|
+
const html = await decode(pRes.headers, chunks);
|
|
164
|
+
const out = Buffer.from(injectOverlay(html), 'utf8');
|
|
223
165
|
const headers = { ...pRes.headers };
|
|
224
166
|
delete headers['content-encoding'];
|
|
225
167
|
headers['content-length'] = out.length;
|
|
226
|
-
headers['cache-control']
|
|
168
|
+
headers['cache-control'] = 'no-store';
|
|
227
169
|
res.writeHead(pRes.statusCode, headers);
|
|
228
170
|
res.end(out);
|
|
229
|
-
} catch(e) {
|
|
171
|
+
} catch (e) {
|
|
230
172
|
res.writeHead(500); res.end('Draply error: ' + e.message);
|
|
231
173
|
}
|
|
232
174
|
});
|
|
@@ -237,7 +179,7 @@ ${props}
|
|
|
237
179
|
});
|
|
238
180
|
|
|
239
181
|
pReq.on('error', () => {
|
|
240
|
-
res.writeHead(502, {'Content-Type':'text/html'});
|
|
182
|
+
res.writeHead(502, { 'Content-Type': 'text/html' });
|
|
241
183
|
res.end(`<!DOCTYPE html><html><body style="background:#0a0a0f;color:#e8e8f0;font-family:monospace;padding:60px;text-align:center">
|
|
242
184
|
<h2 style="color:#ff6b6b">⚠ Не могу достучаться до ${targetHost}:${targetPort}</h2>
|
|
243
185
|
<p style="color:#555;margin-top:16px">Убедись что dev сервер запущен, потом обнови страницу</p>
|
|
@@ -256,3 +198,105 @@ server.listen(proxyPort, () => {
|
|
|
256
198
|
});
|
|
257
199
|
|
|
258
200
|
process.on('SIGINT', () => { console.log('\n \x1b[90mDraply остановлен\x1b[0m\n'); process.exit(0); });
|
|
201
|
+
|
|
202
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
203
|
+
// PROJECT DETECTION
|
|
204
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
205
|
+
function detectProject(root) {
|
|
206
|
+
const result = { framework: 'unknown', cssStrategy: 'unknown', root };
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const pkgPath = path.join(root, 'package.json');
|
|
210
|
+
if (!fs.existsSync(pkgPath)) return result;
|
|
211
|
+
|
|
212
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
213
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
214
|
+
|
|
215
|
+
// Detect framework
|
|
216
|
+
if (allDeps['next']) result.framework = 'next';
|
|
217
|
+
else if (allDeps['react']) result.framework = 'react';
|
|
218
|
+
else if (allDeps['nuxt']) result.framework = 'nuxt';
|
|
219
|
+
else if (allDeps['vue']) result.framework = 'vue';
|
|
220
|
+
else if (allDeps['@angular/core']) result.framework = 'angular';
|
|
221
|
+
else if (allDeps['svelte']) result.framework = 'svelte';
|
|
222
|
+
else if (allDeps['vite']) result.framework = 'vite';
|
|
223
|
+
|
|
224
|
+
// Detect CSS strategy
|
|
225
|
+
if (allDeps['tailwindcss']) result.cssStrategy = 'tailwind';
|
|
226
|
+
else if (allDeps['styled-components']) result.cssStrategy = 'styled-components';
|
|
227
|
+
else if (allDeps['@emotion/react'] || allDeps['@emotion/styled']) result.cssStrategy = 'emotion';
|
|
228
|
+
else if (allDeps['sass'] || allDeps['node-sass']) result.cssStrategy = 'sass';
|
|
229
|
+
else {
|
|
230
|
+
// Check for CSS modules (usually enabled by default in React/Next)
|
|
231
|
+
if (['react', 'next'].includes(result.framework)) {
|
|
232
|
+
result.cssStrategy = 'css-modules';
|
|
233
|
+
} else {
|
|
234
|
+
result.cssStrategy = 'external';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch { /* ignore */ }
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
243
|
+
// SOURCE FILE FINDER
|
|
244
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
245
|
+
function findSourceFile(root, { selector, tagName, className }) {
|
|
246
|
+
const result = { found: false, file: null, line: null, hint: '' };
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const srcDirs = ['src', 'app', 'pages', 'components', 'lib'];
|
|
250
|
+
const extensions = ['.tsx', '.jsx', '.vue', '.svelte', '.js', '.ts'];
|
|
251
|
+
const searchTerm = className || tagName || '';
|
|
252
|
+
|
|
253
|
+
if (!searchTerm) {
|
|
254
|
+
result.hint = 'Select an element with a class name for better results.';
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const dir of srcDirs) {
|
|
259
|
+
const dirPath = path.join(root, dir);
|
|
260
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
261
|
+
|
|
262
|
+
const files = walkDir(dirPath, extensions);
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
try {
|
|
265
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
266
|
+
const lines = content.split('\n');
|
|
267
|
+
for (let i = 0; i < lines.length; i++) {
|
|
268
|
+
if (lines[i].includes(searchTerm)) {
|
|
269
|
+
result.found = true;
|
|
270
|
+
result.file = path.relative(root, file);
|
|
271
|
+
result.line = i + 1;
|
|
272
|
+
result.hint = `Found "${searchTerm}" at ${result.file}:${result.line}`;
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch { /* skip unreadable files */ }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
result.hint = `Could not find "${searchTerm}" in source files.`;
|
|
281
|
+
} catch (e) {
|
|
282
|
+
result.hint = 'Error searching source files: ' + e.message;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function walkDir(dir, extensions, results = []) {
|
|
289
|
+
try {
|
|
290
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
const fullPath = path.join(dir, entry.name);
|
|
293
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.next' || entry.name === 'dist') continue;
|
|
294
|
+
if (entry.isDirectory()) {
|
|
295
|
+
walkDir(fullPath, extensions, results);
|
|
296
|
+
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
297
|
+
results.push(fullPath);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch { /* ignore */ }
|
|
301
|
+
return results;
|
|
302
|
+
}
|