This would seem like a basic question, but I found no information on it. Websites don't typically consist of a single page. E.g. StackOverflow has menu on the left with Home
, Questions
, Tags
links, and clicking either will trigger a unique GET
request. Similarly, in my Purescript project I'd need to produce 3 different JS files, like tags.js
, home.js
, etc.
Currently I am using Spago, and am not finding way to do that, at least not a straightforward one. Having dug through documentation and spago
options, the closest I managed to find are monorepos and polyrepos. But both are too cumbersome for such a simple usecase. At the very least they'd require you to produce a spago.yaml/dhall
file and src
directory for each page. But you'd typically have dozens of pages on a website! It can't be the way you're supposed to develop a frontend in PureScript, can it?
Neither I seem to see anything suitable in the output
dir.
So, how do you produce unique javascript files for different pages? Preferrably with Spago, but it's not a requirement.
Having spent 2 days banging my head, I devised a solution, but it's kinda awkward. Surprisingly, for such popular usecase PureScript has exactly zero tooling and documentation (well, before this answer, apparently 😊), despite targeting web-devel first.
Code first, explanations later:
Given two Halogen-based PureScript pages:
src/Common.purs
:
module Common where
import Prelude
import Effect (Effect)
import Halogen as H
import Halogen.HTML as HH
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
helloWorldHeading :: String -> Effect Unit
helloWorldHeading text = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
where
component = H.mkComponent { initialState: const 0, render, eval: H.mkEval H.defaultEval}
render _ = HH.h1 [ ] [ HH.text text ]
src/pages/Page1.purs
:
module Page1 where
import Prelude
import Common (helloWorldHeading)
import Effect (Effect)
-- The `main` function. It needs to have name matching the one in build_pages.sh
-- script, and it should be unique enough so in the output js it's not renamed.
_pageMain :: Effect Unit
_pageMain = helloWorldHeading "Page1"
src/pages/Page2.purs
:
module Page2 where
import Prelude
import Common (helloWorldHeading)
import Effect (Effect)
-- The `main` function. It needs to have name matching the one in build_pages.sh
-- script, and it should be unique enough so in the output js it's not renamed.
_pageMain :: Effect Unit
_pageMain = helloWorldHeading "Page2"
You produce unique output via build_pages.sh
with the following content:
#!/bin/bash -eu
mkdir -p output/pages/
spago build # build here to skip that step later
for page in src/pages/*.purs; do
filename=$(basename ${page})
filename_noext="${filename%.*}"
modname="${filename_noext^}" # same as capitalized filename w/o extension
output_file="output/pages/${filename_noext}".js
spago bundle-module --main "${modname}" --to ${output_file} --no-build
# Remove the export. It spans multiple lines, but thankfully there is nothing
# afterwards, so we just remove all lines starting with `^export`
sed -i '/^export /,$d' ${output_file}
# I'm not exactly sure why, but browser will be throwing errors unless the code
# is wrapped into a lambda call. So do that.
sed -i '1i (() => {
$a _pageMain();})();' ${output_file}
done
spago bundle-app --no-build
Create HTML files at the top-level dir:
page1.html
:
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<title>Halogen App</title>
</head>
<body>
<div id="app"></div>
<script src="output/pages/Page1.js"></script>
</body></html>
page2.html
:
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<title>Halogen App</title>
</head>
<body>
<div id="app"></div>
<script src="output/pages/Page2.js"></script>
</body></html>
Run python -m http.server
Visit http://localhost:8000/page1.html
and http://localhost:8000/page2.html
in a browser.
You should see large text Page1
and Page2
accordingly.
Fyodor Soikin hinted that it may be possible with spago bundle-module
(not to be confused with bundle-app
), but bundle-module
alone does very little and is bad at it. Problems (being solved in the code section) are:
The command to produce a JS file from a Page1.purs
is: spago bundle-module --main Modname --to output/Page1.js
. That's it. A separate command for every page. Imagine having like a dozen of pages, you can't be running this command manually each time. So you need a script.
But then, a script won't get you far either, because even for "hello world" Halogen pages on a tmpfs the command takes about a second. So, like, for 5 pages you get to wait ≈4s for each rebuild, that's a lot. So now you have to wrap the build system to a build system that would track file changes and avoid running bundle-module
for files that are unchanged.
I almost wrote a build.ninja
generator for the usecase, but at some point dropped the ball because I already spent too much time on a seemingly simple task. I promise to update this answer with such generator if I get to use PureScript in production, but for now I decided to settle with just a script, similar to the one posted here.
Every such module needs an entry point and you can use main
function everywhere. But apparently such name has high chance to clash with something, because in the JS output the entry point was getting named main2
for me, and I presume it may just as easily become main3
or main4
. For this reason, I named it _pageMain
, this way it's unlikely to get renamed.
The code spago bundle-module
produces wouldn't work in a browser as is. You either need to modify it, or alternatively, you can make a "proxy file" import { _pageMain } from './Page1.js'; _pageMain();
and use this HTML syntax to make browser load it: <script type="module" src="output/pages/page1_proxy.js"></script>
.
Now, from the development POV, maintaining such separate files, even if autogenerated, increases margin for mistakes and may potentially take additional time for going through them during debugging. So I'd advice against, make less code not more 😊 In my solution I use a sed
script that modifies the output file so that it becomes loadable.