1
1
import { watch , type FSWatcher } from "node:fs" ;
2
- import { readFile , stat } from "node:fs/promises" ;
2
+ import { readFile , readdir , stat } from "node:fs/promises" ;
3
3
import type { IncomingMessage , RequestListener } from "node:http" ;
4
4
import { createServer } from "node:http" ;
5
5
import { basename , dirname , extname , join , normalize } from "node:path" ;
@@ -9,7 +9,8 @@ import send from "send";
9
9
import { WebSocketServer , type WebSocket } from "ws" ;
10
10
import { HttpError , isHttpError , isNodeError } from "./error.js" ;
11
11
import { computeHash } from "./hash.js" ;
12
- import { renderPreview } from "./render.js" ;
12
+ import { type ParseResult , parseMarkdown } from "./markdown.js" ;
13
+ import { type RenderOptions , renderPreview } from "./render.js" ;
13
14
14
15
const DEFAULT_ROOT = "docs" ;
15
16
@@ -18,9 +19,9 @@ const publicRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "public")
18
19
class Server {
19
20
private _server : ReturnType < typeof createServer > ;
20
21
private _socketServer : WebSocketServer ;
21
- private readonly port : number ;
22
- private readonly hostname : string ;
23
- private readonly root : string ;
22
+ readonly port : number ;
23
+ readonly hostname : string ;
24
+ readonly root : string ;
24
25
25
26
constructor ( { port, hostname, root} : CommandContext ) {
26
27
this . port = port ;
@@ -33,9 +34,9 @@ class Server {
33
34
this . _socketServer . on ( "connection" , this . _handleConnection ) ;
34
35
}
35
36
36
- start ( ) {
37
- this . _server . listen ( this . port , this . hostname , ( ) => {
38
- console . log ( `Server running at http:// ${ this . hostname } : ${ this . port } /` ) ;
37
+ async start ( ) {
38
+ return new Promise < void > ( ( resolve ) => {
39
+ this . _server . listen ( this . port , this . hostname , resolve ) ;
39
40
} ) ;
40
41
}
41
42
@@ -75,7 +76,8 @@ class Server {
75
76
// Otherwise, serve the corresponding Markdown file, if it exists.
76
77
// Anything else should 404; static files should be matched above.
77
78
try {
78
- res . end ( renderPreview ( await readFile ( path + ".md" , "utf-8" ) ) . html ) ;
79
+ const pages = await this . _readPages ( ) ; // TODO cache
80
+ res . end ( renderPreview ( await readFile ( path + ".md" , "utf-8" ) , { path : pathname , pages} ) . html ) ;
79
81
} catch ( error ) {
80
82
if ( ! isNodeError ( error ) || error . code !== "ENOENT" ) throw error ; // internal error
81
83
throw new HttpError ( "Not found" , 404 ) ;
@@ -89,6 +91,24 @@ class Server {
89
91
}
90
92
} ;
91
93
94
+ async _readPages ( ) {
95
+ const pages : RenderOptions [ "pages" ] = [ ] ;
96
+ for ( const file of await readdir ( this . root ) ) {
97
+ if ( extname ( file ) !== ".md" ) continue ;
98
+ let parsed : ParseResult ;
99
+ try {
100
+ parsed = parseMarkdown ( await readFile ( join ( this . root , file ) , "utf-8" ) ) ;
101
+ } catch ( error ) {
102
+ if ( ! isNodeError ( error ) || error . code !== "ENOENT" ) throw error ; // internal error
103
+ continue ;
104
+ }
105
+ const page = { path : `/${ basename ( file , ".md" ) } ` , name : parsed . title ?? "Untitled" } ;
106
+ if ( page . path === "/index" ) pages . unshift ( page ) ;
107
+ else pages . push ( page ) ;
108
+ }
109
+ return pages ;
110
+ }
111
+
92
112
_handleConnection = ( socket : WebSocket , req : IncomingMessage ) => {
93
113
if ( req . url === "/_observablehq" ) {
94
114
handleWatch ( socket , this . root ) ;
@@ -190,5 +210,7 @@ function makeCommandContext(): CommandContext {
190
210
191
211
await ( async function ( ) {
192
212
const context = makeCommandContext ( ) ;
193
- new Server ( context ) . start ( ) ;
213
+ const server = new Server ( context ) ;
214
+ await server . start ( ) ;
215
+ console . log ( `Server running at http://${ server . hostname } :${ server . port } /` ) ;
194
216
} ) ( ) ;
0 commit comments