mdv-live 0.3.0 → 0.3.2

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.
@@ -2,62 +2,70 @@
2
2
  * File type detection and classification
3
3
  */
4
4
 
5
- // File extension to type mapping
5
+ // Helper functions to reduce repetition
6
+ function code(icon, lang) {
7
+ return { type: 'code', icon, lang, binary: false };
8
+ }
9
+
10
+ function binary(type, icon = type) {
11
+ return { type, icon, lang: null, binary: true };
12
+ }
13
+
6
14
  const FILE_TYPES = {
7
15
  // Markdown
8
16
  md: { type: 'markdown', icon: 'markdown', lang: null, binary: false },
9
17
  markdown: { type: 'markdown', icon: 'markdown', lang: null, binary: false },
10
18
 
11
19
  // Code - Python
12
- py: { type: 'code', icon: 'python', lang: 'python', binary: false },
13
- pyw: { type: 'code', icon: 'python', lang: 'python', binary: false },
20
+ py: code('python', 'python'),
21
+ pyw: code('python', 'python'),
14
22
 
15
23
  // Code - JavaScript/TypeScript
16
- js: { type: 'code', icon: 'javascript', lang: 'javascript', binary: false },
17
- mjs: { type: 'code', icon: 'javascript', lang: 'javascript', binary: false },
18
- cjs: { type: 'code', icon: 'javascript', lang: 'javascript', binary: false },
19
- ts: { type: 'code', icon: 'typescript', lang: 'typescript', binary: false },
20
- tsx: { type: 'code', icon: 'react', lang: 'tsx', binary: false },
21
- jsx: { type: 'code', icon: 'react', lang: 'jsx', binary: false },
24
+ js: code('javascript', 'javascript'),
25
+ mjs: code('javascript', 'javascript'),
26
+ cjs: code('javascript', 'javascript'),
27
+ ts: code('typescript', 'typescript'),
28
+ tsx: code('react', 'tsx'),
29
+ jsx: code('react', 'jsx'),
22
30
 
23
31
  // Code - Web
24
- html: { type: 'code', icon: 'html', lang: 'html', binary: false },
25
- htm: { type: 'code', icon: 'html', lang: 'html', binary: false },
26
- css: { type: 'code', icon: 'css', lang: 'css', binary: false },
27
- scss: { type: 'code', icon: 'css', lang: 'scss', binary: false },
28
- less: { type: 'code', icon: 'css', lang: 'less', binary: false },
29
- vue: { type: 'code', icon: 'vue', lang: 'vue', binary: false },
30
- svelte: { type: 'code', icon: 'default', lang: 'svelte', binary: false },
32
+ html: code('html', 'html'),
33
+ htm: code('html', 'html'),
34
+ css: code('css', 'css'),
35
+ scss: code('css', 'scss'),
36
+ less: code('css', 'less'),
37
+ vue: code('vue', 'vue'),
38
+ svelte: code('default', 'svelte'),
31
39
 
32
40
  // Data formats
33
- json: { type: 'code', icon: 'json', lang: 'json', binary: false },
34
- yaml: { type: 'code', icon: 'yaml', lang: 'yaml', binary: false },
35
- yml: { type: 'code', icon: 'yaml', lang: 'yaml', binary: false },
36
- toml: { type: 'code', icon: 'config', lang: 'toml', binary: false },
37
- xml: { type: 'code', icon: 'default', lang: 'xml', binary: false },
41
+ json: code('json', 'json'),
42
+ yaml: code('yaml', 'yaml'),
43
+ yml: code('yaml', 'yaml'),
44
+ toml: code('config', 'toml'),
45
+ xml: code('default', 'xml'),
38
46
 
39
47
  // Shell/Config
40
- sh: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
41
- bash: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
42
- zsh: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
43
- fish: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
44
- env: { type: 'code', icon: 'config', lang: 'bash', binary: false },
45
- ini: { type: 'code', icon: 'config', lang: 'ini', binary: false },
46
- conf: { type: 'code', icon: 'config', lang: 'ini', binary: false },
48
+ sh: code('shell', 'bash'),
49
+ bash: code('shell', 'bash'),
50
+ zsh: code('shell', 'bash'),
51
+ fish: code('shell', 'bash'),
52
+ env: code('config', 'bash'),
53
+ ini: code('config', 'ini'),
54
+ conf: code('config', 'ini'),
47
55
 
48
56
  // Other languages
49
- go: { type: 'code', icon: 'default', lang: 'go', binary: false },
50
- rs: { type: 'code', icon: 'default', lang: 'rust', binary: false },
51
- rb: { type: 'code', icon: 'default', lang: 'ruby', binary: false },
52
- php: { type: 'code', icon: 'default', lang: 'php', binary: false },
53
- java: { type: 'code', icon: 'default', lang: 'java', binary: false },
54
- kt: { type: 'code', icon: 'default', lang: 'kotlin', binary: false },
55
- swift: { type: 'code', icon: 'default', lang: 'swift', binary: false },
56
- c: { type: 'code', icon: 'default', lang: 'c', binary: false },
57
- cpp: { type: 'code', icon: 'default', lang: 'cpp', binary: false },
58
- h: { type: 'code', icon: 'default', lang: 'c', binary: false },
59
- cs: { type: 'code', icon: 'default', lang: 'csharp', binary: false },
60
- sql: { type: 'code', icon: 'database', lang: 'sql', binary: false },
57
+ go: code('default', 'go'),
58
+ rs: code('default', 'rust'),
59
+ rb: code('default', 'ruby'),
60
+ php: code('default', 'php'),
61
+ java: code('default', 'java'),
62
+ kt: code('default', 'kotlin'),
63
+ swift: code('default', 'swift'),
64
+ c: code('default', 'c'),
65
+ cpp: code('default', 'cpp'),
66
+ h: code('default', 'c'),
67
+ cs: code('default', 'csharp'),
68
+ sql: code('database', 'sql'),
61
69
 
62
70
  // Text files
63
71
  txt: { type: 'text', icon: 'text', lang: null, binary: false },
@@ -65,84 +73,85 @@ const FILE_TYPES = {
65
73
  csv: { type: 'text', icon: 'text', lang: null, binary: false },
66
74
 
67
75
  // Images
68
- png: { type: 'image', icon: 'image', lang: null, binary: true },
69
- jpg: { type: 'image', icon: 'image', lang: null, binary: true },
70
- jpeg: { type: 'image', icon: 'image', lang: null, binary: true },
71
- gif: { type: 'image', icon: 'image', lang: null, binary: true },
72
- svg: { type: 'image', icon: 'image', lang: null, binary: true },
73
- webp: { type: 'image', icon: 'image', lang: null, binary: true },
74
- ico: { type: 'image', icon: 'image', lang: null, binary: true },
76
+ png: binary('image'),
77
+ jpg: binary('image'),
78
+ jpeg: binary('image'),
79
+ gif: binary('image'),
80
+ svg: binary('image'),
81
+ webp: binary('image'),
82
+ ico: binary('image'),
75
83
 
76
84
  // PDF
77
- pdf: { type: 'pdf', icon: 'pdf', lang: null, binary: true },
85
+ pdf: binary('pdf'),
78
86
 
79
87
  // Video
80
- mp4: { type: 'video', icon: 'video', lang: null, binary: true },
81
- webm: { type: 'video', icon: 'video', lang: null, binary: true },
82
- mov: { type: 'video', icon: 'video', lang: null, binary: true },
83
- avi: { type: 'video', icon: 'video', lang: null, binary: true },
84
- mkv: { type: 'video', icon: 'video', lang: null, binary: true },
88
+ mp4: binary('video'),
89
+ webm: binary('video'),
90
+ mov: binary('video'),
91
+ avi: binary('video'),
92
+ mkv: binary('video'),
85
93
 
86
94
  // Audio
87
- mp3: { type: 'audio', icon: 'audio', lang: null, binary: true },
88
- wav: { type: 'audio', icon: 'audio', lang: null, binary: true },
89
- ogg: { type: 'audio', icon: 'audio', lang: null, binary: true },
90
- m4a: { type: 'audio', icon: 'audio', lang: null, binary: true },
91
- flac: { type: 'audio', icon: 'audio', lang: null, binary: true },
95
+ mp3: binary('audio'),
96
+ wav: binary('audio'),
97
+ ogg: binary('audio'),
98
+ m4a: binary('audio'),
99
+ flac: binary('audio'),
92
100
 
93
101
  // Archives
94
- zip: { type: 'archive', icon: 'archive', lang: null, binary: true },
95
- tar: { type: 'archive', icon: 'archive', lang: null, binary: true },
96
- gz: { type: 'archive', icon: 'archive', lang: null, binary: true },
97
- rar: { type: 'archive', icon: 'archive', lang: null, binary: true },
98
- '7z': { type: 'archive', icon: 'archive', lang: null, binary: true },
102
+ zip: binary('archive'),
103
+ tar: binary('archive'),
104
+ gz: binary('archive'),
105
+ rar: binary('archive'),
106
+ '7z': binary('archive'),
99
107
 
100
108
  // Office
101
- doc: { type: 'office', icon: 'office', lang: null, binary: true },
102
- docx: { type: 'office', icon: 'office', lang: null, binary: true },
103
- xls: { type: 'office', icon: 'office', lang: null, binary: true },
104
- xlsx: { type: 'office', icon: 'office', lang: null, binary: true },
105
- ppt: { type: 'office', icon: 'office', lang: null, binary: true },
106
- pptx: { type: 'office', icon: 'office', lang: null, binary: true },
109
+ doc: binary('office'),
110
+ docx: binary('office'),
111
+ xls: binary('office'),
112
+ xlsx: binary('office'),
113
+ ppt: binary('office'),
114
+ pptx: binary('office'),
107
115
 
108
116
  // Executables
109
- exe: { type: 'executable', icon: 'executable', lang: null, binary: true },
110
- dmg: { type: 'executable', icon: 'executable', lang: null, binary: true },
111
- app: { type: 'executable', icon: 'executable', lang: null, binary: true },
117
+ exe: binary('executable'),
118
+ dmg: binary('executable'),
119
+ app: binary('executable'),
112
120
  };
113
121
 
114
- // Special filenames
122
+ // Special filenames that don't follow extension-based detection
115
123
  const SPECIAL_FILES = {
116
- 'Dockerfile': { type: 'code', icon: 'config', lang: 'dockerfile', binary: false },
117
- 'Makefile': { type: 'code', icon: 'config', lang: 'makefile', binary: false },
118
- '.gitignore': { type: 'code', icon: 'config', lang: 'gitignore', binary: false },
119
- '.env': { type: 'code', icon: 'config', lang: 'bash', binary: false },
120
- '.env.local': { type: 'code', icon: 'config', lang: 'bash', binary: false },
121
- '.env.example': { type: 'code', icon: 'config', lang: 'bash', binary: false },
124
+ 'Dockerfile': code('config', 'dockerfile'),
125
+ 'Makefile': code('config', 'makefile'),
126
+ '.gitignore': code('config', 'gitignore'),
127
+ '.env': code('config', 'bash'),
128
+ '.env.local': code('config', 'bash'),
129
+ '.env.example': code('config', 'bash'),
122
130
  };
123
131
 
132
+ const DEFAULT_FILE_TYPE = { type: 'text', icon: 'text', lang: null, binary: false };
133
+
124
134
  /**
125
- * Get file type information
135
+ * Get file type information from filename or path
126
136
  * @param {string} filename - File name or path
127
- * @returns {Object} File type info { type, icon, lang, binary }
137
+ * @returns {{ type: string, icon: string, lang: string|null, binary: boolean }}
128
138
  */
129
139
  export function getFileType(filename) {
130
- const basename = filename.split('/').pop();
140
+ const basename = filename.split('/').pop() || '';
131
141
 
132
142
  // Check special filenames first
133
143
  if (SPECIAL_FILES[basename]) {
134
144
  return SPECIAL_FILES[basename];
135
145
  }
136
146
 
137
- // Get extension
138
- const ext = basename.split('.').pop()?.toLowerCase();
139
-
140
- if (ext && FILE_TYPES[ext]) {
141
- return FILE_TYPES[ext];
147
+ // Extract extension (last part after dot)
148
+ const lastDotIndex = basename.lastIndexOf('.');
149
+ if (lastDotIndex === -1 || lastDotIndex === basename.length - 1) {
150
+ return DEFAULT_FILE_TYPE;
142
151
  }
143
152
 
144
- // Default to text file
145
- return { type: 'text', icon: 'text', lang: null, binary: false };
153
+ const ext = basename.slice(lastDotIndex + 1).toLowerCase();
154
+ return FILE_TYPES[ext] || DEFAULT_FILE_TYPE;
146
155
  }
147
156
 
148
- export default { getFileType, FILE_TYPES };
157
+ export { FILE_TYPES };
package/src/utils/path.js CHANGED
@@ -5,19 +5,19 @@
5
5
  import path from 'path';
6
6
 
7
7
  /**
8
- * Validate that a path is within the allowed root directory
9
- * Prevents path traversal attacks
10
- * @param {string} targetPath - Path to validate (must be relative)
8
+ * Validate that a path is within the allowed root directory.
9
+ * Prevents path traversal attacks, null byte injection, and absolute path access.
10
+ * @param {string} targetPath - Relative path to validate
11
11
  * @param {string} rootDir - Allowed root directory
12
- * @returns {boolean} True if path is valid
12
+ * @returns {boolean} True if path is safe and within rootDir
13
13
  */
14
14
  export function validatePath(targetPath, rootDir) {
15
- // Reject null bytes (null byte injection attack)
15
+ // Reject null bytes (injection attack vector)
16
16
  if (targetPath.includes('\0') || targetPath.includes('%00')) {
17
17
  return false;
18
18
  }
19
19
 
20
- // Reject absolute paths (e.g., /etc/passwd)
20
+ // Reject absolute paths
21
21
  if (path.isAbsolute(targetPath)) {
22
22
  return false;
23
23
  }
@@ -27,22 +27,19 @@ export function validatePath(targetPath, rootDir) {
27
27
  return false;
28
28
  }
29
29
 
30
- const fullPath = path.join(rootDir, targetPath);
31
- const resolved = path.resolve(fullPath);
30
+ // Verify resolved path stays within root directory
31
+ const resolved = path.resolve(rootDir, targetPath);
32
32
  const resolvedRoot = path.resolve(rootDir);
33
33
 
34
- // Check if the resolved path starts with the root directory
35
34
  return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep);
36
35
  }
37
36
 
38
37
  /**
39
- * Get relative path from root
40
- * @param {string} fullPath - Full path
38
+ * Convert absolute path to relative path with forward slashes
39
+ * @param {string} fullPath - Absolute file path
41
40
  * @param {string} rootDir - Root directory
42
- * @returns {string} Relative path with forward slashes
41
+ * @returns {string} Relative path using forward slashes
43
42
  */
44
43
  export function getRelativePath(fullPath, rootDir) {
45
44
  return path.relative(rootDir, fullPath).split(path.sep).join('/');
46
45
  }
47
-
48
- export default { validatePath, getRelativePath };
package/src/watcher.js CHANGED
@@ -6,6 +6,30 @@ import chokidar from 'chokidar';
6
6
  import path from 'path';
7
7
  import { renderFile } from './rendering/index.js';
8
8
 
9
+ const IGNORED_PATTERNS = [
10
+ /(^|[\/\\])\../, // Dotfiles
11
+ /node_modules/,
12
+ /\.git/,
13
+ /__pycache__/,
14
+ /\.pyc$/,
15
+ /\.cache/,
16
+ /\.pytest_cache/,
17
+ /\.mypy_cache/,
18
+ /\.ruff_cache/,
19
+ /venv/,
20
+ /\.venv/,
21
+ /dist/,
22
+ /build/,
23
+ /\.next/,
24
+ /\.nuxt/,
25
+ /coverage/,
26
+ /\.DS_Store/,
27
+ /Thumbs\.db/,
28
+ /desktop\.ini/,
29
+ ];
30
+
31
+ const TREE_CHANGE_EVENTS = ['add', 'unlink', 'addDir', 'unlinkDir'];
32
+
9
33
  /**
10
34
  * Setup file watcher
11
35
  * @param {string} rootDir - Root directory to watch
@@ -14,32 +38,7 @@ import { renderFile } from './rendering/index.js';
14
38
  */
15
39
  export function setupWatcher(rootDir, wss) {
16
40
  const watcher = chokidar.watch(rootDir, {
17
- ignored: [
18
- /(^|[\/\\])\../, // Ignore dotfiles
19
- /node_modules/,
20
- /\.git/,
21
- /__pycache__/,
22
- /\.pyc$/,
23
- // Python cache/build
24
- /\.cache/,
25
- /\.pytest_cache/,
26
- /\.mypy_cache/,
27
- /\.ruff_cache/,
28
- /venv/,
29
- /\.venv/,
30
- // Build outputs
31
- /dist/,
32
- /build/,
33
- // Framework specific
34
- /\.next/,
35
- /\.nuxt/,
36
- // Test coverage
37
- /coverage/,
38
- // OS generated files
39
- /\.DS_Store/,
40
- /Thumbs\.db/,
41
- /desktop\.ini/,
42
- ],
41
+ ignored: IGNORED_PATTERNS,
43
42
  persistent: true,
44
43
  ignoreInitial: true,
45
44
  awaitWriteFinish: {
@@ -48,20 +47,22 @@ export function setupWatcher(rootDir, wss) {
48
47
  }
49
48
  });
50
49
 
51
- // Helper to get relative path
52
- const getRelativePath = (filePath) => {
50
+ function toRelativePath(filePath) {
53
51
  return path.relative(rootDir, filePath).split(path.sep).join('/');
54
- };
52
+ }
53
+
54
+ function broadcastTreeUpdate() {
55
+ wss.broadcast({
56
+ type: 'tree_update',
57
+ tree: null
58
+ });
59
+ }
55
60
 
56
- // File change handler
57
61
  watcher.on('change', async (filePath) => {
58
- const relativePath = getRelativePath(filePath);
62
+ const relativePath = toRelativePath(filePath);
59
63
 
60
64
  try {
61
- // Render the file content
62
65
  const rendered = await renderFile(filePath);
63
-
64
- // Broadcast to clients watching this file
65
66
  wss.broadcastFileUpdate(relativePath, {
66
67
  type: 'file_update',
67
68
  path: relativePath,
@@ -72,20 +73,9 @@ export function setupWatcher(rootDir, wss) {
72
73
  }
73
74
  });
74
75
 
75
- // Tree change handlers
76
- const broadcastTreeUpdate = async () => {
77
- // We'll implement getFileTree in api/tree.js
78
- // For now, just notify clients to refresh
79
- wss.broadcast({
80
- type: 'tree_update',
81
- tree: null // Client will fetch via API
82
- });
83
- };
84
-
85
- watcher.on('add', broadcastTreeUpdate);
86
- watcher.on('unlink', broadcastTreeUpdate);
87
- watcher.on('addDir', broadcastTreeUpdate);
88
- watcher.on('unlinkDir', broadcastTreeUpdate);
76
+ for (const event of TREE_CHANGE_EVENTS) {
77
+ watcher.on(event, broadcastTreeUpdate);
78
+ }
89
79
 
90
80
  watcher.on('error', (err) => {
91
81
  console.error('Watcher error:', err);
package/src/websocket.js CHANGED
@@ -2,7 +2,16 @@
2
2
  * WebSocket management for MDV
3
3
  */
4
4
 
5
- import { WebSocketServer } from 'ws';
5
+ import { WebSocketServer, WebSocket } from 'ws';
6
+
7
+ /**
8
+ * Check if a WebSocket client is ready to receive messages
9
+ * @param {WebSocket} client - WebSocket client
10
+ * @returns {boolean} True if client is open and ready
11
+ */
12
+ function isClientReady(client) {
13
+ return client.readyState === WebSocket.OPEN;
14
+ }
6
15
 
7
16
  /**
8
17
  * Setup WebSocket server
@@ -11,8 +20,6 @@ import { WebSocketServer } from 'ws';
11
20
  */
12
21
  export function setupWebSocket(server) {
13
22
  const wss = new WebSocketServer({ server });
14
-
15
- // Track watched files per client
16
23
  const clientWatches = new Map();
17
24
 
18
25
  wss.on('connection', (ws) => {
@@ -23,7 +30,6 @@ export function setupWebSocket(server) {
23
30
  const message = JSON.parse(data.toString());
24
31
 
25
32
  if (message.type === 'watch') {
26
- // Client wants to watch a file
27
33
  const watches = clientWatches.get(ws);
28
34
  watches.clear();
29
35
  watches.add(message.path);
@@ -43,30 +49,28 @@ export function setupWebSocket(server) {
43
49
  });
44
50
  });
45
51
 
46
- // Add broadcast helper
47
52
  wss.broadcast = (data) => {
48
53
  const message = JSON.stringify(data);
49
54
  wss.clients.forEach((client) => {
50
- if (client.readyState === 1) { // WebSocket.OPEN
55
+ if (isClientReady(client)) {
51
56
  client.send(message);
52
57
  }
53
58
  });
54
59
  };
55
60
 
56
- // Add targeted broadcast for file updates
57
61
  wss.broadcastFileUpdate = (filePath, data) => {
58
62
  const message = JSON.stringify(data);
59
63
  wss.clients.forEach((client) => {
60
- if (client.readyState === 1) {
61
- const watches = clientWatches.get(client);
62
- if (watches && watches.has(filePath)) {
63
- client.send(message);
64
- }
64
+ if (!isClientReady(client)) {
65
+ return;
66
+ }
67
+ const watches = clientWatches.get(client);
68
+ if (watches && watches.has(filePath)) {
69
+ client.send(message);
65
70
  }
66
71
  });
67
72
  };
68
73
 
69
- // Store clientWatches for external access
70
74
  wss.clientWatches = clientWatches;
71
75
 
72
76
  return wss;