spago
(IPA: /ˈspaɡo/)
PureScript package manager and build tool.
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 tospago build
).
You can take a look at the content ofoutput/Main/index.js
to see what kind of JavaScript has been generated from your newMain.purs
file - run the generated JS, which is roughly equivalent to running
The above code imports the JS file you just looked at, and runs its$ node -e "import('./output/Main/index').then(m => m.main())"
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:
- see the “How to achieve X” section for practical advice without too much explanation
- see instead the Concepts and Explanations section for more in-depth explanations about the concepts that power Spago, such as package sets, or the Workspace.
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 topurs
. E.g. if you wish to askpurs
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 aversion
. 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 tospago 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 aspago.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 adependencies
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:
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 namedWORKSPACE
which may be empty, or may contain references to external dependencies required to build the outputs.
Directories containing a file calledWORKSPACE
are considered the root of a workspace.
Therefore, Bazel ignores any directory trees in a workspace rooted at a subdirectory containing aWORKSPACE
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 ifXDG_CACHE_HOME
is not set,~/.cache/spago-nodejs
. SeeXDG_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
. SeeXDG_RUNTIME_DIR
.