XUtils

spago

PureScript package manager and build tool powered by Dhall and package-sets


spago

[npm][spago-npm] Latest release build Maintainer: f-f

(IPA: /ˈspaɡo/)

PureScript package manager and build tool.

Spago logo - a 3d box containing a blob of spaghetti

Super quick tutorial

Let’s set up a new project!

$ mkdir purescript-pasta
$ cd purescript-pasta
$ spago init

This last command will create a few files:

.
├── spago.yaml
├── src
│   └── Main.purs
└── test
    └── Test
        └── Main.purs

If you have a look at the spago.yaml file, you’ll see that it contains two sections:

  • the workspace section, which details the configuration for the dependencies of the project as a whole (which can be a monorepo, and contain more than one package), and other general configuration settings. In this sample project, the only configuration needed is the package set version from which all the dependencies will be chosen. See here for more info about how to query the package sets.
  • the package section, that is about the configuration of the package at hand, such as its name, dependencies, and so on.

For more info about all the various configuration settings, visit the section about the configuration format.

To build and run your project, use:

$ spago run

This will:

  • download and compile the necessary dependencies (equivalent to spago install)
  • compile this sample project in the output/ directory (equivalent to spago build).
    You can take a look at the content of output/Main/index.js to see what kind of JavaScript has been generated from your new Main.purs file
  • run the generated JS, which is roughly equivalent to running
    
    $ node -e "import('./output/Main/index').then(m => m.main())"
    
    The above code imports the JS file you just looked at, and runs its main with Node.

You can also bundle the project in a single file with an entry point, so it can be run directly (useful for CLI apps):

$ spago bundle --bundle-type app --platform node
$ node .

Great! If you read unitl here you should be set to go write some PureScript without worrying too much about the build 😊

Where to go from here? There are a few places you should check out:

How do I…

This section contains a collection of mini-recipes you might want to follow in order to get things done with Spago.

Migrate from spago.dhall to spago.yaml

You’ll need to use [spago-legacy] for this.

# Install spago-legacy
npm install -g spago-legacy
# You can then create a `spago.yaml` file with `migrate`
spago-legacy migrate

### Migrate from `bower`

Same as above, but with an additional `spago init` command just after you install [spago-legacy], so that the `bower.json` file is converted into a `spago.dhall` file.

### Setup a new project using a specific package set

Since `spago init` does not necessarily use the latest package set. Fortunately, you can specify which package set to use via the `--package-set` flag:

```console
$ spago init --package-set 41.2.0

See here for how to ask Spago which sets are available for use.

Setup a new project using the solver

Package sets are the default experience to ensure that you always get a buildable project out of the box, but one does not necessarily have to use them.

If you’d like to set up a project that uses the Registry solver to figure out a build plan, you can use:

$ spago init --use-solver

When using the solver (and when publishing a package), it’s important to specify the version bounds for your dependencies, so that the solver can figure out a build plan.

You can ask Spago to come up with a good set of bounds for you by running:

$ spago install --ensure-ranges

Install a direct dependency

To add a dependency to your project you can run:

# E.g. installing Halogen
$ spago install halogen

### Download my dependencies locally

```console
$ spago install

This will download and compile all the transitive dependencies of your project (i.e. the direct dependencies, i.e. the ones listed in the dependencies key of spago.yaml, plus all their dependencies, recursively) to the local .spago folder.

However, running this directly is usually not necessary, as all commands that need the dependencies to be installed will run this for you.

Running spago fetch is equivalent, but skips the compilation step.

Build and run my project

You can build the project and its dependencies by running:

$ spago build

This is mostly just a thin layer above the PureScript compiler command purs compile.

Note: by default the build command will try to install any dependencies that haven’t been fetched yet - if you wish to disable this behaviour, you can pass the --no-install flag.

The build will produce very many JavaScript files in the output/ folder. These are ES modules, and you can just import them e.g. on Node.

[!NOTE]
The wrapper on the compiler is so thin that you can pass options to purs. E.g. if you wish to ask purs to emit errors in JSON format, you can run

> $ spago build --purs-args "--json-errors"
> ```
>
> However, some `purs` flags are covered by Spago ones, e.g. to change the location of the `output` folder:
>
> ```console
> $ spago build --output myOutput
> ```

To run a command before a build you can use the `--before` flag, eg to post a notification that a build has started:

```console
$ spago build --before "notify-send 'Building'"

To run a command after the build, use --then for successful builds, or --else for unsuccessful builds:

$ spago build --then "notify-send 'Built successfully'" --else "notify-send 'Build failed'"

