draply-dev 1.0.1 → 1.1.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 CHANGED
@@ -2,24 +2,25 @@
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
+ 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 = 'data-ps-done="1"';
17
- const INJECT_TAG = `\n<link rel="stylesheet" href="/draply.css">\n<script ${MARKER}>\n${OVERLAY_JS}\n</script>`;
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>')) return html.replace(/<\/body>/i, INJECT_TAG + '\n</body>');
22
- if (html.includes('</html>')) return html.replace(/<\/html>/i, INJECT_TAG + '\n</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') 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')));
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
- // Создаём пустой draply.css при старте если его нет
38
- const projectRoot = process.cwd();
39
- const overridesPath = path.join(projectRoot, 'draply.css');
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
- res.writeHead(200, { 'Content-Type': 'text/css', 'cache-control': 'no-store' });
195
- res.end(css);
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
- res.writeHead(200, { 'Content-Type': 'text/css' });
198
- res.end('');
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: targetPort,
209
- path: req.url,
210
- method: req.method,
211
- headers: { ...req.headers, host: `${targetHost}:${targetPort}`, 'accept-encoding': 'identity' },
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 = await decode(pRes.headers, chunks);
222
- const out = Buffer.from(injectOverlay(html), 'utf8');
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'] = 'no-store';
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draply-dev",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
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",