frontendweb-frontendpurescriptbuild-systemspago

PureScript: how to make build system produce multiple js outputs?


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.


Solution

  • 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:

    Code

    Given two Halogen-based PureScript pages:

    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
    

    Testing

    1. 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>
        
    2. Run python -m http.server

    3. Visit http://localhost:8000/page1.html and http://localhost:8000/page2.html in a browser.

    You should see large text Page1 and Page2 accordingly.

    Explanation

    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:

    1. 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.

    2. 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.

    3. 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.