Recommended tooling for PureScript applications in 2019

tutorial
#1

I often see questions in Slack about what tools are supposed to be used in PureScript projects. This post is an attempt to clarify tooling for the common case: a PureScript app run in the browser, possibly with a mix of JavaScript and PureScript code.

There are a few sections here, feel free to skip around:

  1. Summary of tools
  2. Starting a new project, installing tools
  3. Installing PureScript libraries, building the project
  4. Installing JavaScript libraries
  5. Bundling our application to a single .js file, including JavaScript and PureScript code
  6. Using Yarn + package.json as a script runner

Side note: all of this stuff is used in practice in the Real World Halogen example application, if you’d like to see it in action.

Summary of Tools

If you are starting a new application in PureScript today, I recommend the following tools:
  1. yarn to install all the other tools (including the PureScript compiler), install JavaScript libraries, and run scripts from a package.json file.
  2. spago to install PureScript libraries, build the PureScript project, generate documentation, and perform other miscellaneous PureScript tasks
  3. webpack or parcel to perform bundling and optimization steps on the JavaScript compiled by PureScript and any JavaScript dependencies, to ultimately produce a single app.js file.

Some other tools worth mentioning:

  • npx, which comes with your Node / npm installation, lets you run all commands using locally-installed tools instead of globally-installed ones. For example, to use a local Spago install instead of a global one: npx spago build instead of spago build. Note: @cprussin mentioned that Yarn supports the same functionality without using npx: yarn spago build will look for spago in your scripts and then in your local installed modules.
  • If you use nix, then you can just use nix to install all these tools instead, skipping npx. In fact, with spago2nix and yarn2nix you can generate Nix expressions for all your dependencies.
  • purty is a code formatting tool for PureScript. It’s excellent, and you can install it the same way as everything else.
  • zephyr is a dead code elimination tool for PureScript. If you have a lot of dependencies with unused modules (purescript-web-* is a common culprit) then it can nicely trim down your bundle size. You may have to download the binary directly, or use nix to install it.
  • psc-ide provides really good editor support for PureScript, usable in VSCode, Emacs, Vim, etc. and is provided as part of the compiler by default (you don’t need to install anything).
  • pscid is a really fast file watcher which will report errors and provide suggestions in the shell. As mentioned by @Benjmhart this can be useful when the compiler + your bundler doesn’t provide a fast enough feedback cycle.
  • psvm is a version manager for PureScript; if you don’t use local PureScript installs for your projects and install rely on a global installation, then psvm lets you easily switch between different versions of PureScript. nvm is a similar tool for Node versions, which inspired psvm.

Some other tooling from the PureScript ecosystem which I no longer use often, but which you’ll see in the wild:

  • npm is used for the same things as yarn (install JavaScript libraries, install tooling from the NPM registry like the compiler and Spago, run scripts from package.json) and was the only option before 2016. I and many others prefer Yarn, but it doesn’t really matter which one you use for a PureScript project.

  • bower was a package manager for JavaScript which happened to work well for PureScript, too. It’s now in maintenance mode, and most folks use Yarn for JavaScript dependencies and Spago for PureScript dependencies. Bower is still used as a registry for PureScript packages and you will still need to use it if you are publishing a library (though Spago will support this soon).

  • pulp was the de facto standard build tool for PureScript projects until Spago was released. Both tools will let you build your project using the PureScript compiler (purs) and largely have similar commands.

  • psc-package was the first package manager built on the concept of package sets (a collection of packages which are all known to compile together, which you install without specifying a specific version). However, its main deficiency was that you had to either download the main PureScript package set manually and place it in your project, or write your own. You then had to manually add packages and maintain the package set, all in difficult-to-maintain JSON files. It is not a build tool, but is used by other build tools under the hood. Spago provides a much better workflow, and is also based on package sets. See this issue from the Real World project for problems I had with psc-package and this pull request which migrated the project from Bower to Spago.

  • purp is a tool for building projects using psc-package. I haven’t used the tool myself, but as far as I can tell its functionality is matched (and exceeded) by Spago.

  • spacchetti was the first tool to manage package sets as Dhall files instead of JSON, unlocking significantly easier modification of the main package set + adding your own packages. It’s now used to power the main package set and other functionality has been rolled in to Spago instead.

As of today, if you are publishing a library then you will most likely want to use Pulp + Bower, and otherwise you will most likely want Spago. In the near future, Spago will also support library publishing.

Starting a New Project

Going back to the original list of tools, here’s a sequence of commands to set up a new project which relies on a few libraries from the PureScript and JavaScript. It assumes you already have Yarn installed, but will install the rest along the way.

mkdir my-project && cd my-project
yarn add --dev purescript spago parcel-bundler

At time of writing, Yarn has produced a package.json file containing these dependencies:

{
  "devDependencies": {
    "parcel-bundler": "^1.12.3",
    "purescript": "^0.13.3",
    "spago": "^0.9.0"
  }
}

We can now start using these locally-installed tools to get our project started. First, we’ll initialize our PureScript project — this will create a .gitignore file, src and test directories, and sync us with the latest official package set.

# psc-0.13.3-20190831 is the package set at time of writing
yarn spago init

We can also compile and run the Main.purs file which has been created in the src directory:

$ yarn spago run
Installation complete.
           Src   Lib   All
Warnings   0     0     0
Errors     0     0     0
Build succeeded.
🍝

Installing PureScript Dependencies

Let’s say we’re going to use Halogen to build our UI — we can now use Spago to install this PureScript library:

yarn spago install halogen

