Skip to content

Commit af1a53e

Browse files
hyperpolymathclaude
andcommitted
feat: migrate coq-jr server from TypeScript to ReScript
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 34ec14d commit af1a53e

2 files changed

Lines changed: 170 additions & 96 deletions

File tree

coq-ecosystem/coq-jr/Server.res

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025 Coq-Jr Contributors
3+
//
4+
// Deno HTTP server for Coq-Jr
5+
// Migrated from server.ts to ReScript
6+
7+
/** Port the server listens on */
8+
let port = 8000
9+
10+
// --- Deno FFI bindings ---
11+
12+
/** Deno.readFile returns a promise of Uint8Array */
13+
@scope("Deno") @val
14+
external readFile: string => Js.Promise2.t<Js.TypedArray2.Uint8Array.t> = "readFile"
15+
16+
/** Response constructor options */
17+
type responseInit = {
18+
headers?: Js.Dict.t<string>,
19+
status?: int,
20+
}
21+
22+
/** Deno-compatible Response binding */
23+
@new
24+
external makeResponseFromText: (string, responseInit) => Webapi.Fetch.Response.t = "Response"
25+
26+
@new
27+
external makeResponseFromBuffer: (Js.TypedArray2.Uint8Array.t, responseInit) => Webapi.Fetch.Response.t =
28+
"Response"
29+
30+
/** Deno.serve options */
31+
type serveOptions = {port: int}
32+
33+
/** Deno.serve binding */
34+
@scope("Deno") @val
35+
external serve: (serveOptions, Webapi.Fetch.Request.t => Js.Promise2.t<Webapi.Fetch.Response.t>) => unit =
36+
"serve"
37+
38+
/** URL constructor for parsing request URLs */
39+
type url = {pathname: string}
40+
41+
@new external makeURL: string => url = "URL"
42+
43+
/** Request helpers */
44+
@get external requestUrl: Webapi.Fetch.Request.t => string = "url"
45+
@get external requestMethod: Webapi.Fetch.Request.t => string = "method"
46+
47+
// --- Dynamic import for compiled ReScript page renderer ---
48+
49+
/** Module shape returned by dynamic import of Main.res.js */
50+
type mainModule = {getPageHtml: unit => string}
51+
52+
@val external importModule: string => Js.Promise2.t<mainModule> = "import"
53+
54+
// --- MIME type lookup ---
55+
56+
/** Map of file extensions to MIME types */
57+
let mimeTypes: Js.Dict.t<string> = Js.Dict.fromArray([
58+
(".html", "text/html"),
59+
(".css", "text/css"),
60+
(".js", "application/javascript"),
61+
(".json", "application/json"),
62+
(".png", "image/png"),
63+
(".jpg", "image/jpeg"),
64+
(".jpeg", "image/jpeg"),
65+
(".gif", "image/gif"),
66+
(".svg", "image/svg+xml"),
67+
(".ico", "image/x-icon"),
68+
(".woff", "font/woff"),
69+
(".woff2", "font/woff2"),
70+
])
71+
72+
/** Extract MIME type from a file path based on its extension */
73+
let getMimeType = (path: string): string => {
74+
let lastDotIndex = Js.String2.lastIndexOf(path, ".")
75+
let ext = Js.String2.substr(path, ~from=lastDotIndex)
76+
switch Js.Dict.get(mimeTypes, ext) {
77+
| Some(mime) => mime
78+
| None => "application/octet-stream"
79+
}
80+
}
81+
82+
// --- Fallback HTML when ReScript sources are not yet compiled ---
83+
84+
/** Fallback page displayed when Main.res.js has not been built */
85+
let fallbackHtml = `<!DOCTYPE html>
86+
<html>
87+
<head>
88+
<meta charset="utf-8">
89+
<title>Coq-Jr - Build Required</title>
90+
<style>
91+
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
92+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
93+
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
94+
</style>
95+
</head>
96+
<body>
97+
<h1>Coq-Jr</h1>
98+
<p>The ReScript sources need to be compiled first.</p>
99+
<h2>Quick Start</h2>
100+
<pre><code>npm install
101+
npm run res:build
102+
deno task serve</code></pre>
103+
</body>
104+
</html>`
105+
106+
// --- Page renderer (loaded dynamically, with fallback) ---
107+
108+
/** Mutable reference holding the page HTML generator function.
109+
Initially set to fallback; replaced once Main.res.js loads. */
110+
let getPageHtml: ref<unit => string> = ref(() => fallbackHtml)
111+
112+
/** Attempt to load the compiled ReScript page renderer module */
113+
let _loadMainModule =
114+
importModule("./src/Main.res.js")
115+
->Js.Promise2.then(mod => {
116+
getPageHtml := mod.getPageHtml
117+
Js.Promise2.resolve()
118+
})
119+
->Js.Promise2.catch(_err => {
120+
// Main.res.js not compiled yet; fallback HTML remains active
121+
Js.Promise2.resolve()
122+
})
123+
124+
// --- Static file serving ---
125+
126+
/** Attempt to serve a static file from disk. Returns None on failure. */
127+
let serveStaticFile = (path: string): Js.Promise2.t<option<Webapi.Fetch.Response.t>> => {
128+
readFile(path)
129+
->Js.Promise2.then(file => {
130+
let headers = Js.Dict.fromArray([("content-type", getMimeType(path))])
131+
let response = makeResponseFromBuffer(file, {headers: headers})
132+
Js.Promise2.resolve(Some(response))
133+
})
134+
->Js.Promise2.catch(_err => {
135+
Js.Promise2.resolve(None)
136+
})
137+
}
138+
139+
// --- Request handler ---
140+
141+
/** Main request handler dispatching to index page, static files, or 404 */
142+
let handler = (request: Webapi.Fetch.Request.t): Js.Promise2.t<Webapi.Fetch.Response.t> => {
143+
let urlObj = makeURL(requestUrl(request))
144+
let pathname = urlObj.pathname
145+
146+
Js.log(`${requestMethod(request)} ${pathname}`)
147+
148+
// Serve index page
149+
if pathname == "/" || pathname == "/index.html" {
150+
let html = getPageHtml.contents()
151+
let headers = Js.Dict.fromArray([("content-type", "text/html; charset=utf-8")])
152+
Js.Promise2.resolve(makeResponseFromText(html, {headers: headers}))
153+
} else {
154+
// Try to serve static files
155+
let staticPath = "." ++ pathname
156+
serveStaticFile(staticPath)->Js.Promise2.then(maybeResponse => {
157+
switch maybeResponse {
158+
| Some(response) => Js.Promise2.resolve(response)
159+
| None =>
160+
// 404 for everything else
161+
Js.Promise2.resolve(makeResponseFromText("Not Found", {status: 404}))
162+
}
163+
})
164+
}
165+
}
166+
167+
// --- Server startup ---
168+
169+
Js.log(`Coq-Jr server running at http://localhost:${Belt.Int.toString(port)}/`)
170+
serve({port: port}, handler)

coq-ecosystem/coq-jr/server.ts

Lines changed: 0 additions & 96 deletions
This file was deleted.

0 commit comments

Comments
 (0)