Recommended tooling for PureScript in 2020

Last year I wrote Recommended tooling for PureScript applications in 2019, which is commonly used as a reference for folks setting up PureScript projects. In the last year a few things have changed, including:

  1. Yarn 2 can’t be used to install tooling like the PureScript compiler and NPM has improved a lot in the last couple of years, so I now suggest returning to NPM by default.
  2. GitHub Actions has become a fantastic option for CI, and there’s now setup-purescript to get a full PureScript toolchain in CI without requiring Node, so I’ve added a note on CI.
  3. The community has standardized more on a set of common tools, and in response I’ve trimmed the list of tools to just ones I would recommend.
  4. There are many more example PureScript applications built with React, Halogen, or another framework, or set up via the PureScript Cookbook or various templates. It’s been a great year for access to more examples. With this in mind, I no longer included the project setup guide from last year’s post (but it’s still there if you want it). I’ve sprinkled some links through this post.

As with last year: the Real World Halogen application is built using the tools described in this post and is a great starting point to see how a typical PureScript project might be set up. This year there’s also Real World PureScript React as an example!

Recommended Tools

If you are starting a new project in PureScript today, you will at least need:

  • spago, the build tool and package manager for PureScript. It’s used to install PureScript libraries, build PureScript code, generate documentation, bundle PureScript applications or modules into a single file, and perform other miscellaneous tasks.
  • purs, the compiler. Spago uses purs under the hood to compile your code, start an interactive repl, and so on. The compiler also provides an IDE server for editors to use.

You can install these tools with npm or via Nix with easy-purescript-nix. If your project is PureScript-only then you don’t need anything else. Even so, I recommend also considering:

  • psa, a configurable error-reporting frontend for the compiler. It lets you control reporting for errors and warnings, and Spago uses this tool automatically if it is installed.
  • setup-purescript, a GitHub Action to set up a PureScript toolchain for continuous integration. It lets you set up CI on Windows, macOS, and/or Linux with common PureScript tools including the compiler and Spago (among others).

The purescript-slug library is an example of a project built using only these tools (including CI). However, a typical PureScript application will also use some JavaScript libraries and tools. In that case, I recommend some tools from the JavaScript world:

  • npm, the default JavaScript package manager that ships with Node. It’s used to install JavaScript libraries and tools (and PureScript tooling, if you want) and to run scripts via a package.json file.
  • parcel, a bundler for JavaScript. It’s used to bundle, minify, and optimize all of your project’s JavaScript (including your compiled PureScript code) together into a single file that can be run in the browser or on Node. A bundler is not necessary for PureScript-only projects, but it /is/ necessary for projects that include both PureScript and JavaScript.

The Halogen library uses NPM and a package.json file to install PureScript tooling and run scripts. It can be useful to do this even if you have no JavaScript code or dependencies.

The official Halogen template project and unofficial React Basic Hooks template project install a bundler via NPM and use the package.json file as a script runner.

If you’re building an application then you will probably want to run an extra dead code elimination step on your PureScript code before bundling it. In that case, I recommend also including:

  • zephyr, a dead code elimination tool for PureScript. It’s used to remove unused PureScript code before compiling the result in order to trim bundle sizes. It’s not used in libraries, but it’s common in PureScript applications.

The Real World Halogen application is an example of a PureScript application built using all of these tools — including continuous integration via setup-purescript.

Other Recommended Tools

There are many other tools in the PureScript ecosystem worth exploring. These are ones I recommend checking out first:

  • purty is a source code formatter for PureScript. It’s used to standardize the style of PureScript code, which means less mental energy spent tweaking style and smaller diffs when reviewing code.
  • pscid is a /fast/ file watcher that reports errors and suggestions in the shell. It’s essentially the same as the error reporting provided by your editor, but can often be faster.

Using Nix

If you are a Nix user, you can set up a PureScript tool chain and generate Nix derivations using the following tools:

  • easy-purescript-nix is a one-stop-shop for installing tools for PureScript in Nix. It’s often used to produce developer shells or to write a derivation to build your whole PureScript project in Nix. It provides purs, spago, zephyr, purty, psa, and many other tools at multiple versions via Nix.
  • yarn2nix and spago2nix can be used to generate Nix derivations for your JavaScript and PureScript dependencies in Nix.