Multiple commands are possible - they will be run in the order specified:

$ spago build --before clear --before "notify-send 'Building'"

If you want to run the program, just use run:

$ spago run -p package-name -m Module.Containing.Main

# We can pass arguments through to `purs compile`
$ spago run -p package-name  -m Module.Containing.Main --purs-args "--verbose-errors"

# We can pass arguments through to the program being run
$ spago run -p package-name  -m Module.Containing.Main -- arg1 arg2

Oof! That’s a lot of typing. Fortunately it’s possible to configure most of these parameters in the package.run section of your configuration file, so you don’t have to supply them at the command line.

See here for more info about this, but it allows us to instead write:

# The main module can be defined in the configuration file, but
# purs flags still need to be supplied at the command line
spago run -p package-name --purs-args "--verbose-errors"

# It's possible to even pass arguments from the config, which would look like this:
#
# package:
#   run:
#       main: Main
#       execArgs:
#         - "arg1"
#         - "arg2"
$ spago run -p package-name

Lastly, if you only have a single package defined in the workspace with these parameters defined in the config file, you can just run

spago run

Test my project

You can also test your project with spago:

# Test.Main is the default here, but you can override it as usual
$ spago test --main Test.Main
Build succeeded.
You should add some tests.
Tests succeeded.

As with the run command, it’s possible to configure the tests using the spago.yaml - most importantly to separate test dependencies from the dependencies of your application/library.

Please see the section about the configuration format for more info, but in the meantime note that it’s possible to install test dependencies by running:

$ spago install --test-deps spec

Run a repl

You can start a repl with the following command:

$ spago repl

Run a standalone PureScript file as a script

You can run a standalone PureScript file as a script via spago script. Note: The module name must be Main, and it must export a function main :: Effect Unit.

By default, the following dependencies are installed: effect, console, prelude.

You can run a script via the following, optionally specifying a package set to use, and additional dependencies to pull from there:

$ spago script --package-set 41.2.0 -d node-fs path/to/script.purs

Direct dependencies, i.e. only the ones listed in spago.dhall

$ spago ls deps

Transitive dependencies, i.e. all the dependencies of your dependencies

$ spago ls deps –transitive


You can provide the `--json` flag for a more machine-friendly output.

### Install all the packages in the set

There might be cases where you'd like your project to depend on all the packages
that are contained in the package set (this is sometimes called
["acme build"][acme]).

If you have [`jq`][jq] installed, you can accomplish this in relatively few characters:

