vimd 0.2.4 → 0.3.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/README.md CHANGED
@@ -68,7 +68,7 @@
68
68
  npm install -g vimd
69
69
  ```
70
70
 
71
- **現在の安定版: v0.2.4**
71
+ **現在の安定版: v0.3.0**
72
72
 
73
73
  v0.2.0 からは **pandoc なしで利用可能** になりました。
74
74
  高品質な出力が必要な場合のみ pandoc をインストールしてください。
@@ -103,10 +103,11 @@ vimd config
103
103
 
104
104
  ## バージョン情報
105
105
 
106
- **安定版: v0.2.4**(最新版と同一)
106
+ **安定版: v0.3.0**(最新版と同一)
107
107
 
108
- 安定版は十分なテストを経てリリースされたバージョンです。
109
- 最新版と安定版が異なる場合は、両方を記載します。
108
+ v0.3.0 では内部アーキテクチャを刷新し、パフォーマンスを改善しました:
109
+ - WebSocket 直接通信によるライブリロード(live-server 依存を削除)
110
+ - バンドルサイズ 83% 削減
110
111
 
111
112
  安定版のインストール:
112
113
  ```bash
@@ -196,6 +197,7 @@ export default {
196
197
  - [APIリファレンス](docs/ja/api.md) - 詳細なオプション
197
198
  - [テスト](docs/ja/testing.md) - テスト構成
198
199
  - [トラブルシューティング](docs/ja/troubleshooting.md) - よくある問題
200
+ - [v0.3.0 リリースノート](docs/ja/releases/v0.3.0.md) - WebSocket Direct Communication
199
201
  - [v0.2.0 リリースノート](docs/ja/releases/v0.2.0.md) - Dual Parser System
200
202
 
201
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":"AAUA;;;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;IA4GzC;;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,217 @@
1
+ // src/core/websocket-server.ts
2
+ import http from 'http';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import polka from 'polka';
6
+ import sirv from 'sirv';
7
+ import { WebSocketServer as WSServer, WebSocket } from 'ws';
8
+ import { Logger } from '../utils/logger.js';
9
+ import { SessionManager } from '../utils/session-manager.js';
10
+ /**
11
+ * WebSocket reload script injected into HTML
12
+ * Features:
13
+ * - Reconnection with max 5 retries (prevents infinite loop)
14
+ * - Protocol detection for future HTTPS support
15
+ */
16
+ const RELOAD_SCRIPT = `
17
+ <script>
18
+ (function() {
19
+ var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
20
+ var ws = new WebSocket(protocol + '//' + location.host);
21
+
22
+ ws.onmessage = function(event) {
23
+ var data = JSON.parse(event.data);
24
+ if (data.type === 'reload') {
25
+ location.reload();
26
+ }
27
+ };
28
+
29
+ ws.onclose = function() {
30
+ var retries = 0;
31
+ var maxRetries = 5;
32
+
33
+ function reconnect() {
34
+ if (retries++ >= maxRetries) {
35
+ console.log('[vimd] Server disconnected. Please refresh manually.');
36
+ return;
37
+ }
38
+ setTimeout(function() {
39
+ var newWs = new WebSocket(protocol + '//' + location.host);
40
+ newWs.onopen = function() { location.reload(); };
41
+ newWs.onerror = reconnect;
42
+ }, 1000);
43
+ }
44
+ reconnect();
45
+ };
46
+ })();
47
+ </script>
48
+ `;
49
+ /**
50
+ * WebSocket server for live reload functionality
51
+ * Replaces live-server with direct WebSocket control
52
+ */
53
+ export class WebSocketServer {
54
+ constructor(options) {
55
+ this.httpServer = null;
56
+ this.wsServer = null;
57
+ this.clients = new Set();
58
+ this.options = {
59
+ host: 'localhost',
60
+ ...options,
61
+ };
62
+ this._port = options.port;
63
+ }
64
+ /**
65
+ * Get the actual port the server is running on
66
+ */
67
+ get port() {
68
+ return this._port;
69
+ }
70
+ /**
71
+ * Start the HTTP and WebSocket servers
72
+ */
73
+ async start() {
74
+ const requestedPort = this.options.port;
75
+ let actualPort = requestedPort;
76
+ // Check if port is available, find alternative if not
77
+ if (!(await SessionManager.isPortAvailable(requestedPort))) {
78
+ actualPort = await SessionManager.findAvailablePort(requestedPort + 1);
79
+ Logger.warn(`Port ${requestedPort} was unavailable, using port ${actualPort}`);
80
+ }
81
+ this._port = actualPort;
82
+ // Create static file server with sirv
83
+ const serve = sirv(this.options.root, {
84
+ dev: true, // Enable dev mode for no caching
85
+ });
86
+ // Create polka app with HTML injection middleware
87
+ const app = polka();
88
+ // Middleware to inject reload script into HTML
89
+ // For HTML files, read directly to avoid sirv streaming issues
90
+ app.use((req, res, next) => {
91
+ // Only process GET requests for HTML files
92
+ if (req.method !== 'GET' || !req.url?.endsWith('.html')) {
93
+ return serve(req, res, next);
94
+ }
95
+ // Parse the URL and resolve the file path
96
+ const urlPath = req.url || '/index.html';
97
+ const filePath = path.join(this.options.root, urlPath);
98
+ // Check if file exists
99
+ if (!fs.existsSync(filePath)) {
100
+ // Let sirv handle 404
101
+ return serve(req, res, next);
102
+ }
103
+ try {
104
+ // Read HTML file directly
105
+ const html = fs.readFileSync(filePath, 'utf8');
106
+ // Inject reload script
107
+ const injectedHtml = this.injectReloadScript(html);
108
+ // Send response with proper headers
109
+ res.writeHead(200, {
110
+ 'Content-Type': 'text/html; charset=utf-8',
111
+ 'Content-Length': Buffer.byteLength(injectedHtml, 'utf8'),
112
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
113
+ });
114
+ res.end(injectedHtml);
115
+ }
116
+ catch (error) {
117
+ // On error, fall back to sirv
118
+ Logger.warn(`Failed to read HTML file: ${filePath}`);
119
+ serve(req, res, next);
120
+ }
121
+ });
122
+ // Create HTTP server
123
+ this.httpServer = http.createServer(app.handler);
124
+ // Create WebSocket server attached to HTTP server
125
+ this.wsServer = new WSServer({ server: this.httpServer });
126
+ // Handle WebSocket connections
127
+ this.wsServer.on('connection', (ws) => {
128
+ this.clients.add(ws);
129
+ Logger.info(`WebSocket client connected (${this.clients.size} total)`);
130
+ ws.on('close', () => {
131
+ this.clients.delete(ws);
132
+ Logger.info(`WebSocket client disconnected (${this.clients.size} remaining)`);
133
+ });
134
+ ws.on('error', (error) => {
135
+ Logger.warn(`WebSocket error: ${error.message}`);
136
+ this.clients.delete(ws);
137
+ });
138
+ });
139
+ // Start listening
140
+ await new Promise((resolve, reject) => {
141
+ const timeout = setTimeout(() => {
142
+ reject(new Error('Server start timeout'));
143
+ }, 10000);
144
+ this.httpServer.listen(actualPort, this.options.host, () => {
145
+ clearTimeout(timeout);
146
+ resolve();
147
+ });
148
+ this.httpServer.on('error', (err) => {
149
+ clearTimeout(timeout);
150
+ reject(err);
151
+ });
152
+ });
153
+ const url = `http://${this.options.host}:${actualPort}`;
154
+ Logger.success(`Server started at ${url}`);
155
+ return {
156
+ actualPort,
157
+ requestedPort,
158
+ portChanged: actualPort !== requestedPort,
159
+ };
160
+ }
161
+ /**
162
+ * Stop the server
163
+ */
164
+ async stop() {
165
+ // Close all WebSocket connections
166
+ for (const client of this.clients) {
167
+ try {
168
+ client.close();
169
+ }
170
+ catch {
171
+ // Ignore close errors
172
+ }
173
+ }
174
+ this.clients.clear();
175
+ // Close WebSocket server
176
+ if (this.wsServer) {
177
+ await new Promise((resolve) => {
178
+ this.wsServer.close(() => resolve());
179
+ });
180
+ this.wsServer = null;
181
+ }
182
+ // Close HTTP server
183
+ if (this.httpServer) {
184
+ await new Promise((resolve) => {
185
+ this.httpServer.close(() => resolve());
186
+ });
187
+ this.httpServer = null;
188
+ }
189
+ Logger.info('Server stopped');
190
+ }
191
+ /**
192
+ * Broadcast a message to all connected clients
193
+ */
194
+ broadcast(type, data) {
195
+ const message = JSON.stringify({ type, data });
196
+ for (const client of this.clients) {
197
+ if (client.readyState === WebSocket.OPEN) {
198
+ client.send(message);
199
+ }
200
+ }
201
+ }
202
+ /**
203
+ * Inject reload script into HTML content
204
+ */
205
+ injectReloadScript(html) {
206
+ // Try to inject before </body>
207
+ if (html.includes('</body>')) {
208
+ return html.replace('</body>', `${RELOAD_SCRIPT}</body>`);
209
+ }
210
+ // Fallback: inject before </html>
211
+ if (html.includes('</html>')) {
212
+ return html.replace('</html>', `${RELOAD_SCRIPT}</html>`);
213
+ }
214
+ // Last resort: append to the end
215
+ return html + RELOAD_SCRIPT;
216
+ }
217
+ }
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.4",
3
+ "version": "0.3.1",
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
  }