Libraries like Halogen Hooks use a Nix shell for development dependencies. There is also an example project by @tbenst which uses these tools to build a Halogen application with Nix.

Editor support

The PureScript compiler has an IDE server included, which has been used to implement a PureScript language server that major editors can use. Some of the features that PureScript’s IDE tooling supports include:

  • Auto-completion, definitions & documentation on hover, and jump-to-definition
  • Automatically fix imports, missing types, and other compiler errors / warnings
  • Fast rebuilds on file save
  • …many more!

Most folks I know writing PureScript use:

JavaScript Tools

There are some tools from JavaScript useful in a typical PureScript project – I won’t go in depth, but these are a starting point for exploring further:

  • webpack (optionally with purs-loader) and parcel are a few of the many tools you can use to bundle and minify assets like JavaScript and CSS in your web projects. PureScript produces JavaScript, so most of these tools work fine on your compiled code. Some tools rely on ES modules, which PureScript doesn’t yet produce, and so you won’t be able to use them on your compiled output.
  • eslint and jsconfig.json are useful for linting your JavaScript FFI code.
  • npm, yarn, and pnpm are a few of the many package managers available for JavaScript. By default I recommend using npm, but there are many alternatives out there.
25 Likes

Thank you for the updated post!

This inspired me to finally try Zephyr - wow! That’s nice!

Note that the default bundle-app behavior is sometimes as good, or even better than zephyr for producing small bundles.

For example, with the Halogen template, the zephyr workflow produces bundles that are slightly larger than just using bundle-app alone.

- index.js parcel minified
Just bundle-app 316K 128K
With zephyr 332K 132K
git clone https://github.com/purescript-halogen/purescript-halogen-template.git bundle-test
cd bundle-test

# Workflow with just spago bundle-app and parcel (no zephyr)

npm run build-prod
du -hs prod/index.js dist/prod.*.js
# 316K    prod/index.js          # bundle-app output
# 128K    dist/prod.0a4f58ae.js  # minified by parcel

# Including zephyr

rm -r dist
spago build --purs-args '--codegen js,corefn' && zephyr Main.main --dce-foreign
purs bundle -o prod/index.js -m Main dce-output/**/*.js
parcel build prod/index.html
du -hs prod/index.js dist/prod.*.js
# 332K    prod/index.js          # zephyr + bundle-app output
# 132K    dist/prod.08b89072.js  # minified by parcel
4 Likes

@milesfrain Interesting. What’s the difference for something the size of Thomas’s RealWorldHalogen project?

great content @thomashoneyman , was looking at doing some nix setup as been battling with Haskell and hie for couple of weeks!!

Hopefully getting pscid working in nix shell won’t be the same pain :slight_smile:

1 Like

Thank you, this is super helpful!

For Vim editor support I’d add:
psc-ide-vim or/and coc.nvim for IDE features.

There has been another helpful post to summarise the Vim options: https://discourse.purescript.org/t/vim-ide-options/1477

1 Like

This depends on your bundling configuration. There are lots of options, such as:

  • Whether to let purs bundle your app into a single index.js, or to let another bundler (such as parcel) process the CommonJS output.
  • Whether to minify with Parcel v1 or v2, and whether or not to enable “scope hoisting” (default for v2).
Parcel1 Parcel1 Parcel2 Parcel2
index.js default hoisting default no hoisting
CommonJS N/A 1.6M 1.3M 1.3M 1.7M
Bundle-app 1.5M 644K 636K 640K 644K
Zephyr CommonJS N/A 768K 592K 584K 808K
Zephyr Bundle-app 1.5M 648K 644K 644K 652K

The takeaway is that zephyr decreases bundle sizes for CommonJS output, but increases bundle sizes for bundle-app output. The zephyr docs should probably be changed to discourage bundle-app (purs bundle). Best option (at least for this project) is Zephyr applied to CommonJS output, then bundled and minified with Parcel2 (skipping bundle-app). This 584K bundle zips down to 112K, which puts us in the ballpark of the median realworld-app bundle size of ~100K. Still 4x larger than Elm’s 29K bundle though, so plenty of opportunity for improvement.

Here are the steps if you’d like to reproduce any of these numbers or convert into to a script for testing bundling regressions:

# Setup

git clone https://github.com/thomashoneyman/purescript-halogen-realworld.git rwh-test
cd rwh-test

npm i --only=prod
npm i -D parcel@next