```console
$ spago ls packages --json | jq -r 'keys[]' | xargs spago install

Override a package in the package set with a local one

Let’s say I’m a user of the popular aff package. Now, let’s say I stumble upon a bug in there, but thankfully I figure how to fix it. So I clone it locally and add my fix.

Now if I want to test this version in my current project, how can I tell spago to do it?

There’s a section of the spago.yaml file just for that, called extraPackages.

In this case we override the package with its local copy, which should have a spago.yaml - our workspace will look something like this:

workspace:
    registry: 41.2.0
  extraPackages:
    aff:
      path: ../my-purescript-aff

Now if we run spago ls packages, we’ll see that it is now included as a local package:

$ spago ls packages
+----------------------------------+------------------------------------------+------------------------------------------------+
| Package                          | Version                                  | Location                                       |
+----------------------------------+------------------------------------------+------------------------------------------------+
| abc-parser                       | 2.0.0                                    | -                                              |
| ace                              | 9.1.0                                    | -                                              |
| aff                              | local                                    | ../my-purescript-aff                           |
| aff-bus                          | 6.0.0                                    | -                                              |
| aff-coroutines                   | 9.0.0                                    | -                                              |
| aff-promise                      | 4.0.0                                    | -                                              |
...

Override a package in the package set with a remote one

Let’s now say that we test that our fix from above works, and we are ready to Pull Request the fix.

So we push our fork and open the PR, but we want to already use the fix in our build, while we wait for it to land upstream and then on the next package set.

In this case, we can just change the override to point to some commit of our fork, like this:

workspace:
    registry: 41.2.0
  extraPackages:
    aff:
      git: https://github.com/my-user/purescript-aff.git
      ref: aaa0aca7a77af368caa221a2a06d6be2079d32da

[!WARNING]
You can use a “branch”, a “tag” or a “commit hash” as a version. It’s strongly recommended to avoid using branches, because if you push new commits to a branch, spago won’t pick them up unless you delete the .spago/packages/aff/your-branch folder.

Add a package to the package set

[!IMPORTANT]
You still need to spago install my-new-package after adding it to the package set, or Spago will not know that you want to use it as a dependency!

If a package is not in the upstream package set, you can add it exactly in the same way, by adding it to extraPackages.

E.g. if we want to add the facebook package:

workspace:
    registry: 41.2.0
  extraPackages:
    facebook:
      git: https://github.com/Unisay/purescript-facebook.git
      ref: v0.3.0 # branch, tag, or commit hash

[!NOTE]
If the upstream library that you are adding has a spago.yaml file, then Spago will just pick up the dependencies from there. If that’s not the case, then you’ll have the provide the dependencies yourself, adding a dependencies field.

As you might expect, this works also in the case of adding local packages:

workspace:
    registry: 41.2.0
  extraPackages:
    facebook:
      path: ../my-purescript-facebook

Querying package sets

Since the versioning scheme for package sets does not tell anything about the compiler version or when they were published, you might want to have a look at the list of all the available ones. You can do that with:

$ spago registry package-sets

This will print a list of all the package sets ever releases, which could be overwhelming, as you’d likely only be interested in the latest one.

This is how you would ask for the latest package sets for each compiler version:

$ spago registry package-sets --latest
+---------+------------+----------+
| VERSION | DATE       | COMPILER |
+---------+------------+----------+
| 10.0.0  | 2023-01-05 | 0.15.4   |
| 20.0.3  | 2023-04-08 | 0.15.7   |
| 27.2.0  | 2023-06-17 | 0.15.8   |
| 29.1.0  | 2023-07-18 | 0.15.9   |
| 42.1.0  | 2023-09-26 | 0.15.10  |
+---------+------------+----------+

Upgrading packages and the package set

If your project is using the Registry solver (i.e. no package set and only version bounds), then running spago upgrade will try to put together a new build plan with the latest package versions published on the Registry, given that they are still compatible with your current compiler.

If instead you are using package sets, then spago upgrade will bump your package set version to the latest package set available for your compiler version.

You can pass the --package-set flag if you’d rather upgrade to a specific package set version. You can of course just edit the workspace.packageSet field in the spago.yaml file.

Graph the project modules and dependencies

You can use the graph command to generate a graph of the modules and their dependencies:

$ spago graph modules

The same goes for packages:

$ spago graph packages

The command accepts the --json and --dot flags to output the graph in JSON or DOT format respectively.

This means that you can pipe the output to other tools, such as [graphviz][graphviz] to generate a visual representation of the graph:

$ spago graph packages --dot | dot -Tpng > graph.png

…which will generate something like this:

packages-graph

Finally, the graph command is also able to return a topological sorting of the modules or packages, with the --topo flag:

$ spago graph modules --topo

Test dependencies

Like this:

package:
  name: mypackage
  dependencies:
    - effect
    - console
    - prelude
  test:
    main: Test.Main
    dependencies:
      - spec

You can add more with spago install --test-deps some-new-package.

It is then possible to run it with node:

$ node index.js


> [!NOTE]\
> Spago will bundle your project in the [esbuild bundle format](https://esbuild.github.io/api/#format) [IIFE](https://esbuild.github.io/api/#format-iife).

When bundling a `module` instead, the output will be a single JS module that you can `import` from JavaScript:

```console
# You can specify the main module and the target file, or these defaults will be used
$ spago bundle --bundle-type module --main Main --outfile index.js

Can now import it in your Node project:

$ node -e "import('./index.js').then(m => console.log(m.main))"
[Function]

