Cheng Shao pushed to branch wip/wasm-ghci-file-server at Glasgow Haskell Compiler / GHC Commits: c951fef1 by Cheng Shao at 2026-02-25T20:58:28+00:00 wasm: add /assets endpoint to serve user-specified assets This patch adds an `/assets` endpoint to the wasm dyld http server, so that users can also fetch assets from the same host with sensible default MIME types, without needing a separate http server for assets that also introduces CORS headaches: - A `-fghci-browser-assets-dir` driver flag is added to specify the assets root directory (defaults to `$PWD`) - The dyld http server fetches `mime-db` on demand and uses it as source of truth for mime types. Closes #26951. - - - - - 8 changed files: - compiler/GHC/Driver/Config/Interpreter.hs - compiler/GHC/Driver/DynFlags.hs - compiler/GHC/Driver/Session.hs - compiler/GHC/Runtime/Interpreter/Init.hs - compiler/GHC/Runtime/Interpreter/Types.hs - compiler/GHC/Runtime/Interpreter/Wasm.hs - docs/users_guide/wasm.rst - utils/jsffi/dyld.mjs Changes: ===================================== compiler/GHC/Driver/Config/Interpreter.hs ===================================== @@ -30,6 +30,7 @@ initInterpOpts dflags = do , interpBrowser = gopt Opt_GhciBrowser dflags , interpBrowserHost = ghciBrowserHost dflags , interpBrowserPort = ghciBrowserPort dflags + , interpBrowserAssetsDir = ghciBrowserAssetsDir dflags , interpBrowserRedirectWasiConsole = gopt Opt_GhciBrowserRedirectWasiConsole dflags , interpBrowserPuppeteerLaunchOpts = ghciBrowserPuppeteerLaunchOpts dflags , interpBrowserPlaywrightBrowserType = ghciBrowserPlaywrightBrowserType dflags @@ -43,4 +44,3 @@ initInterpOpts dflags = do , interpCcConfig = configureCc dflags , interpExecutableLinkOpts = initExecutableLinkOpts dflags Dynamic } - ===================================== compiler/GHC/Driver/DynFlags.hs ===================================== @@ -426,6 +426,7 @@ data DynFlags = DynFlags { -- wasm ghci browser mode ghciBrowserHost :: !String, ghciBrowserPort :: !Int, + ghciBrowserAssetsDir :: !(Maybe FilePath), ghciBrowserPuppeteerLaunchOpts :: !(Maybe String), ghciBrowserPlaywrightBrowserType :: !(Maybe String), ghciBrowserPlaywrightLaunchOpts :: !(Maybe String), @@ -727,6 +728,7 @@ defaultDynFlags mySettings = ghciBrowserHost = "127.0.0.1", ghciBrowserPort = 0, + ghciBrowserAssetsDir = Nothing, ghciBrowserPuppeteerLaunchOpts = Nothing, ghciBrowserPlaywrightBrowserType = Nothing, ghciBrowserPlaywrightLaunchOpts = Nothing, ===================================== compiler/GHC/Driver/Session.hs ===================================== @@ -1896,6 +1896,8 @@ dynamic_flags_deps = [ $ hasArg $ \f d -> d { ghciBrowserHost = f } , make_ord_flag defGhciFlag "fghci-browser-port" $ intSuffix $ \n d -> d { ghciBrowserPort = n } + , make_ord_flag defGhciFlag "fghci-browser-assets-dir" + $ hasArg $ \f d -> d { ghciBrowserAssetsDir = Just f } , make_ord_flag defGhciFlag "fghci-browser-puppeteer-launch-opts" $ hasArg $ \f d -> d { ghciBrowserPuppeteerLaunchOpts = Just f } , make_ord_flag defGhciFlag "fghci-browser-playwright-browser-type" ===================================== compiler/GHC/Runtime/Interpreter/Init.hs ===================================== @@ -49,6 +49,7 @@ data InterpOpts = InterpOpts , interpBrowser :: Bool , interpBrowserHost :: String , interpBrowserPort :: Int + , interpBrowserAssetsDir :: !(Maybe FilePath) , interpBrowserRedirectWasiConsole :: Bool , interpBrowserPuppeteerLaunchOpts :: Maybe String , interpBrowserPlaywrightBrowserType :: Maybe String @@ -89,6 +90,7 @@ initInterpreter dflags tmpfs logger platform finder_cache unit_env opts = do , wasmInterpBrowser = interpBrowser opts , wasmInterpBrowserHost = interpBrowserHost opts , wasmInterpBrowserPort = interpBrowserPort opts + , wasmInterpBrowserAssetsDir = interpBrowserAssetsDir opts , wasmInterpBrowserRedirectWasiConsole = interpBrowserRedirectWasiConsole opts , wasmInterpBrowserPuppeteerLaunchOpts = interpBrowserPuppeteerLaunchOpts opts , wasmInterpBrowserPlaywrightBrowserType = interpBrowserPlaywrightBrowserType opts ===================================== compiler/GHC/Runtime/Interpreter/Types.hs ===================================== @@ -220,6 +220,7 @@ data WasmInterpConfig = WasmInterpConfig , wasmInterpBrowser :: !Bool , wasmInterpBrowserHost :: !String , wasmInterpBrowserPort :: !Int + , wasmInterpBrowserAssetsDir :: !(Maybe FilePath) , wasmInterpBrowserRedirectWasiConsole :: !Bool , wasmInterpBrowserPuppeteerLaunchOpts :: !(Maybe String) , wasmInterpBrowserPlaywrightBrowserType :: !(Maybe String) ===================================== compiler/GHC/Runtime/Interpreter/Wasm.hs ===================================== @@ -52,6 +52,7 @@ spawnWasmInterp WasmInterpConfig {..} = do let dyld_env = [("GHCI_BROWSER", "1") | wasmInterpBrowser] ++ [("GHCI_BROWSER_HOST", wasmInterpBrowserHost), ("GHCI_BROWSER_PORT", show wasmInterpBrowserPort)] + ++ [("GHCI_BROWSER_ASSETS_DIR", f) | f <- maybeToList wasmInterpBrowserAssetsDir] ++ [("GHCI_BROWSER_REDIRECT_WASI_CONSOLE", "1") | wasmInterpBrowserRedirectWasiConsole] ++ [("GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS", f) | f <- maybeToList wasmInterpBrowserPuppeteerLaunchOpts] ++ [("GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE", f) | f <- maybeToList wasmInterpBrowserPlaywrightBrowserType] ===================================== docs/users_guide/wasm.rst ===================================== @@ -193,6 +193,18 @@ See below for other optional GHC flags of wasm ghci browser mode: Specify the port that the ``dyld`` HTTP server should listen on. Defaults to a random idle port. +.. ghc-flag:: -fghci-browser-assets-dir + :shortdesc: User-specified assets root directory + :type: dynamic + + :default: ``$PWD`` + + The HTTP server also exposes an ``/assets`` endpoint that allows + the users to fetch custom assets with sensible default MIME type, + e.g. `http://127.0.0.1:8080/assets/index.html` would fetch + `index.html` in the assets root directory with ``text/html`` MIME + type. + .. ghc-flag:: -fghci-browser-redirect-wasi-console :shortdesc: Redirect wasi console stdout/stderr back to host ghci. :type: dynamic ===================================== utils/jsffi/dyld.mjs ===================================== @@ -560,6 +560,7 @@ export class DyLDRPC { // Actual implementation of endpoints used by DyLDRPC class DyLDRPCServer { + #mimeDb; #dyldHost; #server; #wss; @@ -567,6 +568,7 @@ class DyLDRPCServer { constructor({ host, port, + assetsDir, dyldPath, searchDirs, mainSoPath, @@ -575,6 +577,20 @@ class DyLDRPCServer { args, redirectWasiConsole, }) { + this.#mimeDb = fetch("https://cdn.jsdelivr.net/npm/mime-db@1.54.0/db.json") + .then((resp) => resp.json()) + .then((db) => { + const ext2mime = {}; + for (const mime in db) { + if (db[mime].extensions) { + for (const ext of db[mime].extensions) { + ext2mime[`.${ext}`] = mime; + } + } + } + return ext2mime; + }); + this.#dyldHost = new DyLDHost({ outFd, inFd }); this.#server = http.createServer(async (req, res) => { @@ -634,6 +650,33 @@ args.rpc.opened.then(() => main(args)); return; } + if (req.url.startsWith("/assets")) { + const p = path.resolve( + assetsDir, + new URL(req.url, origin).pathname.replace("/assets/", ""), + ); + try { + await fs.promises.access(p, fs.promises.constants.R_OK); + + res.setHeader( + "Content-Type", + (await this.#mimeDb)[path.extname(p)] || "application/octet-stream", + ); + + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + + res.writeHead(200); + fs.createReadStream(p).pipe(res); + } catch { + res.writeHead(404, { + "Content-Type": "text/plain", + }); + res.end("not found"); + } + + return; + } + if (req.url.startsWith("/rpc")) { const endpoint = req.url.replace("/rpc/", ""); @@ -1373,6 +1416,7 @@ async function nodeMain({ searchDirs, mainSoPath, outFd, inFd, args }) { const server = new DyLDRPCServer({ host: process.env.GHCI_BROWSER_HOST || "127.0.0.1", port: process.env.GHCI_BROWSER_PORT || 0, + assetsDir: process.env.GHCI_BROWSER_ASSETS_DIR || process.cwd(), dyldPath: import.meta.filename, searchDirs, mainSoPath, View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/c951fef11c9cac9c43c6520905103e39... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/c951fef11c9cac9c43c6520905103e39... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Cheng Shao (@TerrorJack)