Now that it’s installed, we can recompile our project so Halogen and its dependencies are built:

yarn spago build

And we could now start using Halogen in our project:

module MyApp.MyComponent where

import Halogen as H
import Halogen.HTML as HH
...

Installing JavaScript Libraries

Let’s say we want to use `marked`, a JavaScript library for Markdown, via PureScript’s foreign-function interface. We can go back to Yarn to install the library:
yarn add marked

And now our package.json has the marked library installed, too.

{
  "devDependencies": {
    "parcel-bundler": "^1.12.3",
    "purescript": "^0.13.3",
    "spago": "^0.9.0"
  },
  "dependencies": {
    "marked": "^0.7.0"
  }
}

Setting Up Bundling (Serving to the Browser)

We’ve compiled our PureScript project, which has produced JavaScript files in our `output` directory, but we haven’t yet got anything we can actually send to the browser.

Note: this section is going to be rapid-fire just so you’re aware of the basics. It’s not a tutorial on using tools like Parcel and Webpack. See projects like Vidtracker or Real World Halogen for real world examples.

Let’s use Spago to create a single app.js file from our project, instead of just compiling a bunch of modules in the output directory. That way we can actually import the resulting JavaScript file into an HTML file and view it in the browser.

mkdir dist
yarn spago bundle-app --main Main --to dist/app.js

Now, let’s create a tiny HTML file which imports this JavaScript. We’ll then be able to open that file in the browser and use our app.

mkdir assets && touch assets/index.html

We can use a tiny HTML file like this one — just enough to provide a DOM node for our application to run inside, and an import for our bundled app.js file, which we’ll create in a moment:

<!-- my-project/assets/index.html -->
<!DOCTYPE html>
<html>
  <body>
    <div id="app"></div>
    <script src="../dist/app.js"></script>
  </body>
</html>

If we had actually made a Halogen app, we’d be able to see it now. But this isn’t quite enough — we actually need to bundle in a JavaScript library, too, the marked library we installed earlier.

Most single-page applications will use a bundler like Parcel or Webpack to bring all their JavaScript, CSS, and HTML together, minify them, and do other processing for efficiency. They also typically support other conveniences like dev servers and hot reloading.

Let’s get Parcel working to bring our JavaScript and PureScript together in a single JavaScript file which can be shipped to the browser. Luckily for us, all we need to do is run parcel on our index.html file:

yarn parcel build assets/index.html

Running Scripts

It’s a bit tedious running all these commands manually. What if we wanted some automation? We don’t need to introduce any new tools.

We can adjust the package.json file to have a ”scripts" key where we put commands we’d like to be able to run with Yarn. Here’s a small example:

{
  "scripts": {
    "postinstall": "spago install",
    "build": "spago build",
    "clean": "rm -rf node_modules output .spago dist/* *.lock",
    "bundle": "spago bundle-app --main Main --to dist/app.js && parcel build assets/index.html",
  },
  "devDependencies": {
    "parcel-bundler": "^1.12.3",
    "purescript": "^0.13.3",
    "spago": "^0.9.0"
  },
  "dependencies": {
    "marked": "^0.7.0"
  }
}

Now that we have these scripts in place, we can run commands like:

  • yarn build to replace yarn spago build
  • yarn bundle to replace yarn spago bundle-app --main Main --to dist/app.js && parcel build assets/index.html
  • yarn install to replace yarn install && yarn spago install
16 Likes
#2

This is a great post.

I will say that using halogen with hot reloading tends to result in duplicate components being rendered, so in your JS root file, it’s good to actually trigger a reload.

I have a little starter template with some built in hot reloading behaviour here: https://github.com/Benjmhart/purehmr

something i also find with this workflow is that the feedback cycle can be a bit long for the purescript compiler + parcel. (also I occasionally hit some bugs with the way purescript gets interpreted by parcel which can’t really be explained here)

to have a shorted feedback cycle where we’re concerned primarily with compilation errors, I recommend PSCID https://github.com/kRITZCREEK/pscid

1 Like
#3

Some notes about npx:

  1. yarn spago (or yarn run spago) will run the locally installed spago, no need to use npx for this (see https://yarnpkg.com/lang/en/docs/cli/run/#toc-yarn-run-script).

  2. npx has a resolution order for running stuff. It will first check $PATH, then local packages, then if a command wasn’t found locally in either of those places, it will try to install the relevant package from the package repository (see https://github.com/npm/npx/blob/latest/README.md#description). It will not update the package.json of the project to indicate the packages that were installed. This has led my company to some hard to understand issues where things would work on one developer’s machines but not others, and makes it generally hard to reproduce system state.

Due to these points, I strongly recommend moving away from npx (my project at work has done so and the behavior of yarn run effectively supersedes npx). Note that the yarn run behavior mentioned here does not apply to npm, if you’re on npm you may still benefit from npx.

1 Like
#4

@Benjmhart that’s a good point about Halogen and hot reloading. Thanks for the link! I’ve also added pscid to the list of tooling, because I’d forgotten about it – I find the compile / error loop to be quite fast in VSCode and Vim with the IDE server, but I’m not waiting on Parcel or Webpack.

@cprussin That’s good to learn about. I wasn’t aware you could do this with Yarn, and it’s one more reason to move away from npm / npx. I’ve updated the post to include this information.

At this point you can easily get away with managing a PureScript project with only Yarn installed and then using it to install the compiler, Spago, and Parcel. That’s a reasonably lean set of tools. I have a soft spot for managing everything with Nix, but I don’t think that’s a reasonable suggestion as the default for most people.