Spago does not wrap the entirety of the bundler’s API (esbuild for JS builds), so it’s possible to pass arguments through to it. E.g. to exclude an NPM package from the bundle you can pass the --external flag to esbuild:

  • either through the command line, with the --bundler-args flag, i.e. --bundler-args "--external:better-sqlite3".

  • or by adding it to the configuration file: “`yaml package: bundle: extra_args:

    - "--external:better-sqlite3"
    

    Enable source maps

When bundling, you can include --source-maps to generate a final source map for your bundle.

Example:

spago bundle -p my-project --source-maps --minify --outfile=bundle.js

will generate a minified bundle: bundle.js, and a source map: bundle.js.map.

Node

If your target platform is node, then you need to ensure your node version is >= 12.2.0 and enable source maps when executing your script:

spago bundle -p my-project --platform node --source-maps --minify --outfile=bundle.js
node --enable-source-maps bundle.js

Browsers

If you are targeting browsers, then you will need to ensure your server is configured to serve the source map from the same directory as your bundle.

So for example if your server is configured to serve files from public/, you might run:

spago bundle -p my-project --platform browser --source-maps --minify --outfile=public/bundle.js

Skipping the “build” step

When running spago bundle, Spago will first try to build your project, since bundling requires the project to be compiled first.

If you already compiled your project and want to skip this step you can pass the --no-build flag.

Generated build info/metadata

Spago will include some metadata in the build, such as the version of the compiler used, the version of Spago, and the versions of the package itself.

This is so that you can access all these things from your application, e.g. to power a --version command in your CLI app.

This info will be available in the Spago.Generated.BuildInfo module, which you can import in your project.

The file itself is stored in the .spago folder if you’d like to have a look at it.

Publish my library

To publish your library to the [PureScript Registry][registry], you can run:

$ spago publish

…and follow the instructions 🙂

Know which purs commands are run under the hood

The -v flag will print out all the purs commands that spago invokes during its operations, plus a lot of diagnostic info, so you might want to use it to troubleshoot weird behaviours and/or crashes.

Install autocompletions for zsh

Autocompletions for zsh need to be somewhere in the fpath - you can see the folders included in your by running echo $fpath.

You can also make a new folder - e.g. ~/.my-completions - and add it to the fpath by just adding this to your ~/.zshrc:

fpath=(~/.my-completions $fpath)

Then you can obtain the completion definition for zsh and put it in a file called _spago (yes it needs to be called like that):

spago --zsh-completion-script $(which spago) > ~/.my-completions/_spago

Then, reload completions with:

compinit

[!NOTE]
You might need to call this multiple times for it to work.

[!NOTE]
See the note in the Bash section above when installing Spago with a package manager other than NPM.

Concepts and explanations

This section details some of the concepts that are useful to know when using Spago. You don’t have to read through this all at once, it’s meant to be a reference for when you need it.

The workspace

For any software project, it’s usually possible to find a clear line between “the project” and “the dependencies of the project”: we “own” our sources, while the dependencies only establish some sort of substrate over which our project lives and thrives.

Following this line of reasoning, Spago - taking inspiration from other tools such as [Bazel][bazel] - uses the concept of of a “workspace” to characterise the sum of all the project packages and their dependencies (including only “potential” ones).

A very succint introduction to this idea can be found [in Bazel’s documentation][bazel-workspace]:

A workspace is a directory tree on your filesystem that contains the source files for the software you want to build.
Each workspace has a text file named WORKSPACE which may be empty, or may contain references to external dependencies required to build the outputs.
Directories containing a file called WORKSPACE are considered the root of a workspace.
Therefore, Bazel ignores any directory trees in a workspace rooted at a subdirectory containing a WORKSPACE file, as they form another workspace.

Spago goes by these same rules, with the difference that we do not use a separate WORKSPACE file, but instead use the workspace section of the spago.yaml file to define what the set of our external dependencies are, and where they come from.

This can be as simple as:

workspace: {}

…which means that “this is now a workspace, and all the dependencies are going to be fetched from the Registry”.

Or it can be more complex, e.g.:

workspace:
  packageSet:
    url: https://raw.githubusercontent.com/some-user/custom-package-sets/some-release/packages.json
  extraPackages:
    aff:
      path: ../my-purescript-aff

…which means that “this is now a workspace, and all the dependencies are going to be fetched using instructions from this custom package set (which could point to the Registry packages or somewhere else), except for the aff package, which is going to be fetched from the local folder ../my-purescript-aff”.

As described in the Bazel docs quoted above, the presence of a workspace section will denote the current folder as the root of a workspace, and Spago will recurse into its subfolders to find all the packages that are part of it - by looking for spago.yaml files with a package section - but ignore the subdirectory trees that are themselves workspaces - i.e. containing spago.yaml files with a workspace section.

The configuration file

This section documents all the possible fields that can be present in the spago.yaml file, and their meaning.

”`yaml

The workspace section is one of the two sections that can be present

at the top level. As described above, it defines where all of the

dependencies of the project come from.

It’s optional, as it will be found only in the root of the project

(defining the workspace), and not in any sub-package configurations,

which will only contain the package section.

workspace:

# The packageSet field defines where to fetch the package set from. # It’s optional - not defining this field will make Spago use the # Registry solver instead, to come up with a build plan. packageSet:

# It could either be a pointer to the official registry sets that
# live at https://github.com/purescript/registry/tree/main/package-sets
registry: 11.10.0
# Or it could just point to a URL of a custom package set
# See the "Custom package sets" section for more info on making one.
url: https://raw.githubusercontent.com/purescript/package-sets/psc-0.15.7-20230207/packages.json
# It is also possible to point to a local package set instead:
path: ./my-custom-package-set.json

