vimd 0.2.3 → 0.3.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/README.md CHANGED
@@ -68,6 +68,8 @@
68
68
  npm install -g vimd
69
69
  ```
70
70
 
71
+ **現在の安定版: v0.3.0**
72
+
71
73
  v0.2.0 からは **pandoc なしで利用可能** になりました。
72
74
  高品質な出力が必要な場合のみ pandoc をインストールしてください。
73
75
 
@@ -99,6 +101,21 @@ vimd config
99
101
 
100
102
  ---
101
103
 
104
+ ## バージョン情報
105
+
106
+ **安定版: v0.3.0**(最新版と同一)
107
+
108
+ v0.3.0 では内部アーキテクチャを刷新し、パフォーマンスを改善しました:
109
+ - WebSocket 直接通信によるライブリロード(live-server 依存を削除)
110
+ - バンドルサイズ 83% 削減
111
+
112
+ 安定版のインストール:
113
+ ```bash
114
+ npm install -g vimd@latest
115
+ ```
116
+
117
+ ---
118
+
102
119
  ## コマンド
103
120
 
104
121
  | コマンド | 説明 |
@@ -180,6 +197,7 @@ export default {
180
197
  - [APIリファレンス](docs/ja/api.md) - 詳細なオプション
181
198
  - [テスト](docs/ja/testing.md) - テスト構成
182
199
  - [トラブルシューティング](docs/ja/troubleshooting.md) - よくある問題
200
+ - [v0.3.0 リリースノート](docs/ja/releases/v0.3.0.md) - WebSocket Direct Communication
183
201
  - [v0.2.0 リリースノート](docs/ja/releases/v0.2.0.md) - Dual Parser System
184
202
 
185
203
  ---
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAaA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CA6Jf"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAcA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAwKf"}
@@ -2,7 +2,7 @@
2
2
  import { ConfigLoader } from '../../config/loader.js';
3
3
  import { FileWatcher } from '../../core/watcher.js';
4
4
  import { MarkdownConverter } from '../../core/converter.js';
5
- import { LiveServer } from '../../core/server.js';
5
+ import { WebSocketServer } from '../../core/websocket-server.js';
6
6
  import { PandocDetector } from '../../core/pandoc-detector.js';
7
7
  import { ParserFactory } from '../../core/parser/index.js';
8
8
  import { Logger } from '../../utils/logger.js';
@@ -10,6 +10,7 @@ import { ProcessManager } from '../../utils/process-manager.js';
10
10
  import { SessionManager } from '../../utils/session-manager.js';
11
11
  import * as path from 'path';
12
12
  import fs from 'fs-extra';
13
+ import open from 'open';
13
14
  export async function devCommand(filePath, options) {
14
15
  try {
15
16
  Logger.info('Starting vimd dev...');
@@ -80,19 +81,29 @@ export async function devCommand(filePath, options) {
80
81
  const html = await converter.convertWithTemplate(absolutePath);
81
82
  await converter.writeHTML(html, htmlPath);
82
83
  Logger.success('Conversion complete');
83
- // 11. Start live server from source directory
84
- const server = new LiveServer({
84
+ // 11. Start WebSocket server from source directory
85
+ const server = new WebSocketServer({
85
86
  port: port,
86
87
  host: config.host,
87
- open: config.open,
88
88
  root: sourceDir,
89
89
  });
90
- const startResult = await server.start(htmlPath);
91
- // Update port if live-server used a different one
90
+ const startResult = await server.start();
91
+ // Update port if server used a different one
92
92
  const actualPort = startResult.actualPort;
93
93
  if (startResult.portChanged) {
94
94
  port = actualPort;
95
95
  }
96
+ // 11.5. Open browser if configured
97
+ if (config.open) {
98
+ const url = `http://${config.host}:${actualPort}/${htmlFileName}`;
99
+ try {
100
+ await open(url);
101
+ Logger.info('Browser opened');
102
+ }
103
+ catch {
104
+ Logger.warn('Failed to open browser automatically');
105
+ }
106
+ }
96
107
  // 12. Save session with actual port
