Skip to content

Commit 651533f

Browse files
committed
Expose proxy to watch notifier WebSocket server in server mode: /.__marp-cli-watch-notifier__/*
1 parent 08d73ec commit 651533f

File tree

4 files changed

+108
-16
lines changed

4 files changed

+108
-16
lines changed

src/converter.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { isReadable } from './utils/finder'
4141
import { png2jpegViaPuppeteer } from './utils/jpeg'
4242
import { pdfLib, setOutline } from './utils/pdf'
4343
import { translateWSLPathToWindows } from './utils/wsl'
44-
import { notifier } from './watcher'
44+
import { notifier, type WatchNotifierEntrypointType } from './watcher'
4545

4646
const CREATED_BY_MARP = 'Created by Marp'
4747

@@ -93,7 +93,7 @@ export interface ConverterOption {
9393
templateOption?: TemplateOption
9494
themeSet: ThemeSet
9595
type: ConvertType
96-
watch: boolean
96+
watch: boolean | WatchNotifierEntrypointType
9797
}
9898

9999
export interface ConvertFileOption {
@@ -184,7 +184,12 @@ export class Converter {
184184
base: await resolveBase(file),
185185
notifyWS:
186186
isFile(file) && this.options.watch && type === ConvertType.html
187-
? await notifier.register(file.absolutePath)
187+
? await notifier.register(
188+
file.absolutePath,
189+
typeof this.options.watch === 'string'
190+
? this.options.watch
191+
: undefined
192+
)
188193
: undefined,
189194
renderer: async (tplOpts) => {
190195
const engine = await this.generateEngine(tplOpts)

src/server.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import { CLIError, CLIErrorCode, error, isError } from './error'
2020
import { File, markdownExtensions } from './file'
2121
import serverIndex from './server/index.pug'
2222
import style from './server/index.scss'
23+
import { notifier } from './watcher'
24+
25+
export const watchNotifierWebSocketEntrypoint = '.__marp-cli-watch-notifier__'
2326

2427
export class Server extends (EventEmitter as new () => TypedEmitter<Server.Events>) {
2528
readonly converter: Converter
@@ -69,6 +72,20 @@ export class Server extends (EventEmitter as new () => TypedEmitter<Server.Event
6972
}
7073
})()
7174
)
75+
76+
this.httpServer.on('upgrade', (request, socket, head) => {
77+
if (request.url?.startsWith(`/${watchNotifierWebSocketEntrypoint}/`)) {
78+
const ws = notifier.server
79+
80+
if (ws) {
81+
ws.handleUpgrade(request, socket, head, (client) => {
82+
ws.emit('connection', client, request)
83+
})
84+
return
85+
}
86+
}
87+
socket.destroy()
88+
})
7289
})
7390
}
7491

@@ -105,6 +122,7 @@ export class Server extends (EventEmitter as new () => TypedEmitter<Server.Event
105122
this.converter.options.output = false
106123
this.converter.options.pages = false
107124
this.converter.options.type = type
125+
this.converter.options.watch = 'server'
108126

109127
const result = await this.converter.convertFile(new File(filename))
110128
this.emit('converted', result)
@@ -179,6 +197,9 @@ export class Server extends (EventEmitter as new () => TypedEmitter<Server.Event
179197

180198
this.server = express.default()
181199
this.server
200+
.get(`/${watchNotifierWebSocketEntrypoint}/*all`, (_, res) => {
201+
res.status(426).end('Upgrade Required')
202+
})
182203
.get('*all', (req, res, next) =>
183204
this.preprocess(req, res).then(() => {
184205
if (!res.writableEnded) next()

src/utils/debug.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export const debugBrowserFinder = dbg('marp-cli:browser:finder')
77
export const debugEngine = dbg('marp-cli:engine')
88
export const debugPreview = dbg('marp-cli:preview')
99
export const debugWatcher = dbg('marp-cli:watcher')
10+
export const debugWatcherWS = dbg('marp-cli:watcher:ws')

src/watcher.ts

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type { ServerOptions } from 'ws'
88
import { Converter, ConvertedCallback } from './converter'
99
import { isError } from './error'
1010
import { File, FileType } from './file'
11-
import { debugWatcher } from './utils/debug'
11+
import { watchNotifierWebSocketEntrypoint } from './server'
12+
import { debugWatcher, debugWatcherWS } from './utils/debug'
1213

1314
const chokidarWatch: typeof _watch = (...args) => {
1415
debugWatcher('Start watching with chokidar: %O', args)
@@ -91,26 +92,47 @@ export class Watcher {
9192
}
9293
}
9394

95+
export type WatchNotifierEntrypointType = 'static' | 'server'
96+
9497
export class WatchNotifier {
9598
listeners = new Map<string, Set<any>>()
9699

97100
private wss?: WebSocketServer
98101
private portNumber?: number
99102

103+
get server() {
104+
return this.wss
105+
}
106+
100107
async port() {
101108
if (this.portNumber === undefined)
102109
this.portNumber = await getPortPromise({ port: 37717 })
103110

104111
return this.portNumber
105112
}
106113

107-
async register(fn: string) {
114+
async register(
115+
fn: string,
116+
entrypointType: WatchNotifierEntrypointType = 'static'
117+
) {
108118
const identifier = WatchNotifier.sha256(fn)
109119

110120
if (!this.listeners.has(identifier))
111121
this.listeners.set(identifier, new Set())
112122

113-
return `ws://localhost:${await this.port()}/${identifier}`
123+
return await this.entrypoint(identifier, entrypointType)
124+
}
125+
126+
async entrypoint(
127+
identifier: string,
128+
entrypointType: WatchNotifierEntrypointType = 'static'
129+
) {
130+
if (entrypointType === 'server') {
131+
return `/${watchNotifierWebSocketEntrypoint}/${identifier}`
132+
}
133+
134+
const port = await this.port()
135+
return `ws://localhost:${port}/${identifier}`
114136
}
115137

116138
sendTo(fn: string, command: string) {
@@ -124,21 +146,64 @@ export class WatchNotifier {
124146
}
125147

126148
async start(opts: ServerOptions = {}) {
127-
this.wss = new WebSocketServer({ ...opts, port: await this.port() })
128-
this.wss.on('connection', (ws, sock) => {
129-
if (sock.url) {
130-
const [, identifier] = sock.url.split('/')
131-
const wsSet = this.listeners.get(identifier)
149+
const port = await this.port()
132150

133-
if (wsSet !== undefined) {
134-
this.listeners.set(identifier, wsSet.add(ws))
151+
this.wss = new WebSocketServer({ ...opts, port })
135152

136-
ws.on('close', () => this.listeners.get(identifier)!.delete(ws))
153+
debugWatcherWS(
154+
'WebSocket server for watch notifier started on port %d.',
155+
port
156+
)
137157

138-
ws.send('ready')
139-
return
158+
this.wss.on('connection', (ws, sock) => {
159+
if (sock.url) {
160+
debugWatcherWS('New WebSocket connection: %s', sock.url)
161+
162+
const identifier = (() => {
163+
try {
164+
const parsedUrl = new URL(sock.url, `ws://localhost:${port}`)
165+
const detectedIdentifier = parsedUrl.pathname.split('/').pop()
166+
167+
debugWatcherWS(
168+
'Detected identifier from WebSocket connection: %s',
169+
detectedIdentifier
170+
)
171+
172+
return detectedIdentifier
173+
} catch (e: unknown) {
174+
debugWatcherWS('Error occurred during parsing identifier: %o', e)
175+
return undefined
176+
}
177+
})()
178+
179+
if (identifier) {
180+
const wsSet = this.listeners.get(identifier)
181+
182+
if (wsSet !== undefined) {
183+
this.listeners.set(identifier, wsSet.add(ws))
184+
debugWatcherWS(
185+
'WebSocket connection for identifier "%s" registered',
186+
identifier
187+
)
188+
189+
ws.on('close', () => {
190+
this.listeners.get(identifier)!.delete(ws)
191+
debugWatcherWS(
192+
'WebSocket connection for identifier "%s" closed',
193+
identifier
194+
)
195+
})
196+
197+
ws.send('ready')
198+
return
199+
}
140200
}
141201
}
202+
203+
debugWatcherWS(
204+
'WebSocket connection request has been dismissed: %s',
205+
sock.url
206+
)
142207
ws.close()
143208
})
144209
}

0 commit comments

Comments
 (0)