# This section defines any other packages that you’d like to include # in the build. It’s optional, in case you just want to use the ones # coming from the Registry/package set. extraPackages:

# Packages are always specified as a mapping from "package name" to
# "where to find them", and there are quite a few ways to define
# these locations:
# 1) local package - it's on your filesystem but not in the workspace
some-local-package:
  path: ../some-local-package
# 2) remote package from the Registry
#    Just specify the version you want to use. You can run
#    `spago registry info` on a package to see its versions.
#    This is useful for packages that are not in the package set,
#    and useless when using the Registry solver.
some-registry-package: 1.0.2
# 3) remote package from Git
#    The `git` and `ref` fields are required (`ref` can be a branch,
#    a tag, or a commit hash).
#    The `subdir` field is optional and necessary if you'd like to use
#    a package that is not located in the root of the repo.
#    The `dependencies` field is optional and necessary if the package
#    does not have a `spago.yaml` file. In that case Spago will figure
#    out the dependencies automatically.
some-package-from-git:
    git: https://github.com/purescript/registry-dev.git
    ref: 68dddd9351f256980454bc2c1d0aea20e4d53fa9
    subdir: lib
    dependencies:
      - foo
# 4) remote package from Git, legacy style (as the old package sets)
#    Works like the above, but all fields are mandatory.
legacy-package-style:
    repo: "https://github.com/purescript/purescript-prelude.git"
    version: "v6.0.1"
    dependencies:
      - prelude
      - effect
      - console

# This section is optional, and you should specify it only if you’d like # to build with a custom backend, such as purerl. # Please see the “Alternate backends” section for more info. backend:

# The command to run to build with this backend - required.
cmd: "node"
# Optional list of arguments to pass to the backend when building.
args:
  - "arg1"
  - "arg2"
  - "arg3"

# Optional setting to enable the “lockfile”. Enabling it will generate # a spago.lock file with a cache of the build plan. # It’s disabled by default when using package sets (because we already # get a stable build plan from there) and enabled by default when using # the Registry solver. # See “The lock file” section for more details. lock: false

# Optional section to further customise the build. buildOpts:

# Directory for the compiler products - optional, defaults to `output`.
output: "output"
# Specify whether to censor warnings coming from the compiler
# for files in the `.spago` directory`.
# Optional and can be one of two possible values
censorLibraryWarnings:
  # Value 1: "all" - All warnings are censored
  all

  # Value 2: `NonEmptyArray (Either String { byPrefix :: String })`
  # - String values:
  #      censor warnings if the code matches this code
  # - { byPrefix } values:
  #      censor warnings if the warning's message
  #      starts with the given text
  - CodeName
  # Note: when using `byPrefix`, use the `>` for block-string:
  # see https://yaml-multiline.info/
  - byPrefix: >
    "Data.Map"'s `Semigroup instance`

# Specify whether to show statistics at the end of the compilation,
# and how verbose they should be.
# Can be 'no-stats', 'compact-stats' (default), or 'verbose-stats',
# which breaks down the statistics by warning code.
statVerbosity: "compact-stats"

This is the only other section that can be present at the top level.

It specifies the configuration for a package in the current folder,

The lock file

The lock file is a file that Spago can generate to cache the build plan, so that it can be reused in subsequent builds.

When using package sets it is disabled by default - since we already get a stable build plan from there - while it’s enabled by default when using the Registry solver.

You can enable it manually by adding a lock: true field to the workspace section of your spago.yaml file, and that will keep it on regardless of which solving mode you’re using.

File System Paths used in Spago

Run spago ls paths to see all paths used by Spago. But in general, Spago utilizes two main directories for every project:

  • the local cache directory
  • the global cache directory

The local cache directory is located at <project-directory>/.spago and its location cannot be changed.

The global cache directory’s location depends on your OS. Its location can be changed by configuring the corresponding environment variable, if it is used:

  • Mac: ~/Library/Caches/spago-nodejs. The location cannot be changed.
  • Linux: ${XDG_CACHE_HOME}/spago-nodejs, or if XDG_CACHE_HOME is not set, ~/.cache/spago-nodejs. See XDG_CACHE_HOME.
  • Windows: %LOCALAPPDATA%\spago-nodejs\Cache, or if $LOCALAPPDATA% is not set, C:\Users\USERNAME\AppData\Local\spago-nodejs\Cache.
  • NixOS: ${XDG_RUNTIME_DIR}/spago-nodejs. See XDG_RUNTIME_DIR.

FAQ

Differences from legacy spago


Articles

  • coming soon...