# Versions

parcel --version
# 1.12.4
npx parcel --version
# 2.0.0-beta.1
spago version
# 0.15.3
purs version
# 0.13.8
zephyr --version
# 0.3.2

# Row 1

# disable use of zephyr output
sed -i 's/dce-output/output/g' index.js

spago build

parcel build assets/index.html; du -hs dist/*.js
# 1.6M    dist/rwh-test.a20f031f.js

rm -r dist; parcel build assets/index.html  --experimental-scope-hoisting; du -hs dist/*.js
# 1.3M    dist/rwh-test.0df2a89f.js

rm -r dist; npx parcel build assets/index.html; du -hs dist/*.js
# 1.3M    dist/rwh-test.5618311b.js

rm -r dist; npx parcel build assets/index.html --no-scope-hoist; du -hs dist/*.js
# 1.7M    dist/rwh-test.f1ba5345.js

# Row 2

spago bundle-app; du -hs index.js
# 1.5M    index.js

rm -r dist; parcel build assets/index.html; du -hs dist/*.js
# 644K    dist/rwh-test.3df6830c.js

rm -r dist; parcel build assets/index.html  --experimental-scope-hoisting; du -hs dist/*.js
# 636K    dist/rwh-test.88faaf97.js

rm -r dist; npx parcel build assets/index.html; du -hs dist/*.js
# 640K    dist/rwh-test.db1f5b93.js

rm -r dist; npx parcel build assets/index.html --no-scope-hoist; du -hs dist/*.js
# 644K    dist/rwh-test.5f733543.js

# Row 3

# restore use of zephyr output
git checkout index.js

spago build --purs-args '--codegen corefn'
zephyr -f Main.main

rm -r dist; parcel build assets/index.html; du -hs dist/*.js
# 768K    dist/rwh-test.a79df41d.js

rm -r dist; parcel build assets/index.html --experimental-scope-hoisting; du -hs dist/*.js
# 592K    dist/rwh-test.c998d679.js

rm -r dist; npx parcel build assets/index.html; du -hs dist/*.js
# 584K    dist/rwh-test.826b7d67.js

rm -r dist; npx parcel build assets/index.html --no-scope-hoist; du -hs dist/*.js
# 808K    dist/rwh-test.159c599a.js

# Row 4

purs bundle -o index.js -m Main dce-output/**/*.js; du -hs index.js
# 1.5M    index.js

rm -r dist; parcel build assets/index.html; du -hs dist/*.js
# 648K    dist/rwh-test.0d71f931.js

rm -r dist; parcel build assets/index.html --experimental-scope-hoisting; du -hs dist/*.js
# 644K    dist/rwh-test.6c4b2a05.js

rm -r dist; npx parcel build assets/index.html; du -hs dist/*.js
# 644K    dist/rwh-test.81baaf95.js

rm -r dist; npx parcel build assets/index.html --no-scope-hoist; du -hs dist/*.js
# 652K    dist/rwh-test.4aa98725.js
3 Likes

I’ve updated the post with these notes. Thanks!

1 Like

Thanks @thomashoneyman, this is a great resource!

I’ve recently started work on a project for bootstrapping an (opinionated) set of tools/configuration for PureScript projects, so I’ll definitely be incorporating these recommendations into it.

2 Likes

Are the instructions for the fourth row right? zephyr is not used there, but you mentioned it in the result table.

The zephyr output generated at the beginning of row 3 is preserved into row 4, so I believe both are using zephyr.

Not following the best FP practices here though in the script (with assumed side effects), so it would break if rows 3 and 4 are swapped in the script.

Sorry for the noise in advance, but is psc-package not used anymore? Started brushing up on purescript a couple hours ago after 2 years, and got gradually more confused the more I’ve been reading…

Thank you also for this resource! As it is so fresh, just going to follow the recommendations here, but still, getting the big picture never hurts.

Edit: just looked at the 2019 edition that has detailed explanations. Thanks for compiling that list as well!

4 Likes

To summarize what’s happened… psc-package still works, but isn’t the recommended tool anymore. spago has largely absorbed the functionality of psc-package and provides a better overall end-user experience. The Bower registry won’t accept any new uploads, so we’re currently in the process of creating PS’ own registry. As a result, bower is no longer recommended either, though it still works for managing your dependencies.

6 Likes