97
108
  await SessionManager.saveSession({
98
109
  pid: process.pid,
@@ -110,6 +121,7 @@ export async function devCommand(filePath, options) {
110
121
  try {
111
122
  const newHtml = await converter.convertWithTemplate(changedPath);
112
123
  await converter.writeHTML(newHtml, htmlPath);
124
+ server.broadcast('reload');
113
125
  Logger.success('Reconversion complete');
114
126
  }
115
127
  catch (error) {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WebSocketServer options
3
+ * Note: 'open' property is not included (handled by dev.ts)
4
+ */
5
+ export interface WebSocketServerOptions {
6
+ port: number;
7
+ root: string;
8
+ host?: string;
9
+ }
10
+ /**
11
+ * Result of server start operation
12
+ * Same interface as previous LiveServer for compatibility
13
+ */
14
+ export interface ServerStartResult {
15
+ actualPort: number;
16
+ requestedPort: number;
17
+ portChanged: boolean;
18
+ }
19
+ /**
20
+ * WebSocket server for live reload functionality
21
+ * Replaces live-server with direct WebSocket control
22
+ */
23
+ export declare class WebSocketServer {
24
+ private httpServer;
25
+ private wsServer;
26
+ private clients;
27
+ private options;
28
+ private _port;
29
+ constructor(options: WebSocketServerOptions);
30
+ /**
31
+ * Get the actual port the server is running on
32
+ */
33
+ get port(): number;
34
+ /**
35
+ * Start the HTTP and WebSocket servers
36
+ */
37
+ start(): Promise<ServerStartResult>;
38
+ /**
39
+ * Stop the server
40
+ */
41
+ stop(): Promise<void>;
42
+ /**
43
+ * Broadcast a message to all connected clients
44
+ */
45
+ broadcast(type: string, data?: unknown): void;
46
+ /**
47
+ * Inject reload script into HTML content
48
+ */
49
+ private injectReloadScript;
50
+ }
51
+ //# sourceMappingURL=websocket-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-server.d.ts","sourceRoot":"","sources":["../../src/core/websocket-server.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AA0CD;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,KAAK,CAAS;gBAEV,OAAO,EAAE,sBAAsB;IAQ3C;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAwGzC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B3B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI;IAU7C;;OAEG;IACH,OAAO,CAAC,kBAAkB;CAc3B"}
@@ -0,0 +1,208 @@
1
+ // src/core/websocket-server.ts
2
+ import http from 'http';
3
+ import polka from 'polka';
4
+ import sirv from 'sirv';
5
+ import { WebSocketServer as WSServer, WebSocket } from 'ws';
6
+ import { Logger } from '../utils/logger.js';
7
+ import { SessionManager } from '../utils/session-manager.js';
8
+ /**
9
+ * WebSocket reload script injected into HTML
10
+ * Features:
11
+ * - Reconnection with max 5 retries (prevents infinite loop)
12
+ * - Protocol detection for future HTTPS support
13
+ */
14
+ const RELOAD_SCRIPT = `
15
+ <script>
16
+ (function() {
17
+ var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
18
+ var ws = new WebSocket(protocol + '//' + location.host);
19
+
20
+ ws.onmessage = function(event) {
21
+ var data = JSON.parse(event.data);
22
+ if (data.type === 'reload') {
23
+ location.reload();
24
+ }
25
+ };
26
+
27
+ ws.onclose = function() {
28
+ var retries = 0;
29
+ var maxRetries = 5;
30
+
31
+ function reconnect() {
32
+ if (retries++ >= maxRetries) {
33
+ console.log('[vimd] Server disconnected. Please refresh manually.');
34
+ return;
35
+ }
36
+ setTimeout(function() {
37
+ var newWs = new WebSocket(protocol + '//' + location.host);
38
+ newWs.onopen = function() { location.reload(); };
39
+ newWs.onerror = reconnect;
40
+ }, 1000);
41
+ }
42
+ reconnect();
43
+ };
44
+ })();
45
+ </script>
46
+ `;
47
+ /**
48
+ * WebSocket server for live reload functionality
49
+ * Replaces live-server with direct WebSocket control
50
+ */
51
+ export class WebSocketServer {
52
+ constructor(options) {
53
+ this.httpServer = null;
54
+ this.wsServer = null;
55
+ this.clients = new Set();
56
+ this.options = {
57
+ host: 'localhost',
58
+ ...options,
59
+ };
60
+ this._port = options.port;
61
+ }
62
+ /**
63
+ * Get the actual port the server is running on
64
+ */
65
+ get port() {
66
+ return this._port;
67
+ }
68
+ /**
69
+ * Start the HTTP and WebSocket servers
70
+ */
71
+ async start() {
72
+ const requestedPort = this.options.port;
73
+ let actualPort = requestedPort;
74
+ // Check if port is available, find alternative if not
75
+ if (!(await SessionManager.isPortAvailable(requestedPort))) {
76
+ actualPort = await SessionManager.findAvailablePort(requestedPort + 1);
77
+ Logger.warn(`Port ${requestedPort} was unavailable, using port ${actualPort}`);
78
+ }
79
+ this._port = actualPort;
80
+ // Create static file server with sirv
81
+ const serve = sirv(this.options.root, {
82
+ dev: true, // Enable dev mode for no caching
83
+ });
84
+ // Create polka app with HTML injection middleware
85
+ const app = polka();
86
+ // Middleware to inject reload script into HTML
87
+ app.use((req, res, next) => {
88
+ // Only process GET requests for HTML files
89
+ if (req.method !== 'GET' || !req.url?.endsWith('.html')) {
90
+ return serve(req, res, next);
91
+ }
92
+ // For HTML files, we need to inject the script
93
+ // Override res.end to modify the response
94
+ const originalEnd = res.end.bind(res);
95
+ let body = '';
96
+ res.write = (chunk) => {
97
+ body += chunk.toString();
98
+ return true;
99
+ };
100
+ res.end = ((chunk) => {
101
+ if (chunk) {
102
+ body += chunk.toString();
103
+ }
104
+ // Inject reload script before </body> or </html>
105
+ const injectedBody = this.injectReloadScript(body);
106
+ // Update Content-Length header
107
+ const contentLength = Buffer.byteLength(injectedBody, 'utf8');
108
+ res.setHeader('Content-Length', contentLength);
109
+ return originalEnd(injectedBody);
110
+ });
111
+ serve(req, res, next);
112
+ });
113
+ // Create HTTP server
114
+ this.httpServer = http.createServer(app.handler);
115
+ // Create WebSocket server attached to HTTP server
116
+ this.wsServer = new WSServer({ server: this.httpServer });
117
+ // Handle WebSocket connections
118
+ this.wsServer.on('connection', (ws) => {
119
+ this.clients.add(ws);
120
+ Logger.info(`WebSocket client connected (${this.clients.size} total)`);
121
+ ws.on('close', () => {
122
+ this.clients.delete(ws);
123
+ Logger.info(`WebSocket client disconnected (${this.clients.size} remaining)`);
124
+ });
125
+ ws.on('error', (error) => {
126
+ Logger.warn(`WebSocket error: ${error.message}`);
127
+ this.clients.delete(ws);
128
+ });
129
+ });
130
+ // Start listening
131
+ await new Promise((resolve, reject) => {
132
+ const timeout = setTimeout(() => {
133
+ reject(new Error('Server start timeout'));
134
+ }, 10000);
135
+ this.httpServer.listen(actualPort, this.options.host, () => {
136
+ clearTimeout(timeout);
137
+ resolve();
138
+ });
139
+ this.httpServer.on('error', (err) => {
140
+ clearTimeout(timeout);
141
+ reject(err);
142
+ });
143
+ });
144
+ const url = `http://${this.options.host}:${actualPort}`;
145
+ Logger.success(`Server started at ${url}`);
146
+ return {
147
+ actualPort,
148
+ requestedPort,
149
+ portChanged: actualPort !== requestedPort,
150
+ };
151
+ }
152
+ /**
153
+ * Stop the server
154
+ */
155
+ async stop() {
156
+ // Close all WebSocket connections
157
+ for (const client of this.clients) {
158
+ try {
159
+ client.close();
160
+ }
161
+ catch {
162
+ // Ignore close errors
163
+ }
164
+ }
165
+ this.clients.clear();
166
+ // Close WebSocket server
167
+ if (this.wsServer) {
168
+ await new Promise((resolve) => {
169
+ this.wsServer.close(() => resolve());
170
+ });
171
+ this.wsServer = null;
172
+ }
173
+ // Close HTTP server
174
+ if (this.httpServer) {
175
+ await new Promise((resolve) => {
176
+ this.httpServer.close(() => resolve());
177
+ });
178
+ this.httpServer = null;
179
+ }
180
+ Logger.info('Server stopped');
181
+ }
182
+ /**
183
+ * Broadcast a message to all connected clients
184
+ */
185
+ broadcast(type, data) {
186
+ const message = JSON.stringify({ type, data });
187
+ for (const client of this.clients) {
188
+ if (client.readyState === WebSocket.OPEN) {
189
+ client.send(message);
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Inject reload script into HTML content
195
+ */
196
+ injectReloadScript(html) {
197
+ // Try to inject before </body>
198
+ if (html.includes('</body>')) {
199
+ return html.replace('</body>', `${RELOAD_SCRIPT}</body>`);
200
+ }
201
+ // Fallback: inject before </html>
202
+ if (html.includes('</html>')) {
203
+ return html.replace('</html>', `${RELOAD_SCRIPT}</html>`);
204
+ }
205
+ // Last resort: append to the end
206
+ return html + RELOAD_SCRIPT;
207
+ }
208
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export type { VimdConfig, ThemeInfo } from './config/types.js';
2
2
  export { defineConfig } from './config/types.js';
3
3
  export { MarkdownConverter } from './core/converter.js';
4
4
  export { FileWatcher } from './core/watcher.js';
5
- export { LiveServer } from './core/server.js';
5
+ export { WebSocketServer } from './core/websocket-server.js';
6
6
  export { PandocDetector } from './core/pandoc-detector.js';
7
7
  export { ThemeManager } from './themes/index.js';
8
8
  export { ConfigLoader } from './config/loader.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { defineConfig } from './config/types.js';
4
4
  // ==================== コア機能エクスポート ====================
5
5
  export { MarkdownConverter } from './core/converter.js';
6
6
  export { FileWatcher } from './core/watcher.js';
7
- export { LiveServer } from './core/server.js';
7
+ export { WebSocketServer } from './core/websocket-server.js';
8
8
  export { PandocDetector } from './core/pandoc-detector.js';
9
9
  // ==================== テーマ管理エクスポート ====================
10
10
  export { ThemeManager } from './themes/index.js';
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "vimd",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Real-time Markdown preview tool with pandoc (view markdown)",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "markdown",
8
8
  "preview",
9
- "live-server",
9
+ "live-reload",
10
10
  "pandoc",
11
11
  "cli",
12
- "viewer"
12
+ "viewer",
13
+ "websocket"
13
14
  ],
14
15
  "author": "notokeishou",
15
16
  "license": "MIT",
@@ -53,6 +54,8 @@
53
54
  "@types/inquirer": "^9.0.9",
54
55
  "@types/markdown-it": "^14.1.2",
55
56
  "@types/node": "^20.19.25",
57
+ "@types/polka": "^0.5.8",
58
+ "@types/ws": "^8.18.1",
56
59
  "@typescript-eslint/eslint-plugin": "^6.21.0",
57
60
  "@typescript-eslint/parser": "^6.21.0",
58
61
  "@vitest/coverage-v8": "^1.6.1",
@@ -70,10 +73,12 @@
70
73
  "github-markdown-css": "^5.8.1",
71
74
  "highlight.js": "^11.11.1",
72
75
  "inquirer": "^9.3.8",
73
- "live-server": "^1.2.2",
74
76
  "markdown-it": "^14.1.0",
75
77
  "markdown-it-strikethrough-alt": "^1.0.0",
76
78
  "markdown-it-task-lists": "^2.1.1",
77
- "open": "^9.1.0"
79
+ "open": "^9.1.0",
80
+ "polka": "^0.5.2",
81
+ "sirv": "^3.0.2",
82
+ "ws": "^8.18.3"
78
83
  }
79
84
  }