diff --git a/README.md b/README.md index cc74530a4..489da98ea 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![build](https://github.com/purescript/spago/actions/workflows/build.yml/badge.svg)](https://github.com/purescript/spago/actions/workflows/build.yml) [![Maintainer: f-f](https://img.shields.io/badge/maintainer-f%2d-f-teal.svg)](http://github.com/f-f) -*(IPA: /ˈspaɡo/)* +_(IPA: /ˈspaɡo/)_ PureScript package manager and build tool. @@ -22,16 +22,18 @@ PureScript package manager and build tool. > This new Spago is still in alpha, so while most of it works well, there will be some rough edges here and there. Please report them if you find any! The recommended installation method for Windows, Linux and macOS is `npm` (see the latest releases on npm - [here][spago-npm]): +[here][spago-npm]): ``` npm install -g spago@next ``` Other installation methods available: + - With Nix, using [purescript-overlay] **General notes:** + - The assumption is that you already installed the [PureScript compiler][purescript]. If not, get it with `npm install -g purescript`, or the recommended method for your OS. - You might have issues with `npm` and Docker (e.g. getting the message "Downloading the spago binary failed.." etc) @@ -39,7 +41,6 @@ Other installation methods available: - either **do not run npm as root**, because it doesn't work well with binaries. Use it as a nonprivileged user. - or use `--unsafe-perm`: `npm install -g --unsafe-perm spago@next` - ## Super quick tutorial Let's set up a new project! @@ -63,6 +64,7 @@ This last command will create a few files: ``` If you have a look at the `spago.yaml` file, you'll see that it contains two sections: + - [the `workspace` section](#the-workspace), 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](#whats-a-package-set) version from which all the dependencies will be chosen. See [here](#querying-package-sets) for more info about how to query the package sets. - [the `package` section](#whats-a-package), that is about the configuration of the package at hand, such as its name, dependencies, and so on. @@ -76,6 +78,7 @@ $ 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 @@ -95,6 +98,7 @@ $ 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"](#how-do-i) section for practical advice without too much explanation - see instead the [Concepts and Explanations](#concepts-and-explanations) section for more in-depth explanations about the concepts that power Spago, such as [package sets](#whats-a-package-set), or [the Workspace](#the-workspace). @@ -130,6 +134,8 @@ Where to go from here? There are a few places you should check out: - [Test dependencies](#test-dependencies) - [Bundle a project into a single JS file](#bundle-a-project-into-a-single-js-file) - [Enable source maps](#enable-source-maps) + - [Node](#node) + - [Browsers](#browsers) - [Skipping the "build" step](#skipping-the-build-step) - [Generated build info/metadata](#generated-build-infometadata) - [Generate documentation for my project](#generate-documentation-for-my-project) @@ -146,13 +152,15 @@ Where to go from here? There are a few places you should check out: - [The lock file](#the-lock-file) - [FAQ](#faq) - [Why can't `spago` also install my npm dependencies?](#why-cant-spago-also-install-my-npm-dependencies) +- [Differences from legacy spago](#differences-from-legacy-spago) + - [Watch mode](#watch-mode) - ## Design goals and reasons Our main design goals are: + - **Great UX**: a good build system just does what's most expected and gets out of the way so you can focus on actually thinking about the software itself, instead of spending your time configuring the build. - **Minimal dependencies**: users should not be expected to install a myriad of tools on their system to support various workflows. Spago only expects `git` and `purs` to be installed. - **Reproducible builds**: we exploit [package sets](#whats-a-package-set) and [lock files](#the-lock-file) to make your build reproducible, so that if your project builds today it will also build tomorrow and every day after that. @@ -165,6 +173,7 @@ Some tools that inspired `spago` are: [Rust's Cargo][cargo], [Haskell's Stack][s We'd love your help, and welcome PRs and contributions! Some ideas for getting started: + - [Build and run `spago`](CONTRIBUTING.md#developing-spago) - [Help us fix bugs and build features](https://github.com/purescript/spago/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22+label%3A%22defect%22) - Help us improve our documentation @@ -172,14 +181,13 @@ Some ideas for getting started: For more details see the [`CONTRIBUTING.md`][contributing] - ## 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: +You'll need to use [spago-legacy] for this. ```bash # Install spago-legacy @@ -193,12 +201,18 @@ npm install spago@next rm spago.dhall packages.dhall ``` +> [!NOTE]\ +> Both `spago-legacy` and `spago` use the same NPM package name `spago`. The difference is their version numbers. `spago-legacy` stops at `spago@0.21.0` whereas `spago` is `spago@0.93.X`. +> If `spago-legacy` is installed globally, `spago` can be installed locally via `npm i spago@next` and then used by prefixing `spago` commands with `npx` (e.g. `npx spago build`). Vice versa also works. + Some packages might not be found or have the wrong version, in which case you'll have to carefully: + - try to run `spago install some-package` for packages found in the package set (see [how to query the set](#querying-package-sets)) - [add the packages that are missing from the set](#add-a-package-to-the-package-set) In **all** cases, you'll want to switch to the new Registry package sets, so replace something like this: + ```yaml workspace: package_set: @@ -206,6 +220,7 @@ workspace: ``` ...with this: + ```yaml workspace: package_set: @@ -218,6 +233,8 @@ The new package sets are instead pointing at the Registry, and can fetch compres To figure out what package set you're supposed to be using, see the section about [querying package sets](#querying-package-sets). +You might also want to check out the section about [differences from legacy spago](#differences-from-legacy-spago). + ### 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. @@ -307,7 +324,7 @@ $ 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 +_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 @@ -316,10 +333,13 @@ 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 +> > ```console > $ 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 > ``` @@ -342,23 +362,43 @@ 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 (akin to `pulp run`), just use `run`: +If you want to run the program, just use `run`: + ```console -# The main module defaults to "Main" -$ spago 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 +``` -# Or define your own module path to Main -$ spago run --main ModulePath.To.Main +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. -# And pass arguments through to `purs compile` -$ spago run --main ModulePath.To.Main --purs-args "--verbose-errors" +See [here](#the-configuration-file) for more info about this, but it allows us to instead write: -# Or pass arguments to the backend, in this case node -$ spago run -- arg1 arg2 +```console +# 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 +# exec_args: +# - "arg1" +# - "arg2" +$ spago run -p package-name ``` -It's also possible to configure these parameters in the configuration so you don't have to supply them at the command line. -See [here](#the-configuration-file) for more info about this. +Lastly, if you only have a single package defined in the workspace with these parameters defined in the config file, you can just run + +```console +spago run +``` ### Test my project @@ -514,7 +554,6 @@ workspace: > 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: ```yaml @@ -609,6 +648,7 @@ This package set could look something like this: The second format possible is what Spago calls a `LegacyPackageSet`, and it's simply a map from package names to the location of the package, described as the (4) option for how to specify `extra_packages` in the [configuration format section](#the-configuration-file). Something like this: + ```js { "legacy-package-style": { @@ -628,6 +668,38 @@ This is supported to allow for just using legacy package sets, and be able to au It is not recommended to craft your own package set in the legacy format - please use the `RemotePackageSet` format instead - but if you do just be aware that you'll need to include a package called `metadata` that has a version that matches the compiler version that the set is supposed to support. +### Graph the project modules and dependencies + +You can use the `graph` command to generate a graph of the modules and their dependencies: + +```console +$ spago graph modules +``` + +The same goes for packages: + +```console +$ 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: + +```console +$ spago graph packages --dot | dot -Tpng > graph.png +``` + +...which will generate something like this: + +![packages-graph](./test-fixtures/graph.png) + +Finally, the `graph` command is also able to return a topological sorting of the modules or packages, with the `--topo` flag: + +```console +$ spago graph modules --topo +``` + ### Monorepo support Spago supports ["monorepos"][luu-monorepo] (see [here][monorepo-tools] as well for more monorepo goodness), allowing you to split a pile of code @@ -637,6 +709,7 @@ The vast majority of Spago projects will contain only one package, defined in th It is however possible to define multiple packages in the same repository! The basic rules are: + - [a package](#whats-a-package) is defined by a `spago.yaml` file containing a `package` section. - there can be only one `workspace` section in the whole repository, which defines the "root" of the current [Spago Workspace](#the-workspace). This defines your package set/build plan. - Spago will autodetect all the packages inside the workspace @@ -670,6 +743,7 @@ Then your file tree might look like this: ``` Where: + - the top level `spago.yaml` could look like this: ```yaml @@ -684,9 +758,9 @@ Where: package: name: lib1 dependencies: - - effect - - console - - prelude + - effect + - console + - prelude ``` - then, assuming `lib2` depends on `lib1`, `lib2/spago.yaml` might look like this: @@ -695,14 +769,14 @@ Where: package: name: lib2 dependencies: - - effect - - console - - prelude - - lib1 # <------ Note the dependency here + - effect + - console + - prelude + - lib1 # <------ Note the dependency here tests: main: Test.Lib2.Main dependencies: - - spec + - spec ``` - and then `app1/spago.yaml` would look something like this: @@ -712,9 +786,9 @@ Where: name: app1 # Note that the app does not include all the dependencies that the lib included dependencies: - - prelude - - aff # This dep was not used by the library - - lib2 # And we have `lib2` as a dependency + - prelude + - aff # This dep was not used by the library + - lib2 # And we have `lib2` as a dependency ``` Given this setup, Spago will figure out that there are three separate packages in the repository. @@ -767,16 +841,18 @@ The file tree might look like this: ``` Where the `common/spago.yaml` is just a package with no workspace defined, as it's going to support both the JS and the Erlang backend: + ```yaml package: name: common dependencies: - - effect - - console - - prelude + - effect + - console + - prelude ``` Then the `client/spago.yaml` might look like this: + ```yaml workspace: package_set: @@ -787,12 +863,13 @@ workspace: package: name: client dependencies: - - prelude - - common - - halogen + - prelude + - common + - halogen ``` And the `server/spago.yaml` might look like this: + ```yaml workspace: package_set: @@ -805,12 +882,13 @@ workspace: package: name: server dependencies: - - prelude - - common - - erl-process + - prelude + - common + - erl-process ``` This all means that: + - there is a [Spago Workspace](#the-workspace) in the `client` folder, another one in the `server` folder, but none in the `common` folder - the `common` package is shared between the two workspaces, note that it's included as a local package in both - the `client` workspace uses the default JS package set, and the `server` workspace uses a Purerl package set @@ -824,13 +902,13 @@ Like this: package: name: mypackage dependencies: - - effect - - console - - prelude + - effect + - console + - prelude tests: main: Test.Main dependencies: - - spec + - spec ``` You can add more with `spago install --test-deps some-new-package`. @@ -844,6 +922,7 @@ This is a good-defaults wrapper into `esbuild`, and it's meant to be used for bu See the [`esbuild` getting started][install-esbuild] for installation instructions. This command supports a few options, and the most important ones are: + - the `--bundle-type` flag, which can be either `app` or `module` - the `--platform` flag, which can be either `browser` or `node` @@ -869,6 +948,7 @@ $ spago bundle --bundle-type module --main Main --outfile index.js ``` Can now import it in your Node project: + ```console $ node -e "import('./index.js').then(m => console.log(m.main))" [Function] @@ -879,23 +959,28 @@ $ node -e "import('./index.js').then(m => console.log(m.main))" When bundling, you can include `--source-maps` to generate a final source map for your bundle. Example: + ```console 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](https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--enable-source-maps -) when executing your script: + +If your target platform is node, then you need to ensure your node version is >= 12.2.0 and [enable source maps](https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--enable-source-maps) when executing your script: + ```console 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: + ```console spago bundle -p my-project --platform browser --source-maps --minify --outfile=public/bundle.js ``` @@ -920,6 +1005,7 @@ The file itself is stored in the `.spago` folder if you'd like to have a look at To build documentation for your project and its dependencies (i.e. a "project-local [Pursuit][pursuit]"), you can use the `docs` command: + ```console $ spago docs ``` @@ -928,16 +1014,24 @@ This will generate all the documentation in the `./generated-docs` folder of you You might then want to open the `index.html` file in there. If you wish for the documentation to be opened in browser when generated, you can pass an `open` flag: + ```console $ spago docs --open ``` -To build the documentation as Markdown instead of HTML, or to generate tags for your project, -you can pass a `format` flag: +You can customize the output to other formats beyond html. Supported formats include ctags, etags, and markdown. +For example to generate ctags for use in your editor: + ```console $ spago docs --format ctags ``` +Sometimes you'd like to pull up docs for dependencies even when you have compilation errors in your project. This is a good use case for the --deps-only flag: + +```console +$ spago docs --deps-only` +``` + ### Alternate backends Spago supports compiling with alternate purescript backends, such as [purerl]. @@ -973,6 +1067,11 @@ or alternatively if you don't want to edit your `~/.bashrc`: spago --bash-completion-script $(which spago) >> ~/.bash_completion ``` +> [!NOTE]\ +> If you installed Spago not with NPM, but with PNPM or some other package manager, this package manager might have bundled your installation and your package name in the script may end up being incorrect. +> For example, when installed with PNPM, the resulting script will reference incorrect package `bundle.js` instead of `spago`. +> If you're using something other than NPM, verify the referenced package name in the completions script. + ### Install autocompletions for `zsh` Autocompletions for `zsh` need to be somewhere in the `fpath` - you can see the folders @@ -998,8 +1097,11 @@ Then, reload completions with: compinit ``` -*Note*: you might need to call this multiple times for it to work. +> [!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 @@ -1008,6 +1110,7 @@ This section details some of the concepts that are useful to know when using Spa ### What's a "package"? Spago considers a "package" any folder that contains: + - a `spago.yaml` file with a valid `package` section - a `src` subfolder with PureScript source files @@ -1016,6 +1119,7 @@ That's all there is to it! You can have many of these in your repository if you' The above holds for "workspace packages", i.e. the packages for which you have the source locally, and inside your repository. These are the packages "in your project". Packages on which your project depends on can come from a few different sources: + - [the Registry][registry] - local packages - i.e. packages that are on your filesystem but external to your repository - remote packages - i.e. packages that are not on your filesystem, but somewhere on the internet @@ -1031,6 +1135,7 @@ Packages have "dependencies", which are other packages that are required for the The most generic way of defining a "package set" is "a collection of package versions that are known to build together". The point of a package set is to provide a "stable" set of packages that you can use to build your project, without worrying about version conflicts. In practice, it looks something like [this][sample-package-set]: + ```json { "version": "41.2.0", @@ -1067,6 +1172,7 @@ For any software project, it's usually possible to find a clear line between "th 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.\ @@ -1075,6 +1181,7 @@ A very succint introduction to this idea can be found [in Bazel's documentation] 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: + ```yaml workspace: {} ``` @@ -1082,6 +1189,7 @@ 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.: + ```yaml workspace: package_set: @@ -1190,18 +1298,18 @@ workspace: # Specify whether to censor warnings coming from the compiler # for files in the `.spago` directory`. # Optional and can be one of two possible values - censor_library_warnings: + censor_library_warnings: # Value 1: "all" - All warnings are censored all # Value 2: `NonEmptyArray (Either String { by_prefix :: String })` - # - String values: + # - String values: # censor warnings if the code matches this code - # - { by_prefix } values: - # censor warnings if the warning's message + # - { by_prefix } values: + # censor warnings if the warning's message # starts with the given text - CodeName - # Note: when using `by_prefix`, use the `>` for block-string: + # Note: when using `by_prefix`, use the `>` for block-string: # see https://yaml-multiline.info/ - by_prefix: > "Data.Map"'s `Semigroup instance` @@ -1242,22 +1350,22 @@ package: # Fail the build if this package's `dependencies` field has redundant/underspecified packages. # Optional boolean that defaults to `false`. pedantic_packages: false - + # Specify whether to censor warnings coming from the compiler # for files from this package. # Optional and can be one of two possible values - censor_project_warnings: + censor_project_warnings: # Value 1: "all" - All warnings are censored all # Value 2: `NonEmptyArray (Either String { by_prefix :: String })` - # - String values: + # - String values: # censor warnings if the code matches this code - # - { by_prefix } values: - # censor warnings if the warning's message + # - { by_prefix } values: + # censor warnings if the warning's message # starts with the given text - CodeName - # Note: when using `by_prefix`, use the `>` for block-string: + # Note: when using `by_prefix`, use the `>` for block-string: # see https://yaml-multiline.info/ - by_prefix: > "Data.Map"'s `Semigroup instance` @@ -1291,7 +1399,7 @@ package: # The entrypoint for the program main: Main # List of arguments to pass to the program - execArgs: + exec_args: - "--cli-arg" - "foo" @@ -1303,7 +1411,7 @@ package: dependencies: - foo # Optional list of arguments to pass to the test program - execArgs: + exec_args: - "--cli-arg" - "foo" @@ -1314,18 +1422,18 @@ package: # Specify whether to censor warnings coming from the compiler # for files from this package's test code. # Optional and can be one of two possible values - censor_test_warnings: + censor_test_warnings: # Value 1: "all" - All warnings are censored all # Value 2: `NonEmptyArray (Either String { by_prefix :: String })` - # - String values: + # - String values: # censor warnings if the code matches this code - # - { by_prefix } values: - # censor warnings if the warning's message + # - { by_prefix } values: + # censor warnings if the warning's message # starts with the given text - CodeName - # Note: when using `by_prefix`, use the `>` for block-string: + # Note: when using `by_prefix`, use the `>` for block-string: # see https://yaml-multiline.info/ - by_prefix: > "Data.Map"'s `Semigroup instance` @@ -1409,6 +1517,29 @@ there's enough demand). So this is the reason why if you or one of your dependencies need to depend on some "native" packages, you should run the appropriate package-manager for that (e.g. npm). +### Differences from legacy spago + +#### Watch mode + +Spago dropped support for the --watch flag in `spago build` and `spago test`. + +VSCode users are recommended to use the [Purescript IDE](purescript-ide) extension for seamless experiences with automatic rebuilds. + +Users of other editors, e.g. vim, emacs, etc., can make use of the underlying [LSP plugin](purescript-language-server). + +If you want a very simple drop in replacement for `spago test --watch`, you can use a general purpose tool such as [watchexec]: + +```console +watchexec -e purs,js,yaml -- spago test +``` + +#### `sources` in the configuration file + +The `sources` field in the configuration file does not exist anymore. + +Instead, Spago will look for a `src` folder in the package root, and will use that as the source folder, +and similarly for the `test` folder, using that for the test sources. + [jq]: https://jqlang.github.io/jq/ [acme]: https://hackage.haskell.org/package/acme-everything [pulp]: https://github.com/purescript-contrib/pulp @@ -1430,3 +1561,6 @@ packages, you should run the appropriate package-manager for that (e.g. npm). [bazel-workspace]: https://bazel.build/concepts/build-ref [purescript-overlay]: https://github.com/thomashoneyman/purescript-overlay [sample-package-set]: https://github.com/purescript/registry/blob/main/package-sets/41.2.0.json +[watchexec]: https://github.com/watchexec/watchexec#quick-start +[purescript-langugage-server]: https://github.com/nwolverson/purescript-language-server +[ide-purescript]: https://marketplace.visualstudio.com/items?itemName=nwolverson.ide-purescript diff --git a/bin/spago.yaml b/bin/spago.yaml index c8c62f9d1..044c767cb 100644 --- a/bin/spago.yaml +++ b/bin/spago.yaml @@ -1,7 +1,7 @@ package: name: spago-bin publish: - version: 0.93.17 + version: 0.93.18 license: BSD-3-Clause dependencies: - aff diff --git a/bin/src/Flags.purs b/bin/src/Flags.purs index 564ff987c..ef76036e5 100644 --- a/bin/src/Flags.purs +++ b/bin/src/Flags.purs @@ -136,6 +136,20 @@ json = <> O.help "Format the output as JSON" ) +dot :: Parser Boolean +dot = + O.switch + ( O.long "dot" + <> O.help "Format the output as a DOT (GraphViz) graph" + ) + +topo :: Parser Boolean +topo = + O.switch + ( O.long "topo" + <> O.help "Sort the output topologically" + ) + latest :: Parser Boolean latest = O.switch @@ -264,3 +278,10 @@ sourceMaps = ( O.long "source-maps" <> O.help "Creates a source map for your bundle" ) + +depsOnly :: Parser Boolean +depsOnly = + O.switch + ( O.long "deps-only" + <> O.help "Build depedencies only" + ) diff --git a/bin/src/Main.purs b/bin/src/Main.purs index 51112bd58..902e88bfd 100644 --- a/bin/src/Main.purs +++ b/bin/src/Main.purs @@ -4,17 +4,16 @@ import Spago.Prelude import Control.Monad.Reader as Reader import Data.Array as Array +import Data.Array.NonEmpty as NEA import Data.Array.NonEmpty as NonEmptyArray import Data.Codec.Argonaut.Common as CA.Common import Data.Foldable as Foldable -import Data.JSDate as JSDate import Data.List as List import Data.Map as Map import Data.Maybe as Maybe import Data.String as String import Effect.Aff as Aff -import Effect.Ref as Ref -import Node.FS.Stats (Stats(..)) +import Effect.Now as Now import Node.Path as Path import Node.Process as Process import Options.Applicative (CommandFields, Mod, Parser, ParserPrefs(..)) @@ -25,13 +24,16 @@ import Registry.Constants as Registry.Constants import Registry.ManifestIndex as ManifestIndex import Registry.Metadata as Metadata import Registry.PackageName as PackageName +import Registry.Version as Version import Spago.Bin.Flags as Flags import Spago.Command.Build as Build import Spago.Command.Bundle as Bundle import Spago.Command.Docs as Docs import Spago.Command.Fetch as Fetch +import Spago.Command.Graph (GraphModulesArgs, GraphPackagesArgs) +import Spago.Command.Graph as Graph import Spago.Command.Init as Init -import Spago.Command.Ls (LsDepsArgs, LsPackagesArgs) +import Spago.Command.Ls (LsPathsArgs, LsDepsArgs, LsPackagesArgs) import Spago.Command.Ls as Ls import Spago.Command.Publish as Publish import Spago.Command.Registry (RegistryInfoArgs, RegistrySearchArgs, RegistryPackageSetsArgs) @@ -175,6 +177,7 @@ data Command a | Fetch FetchArgs | Init InitArgs | Install InstallArgs + | LsPaths LsPathsArgs | LsDeps LsDepsArgs | LsPackages LsPackagesArgs | Publish PublishArgs @@ -186,6 +189,8 @@ data Command a | Sources SourcesArgs | Test TestArgs | Upgrade UpgradeArgs + | GraphModules GraphModulesArgs + | GraphPackages GraphPackagesArgs commandParser :: forall (a :: Row Type). String -> Parser (Command a) -> String -> Mod CommandFields (SpagoCmd a) commandParser command_ parser_ description_ = @@ -209,7 +214,6 @@ argParser = , commandParser "repl" (Repl <$> replArgsParser) "Start a REPL" , commandParser "publish" (Publish <$> publishArgsParser) "Publish a package" , commandParser "upgrade" (Upgrade <$> pure {}) "Upgrade to the latest package set, or to the latest versions of Registry packages" - , commandParser "docs" (Docs <$> docsArgsParser) "Generate docs for the project and its dependencies" , O.command "registry" ( O.info @@ -226,10 +230,20 @@ argParser = ( O.hsubparser $ Foldable.fold [ commandParser "packages" (LsPackages <$> lsPackagesArgsParser) "List packages available in the local package set" , commandParser "deps" (LsDeps <$> lsDepsArgsParser) "List dependencies of the project" + , commandParser "paths" (LsPaths <$> lsPathsArgsParser) "List the paths used by Spago" ] ) (O.progDesc "List packages or dependencies") ) + , O.command "graph" + ( O.info + ( O.hsubparser $ Foldable.fold + [ commandParser "modules" (GraphModules <$> graphModulesArgsParser) "Generate a graph of the project's modules" + , commandParser "packages" (GraphPackages <$> graphPackagesArgsParser) "Generate a graph of the project's dependencies" + ] + ) + (O.progDesc "Generate a graph of modules or dependencies") + ) ] {- @@ -363,8 +377,7 @@ publishArgsParser = docsArgsParser :: Parser DocsArgs docsArgsParser = Optparse.fromRecord - -- TODO: --deps-only - { depsOnly: pure false :: Parser Boolean + { depsOnly: Flags.depsOnly , open: O.switch ( O.long "open" <> O.short 'o' @@ -407,6 +420,25 @@ registryPackageSetsArgsParser = , latest: Flags.latest } +graphModulesArgsParser :: Parser GraphModulesArgs +graphModulesArgsParser = Optparse.fromRecord + { dot: Flags.dot + , json: Flags.json + , topo: Flags.topo + } + +graphPackagesArgsParser :: Parser GraphPackagesArgs +graphPackagesArgsParser = Optparse.fromRecord + { dot: Flags.dot + , json: Flags.json + , topo: Flags.topo + } + +lsPathsArgsParser :: Parser LsPathsArgs +lsPathsArgsParser = Optparse.fromRecord + { json: Flags.json + } + lsPackagesArgsParser :: Parser LsPackagesArgs lsPackagesArgsParser = Optparse.fromRecord { json: Flags.json @@ -441,11 +473,17 @@ parseArgs = do ) main :: Effect Unit -main = +main = do + startingTime <- Now.now + let + printVersion = do + logOptions <- mkLogOptions startingTime { noColor: false, quiet: false, verbose: false, offline: Offline } + runSpago { logOptions } do + logInfo BuildInfo.buildInfo.spagoVersion parseArgs >>= \c -> Aff.launchAff_ case c of Cmd'SpagoCmd (SpagoCmd globalArgs@{ offline } command) -> do - logOptions <- mkLogOptions globalArgs + logOptions <- mkLogOptions startingTime globalArgs runSpago { logOptions } case command of Sources args -> do { env } <- mkFetchEnv @@ -573,6 +611,8 @@ main = runSpago buildEnv (Build.run options) testEnv <- runSpago env (mkTestEnv args buildEnv) runSpago testEnv Test.run + LsPaths args -> do + runSpago { logOptions } $ Ls.listPaths args LsPackages args -> do let fetchArgs = { packages: mempty, selectedPackage: Nothing, ensureRanges: false, testDeps: false } { env: env@{ workspace }, fetchOpts } <- mkFetchEnv offline fetchArgs @@ -596,26 +636,32 @@ main = Upgrade _args -> do { env } <- mkFetchEnv offline { packages: mempty, selectedPackage: Nothing, ensureRanges: false, testDeps: false } runSpago env Upgrade.run + -- TODO: add selected to graph commands + GraphModules args -> do + { env, fetchOpts } <- mkFetchEnv offline { packages: mempty, selectedPackage: Nothing, ensureRanges: false, testDeps: false } + dependencies <- runSpago env (Fetch.run fetchOpts) + purs <- Purs.getPurs + runSpago { dependencies, logOptions, purs, workspace: env.workspace } (Graph.graphModules args) + GraphPackages args -> do + { env, fetchOpts } <- mkFetchEnv offline { packages: mempty, selectedPackage: Nothing, ensureRanges: false, testDeps: false } + dependencies <- runSpago env (Fetch.run fetchOpts) + purs <- Purs.getPurs + runSpago { dependencies, logOptions, purs, workspace: env.workspace } (Graph.graphPackages args) Cmd'VersionCmd v -> do when v printVersion where - printVersion = do - logOptions <- mkLogOptions { noColor: false, quiet: false, verbose: false, offline: Offline } - runSpago { logOptions } do - logInfo BuildInfo.buildInfo.spagoVersion - -mkLogOptions :: GlobalArgs -> Aff LogOptions -mkLogOptions { noColor, quiet, verbose } = do - supports <- liftEffect supportsColor - let color = and [ supports, not noColor ] - let - verbosity = - if quiet then - LogQuiet - else if verbose then - LogVerbose - else LogNormal - pure { color, verbosity } + mkLogOptions :: Instant -> GlobalArgs -> Aff LogOptions + mkLogOptions startingTime { noColor, quiet, verbose } = do + supports <- liftEffect supportsColor + let color = and [ supports, not noColor ] + let + verbosity = + if quiet then + LogQuiet + else if verbose then + LogVerbose + else LogNormal + pure { color, verbosity, startingTime } mkBundleEnv :: forall a. BundleArgs -> Spago (Fetch.FetchEnv a) (Bundle.BundleEnv ()) mkBundleEnv bundleArgs = do @@ -676,8 +722,8 @@ mkRunEnv runArgs { dependencies, purs } = do workspacePackages = Config.getWorkspacePackages workspace.packageSet in -- If there's only one package, select that one - case workspacePackages of - [ singlePkg ] -> pure singlePkg + case NEA.length workspacePackages of + 1 -> pure $ NEA.head workspacePackages _ -> do logDebug $ unsafeStringify workspacePackages die @@ -694,7 +740,7 @@ mkRunEnv runArgs { dependencies, purs } = do runConf f = selected.package.run >>= f moduleName = fromMaybe "Main" (runArgs.main <|> runConf _.main) - execArgs = fromMaybe [] (runArgs.execArgs <|> runConf _.execArgs) + execArgs = fromMaybe [] (runArgs.execArgs <|> runConf _.exec_args) runOptions = { moduleName @@ -724,7 +770,7 @@ mkTestEnv testArgs { dependencies, purs } = do testConf f = selected.package.test >>= f moduleName = fromMaybe "Test.Main" (testConf (_.main >>> Just)) - execArgs = fromMaybe [] (testArgs.execArgs <|> testConf _.execArgs) + execArgs = fromMaybe [] (testArgs.execArgs <|> testConf _.exec_args) in { moduleName , execArgs @@ -738,7 +784,7 @@ mkTestEnv testArgs { dependencies, purs } = do let workspacePackages = Config.getWorkspacePackages workspace.packageSet in - case Array.uncons (Array.filter (_.hasTests) workspacePackages) of + case Array.uncons (NonEmptyArray.filter (_.hasTests) workspacePackages) of Just { head, tail } -> pure $ map mkSelectedTest $ NonEmptyArray.cons' head tail Nothing -> die "No package found to test." @@ -799,8 +845,8 @@ mkPublishEnv dependencies = do workspacePackages = Config.getWorkspacePackages env.workspace.packageSet in -- If there's only one package, select that one - case workspacePackages of - [ singlePkg ] -> pure singlePkg + case NEA.length workspacePackages of + 1 -> pure $ NEA.head workspacePackages _ -> do logDebug $ unsafeStringify workspacePackages die @@ -818,7 +864,7 @@ mkReplEnv replArgs dependencies supportPackage = do let selected = case workspace.selected of - Just s -> [ s ] + Just s -> NEA.singleton s Nothing -> Config.getWorkspacePackages workspace.packageSet pure @@ -864,18 +910,44 @@ mkRegistryEnv offline = do -- Make sure we have git and purs git <- Git.getGit purs <- Purs.getPurs + { logOptions } <- ask + + -- Connect to the database - we need it to keep track of when to pull the Registry, + -- so we don't do it too often + db <- liftEffect $ Db.connect + { database: Paths.databasePath + , logger: \str -> Reader.runReaderT (logDebug $ "DB: " <> str) { logOptions } + } - -- we make a Ref for the Index so that we can memoize the lookup of packages - -- and we don't have to read it all together - indexRef <- liftEffect $ Ref.new (Map.empty :: Map PackageName (Map Version Manifest)) + -- we keep track of how old the latest pull was - if the last pull was recent enough + -- we just move on, otherwise run the fibers + fetchingFreshRegistry <- Registry.shouldFetchRegistryRepos db + when fetchingFreshRegistry do + -- clone the registry and index repo, or update them + logInfo "Refreshing the Registry Index..." + runSpago { logOptions, git, offline } $ parallelise + [ Git.fetchRepo { git: "https://github.com/purescript/registry-index.git", ref: "main" } Paths.registryIndexPath >>= case _ of + Right _ -> pure unit + Left _err -> logWarn "Couldn't refresh the registry-index, will proceed anyways" + , Git.fetchRepo { git: "https://github.com/purescript/registry.git", ref: "main" } Paths.registryPath >>= case _ of + Right _ -> pure unit + Left _err -> logWarn "Couldn't refresh the registry, will proceed anyways" + ] + + -- Now that we are up to date with the Registry we init/refresh the database + Registry.updatePackageSetsDb db + + -- Prepare the functions to read the manifests and metadata - here we memoize as much + -- as we can in the DB, so we don't have to read the files every time let + -- Manifests are immutable so we can just lookup in the DB or read from file if not there getManifestFromIndex :: PackageName -> Version -> Spago (LogEnv ()) (Maybe Manifest) getManifestFromIndex name version = do - indexMap <- liftEffect (Ref.read indexRef) - case Map.lookup name indexMap of - Just meta -> pure (Map.lookup version meta) + liftEffect (Db.getManifest db name version) >>= case _ of + Just manifest -> pure (Just manifest) Nothing -> do - -- if we don't have it we try reading it from file + -- if we don't have it we need to read it from file + -- (note that we have all the versions of a package in the same file) logDebug $ "Reading package from Index: " <> PackageName.print name maybeManifests <- liftAff $ ManifestIndex.readEntryFile Paths.registryIndexPath name manifests <- map (map (\m@(Manifest m') -> Tuple m'.version m)) case maybeManifests of @@ -884,50 +956,36 @@ mkRegistryEnv offline = do logWarn $ "Could not read package manifests from index, proceeding anyways. Error: " <> err pure [] let versions = Map.fromFoldable manifests - liftEffect (Ref.write (Map.insert name versions indexMap) indexRef) + -- and memoize it + for_ manifests \(Tuple _ manifest@(Manifest m)) -> do + logDebug $ "Inserting manifest in DB: " <> PackageName.print name <> " v" <> Version.print m.version + liftEffect $ Db.insertManifest db name m.version manifest pure (Map.lookup version versions) - -- same deal for the metadata files - metadataRef <- liftEffect $ Ref.new (Map.empty :: Map PackageName Metadata) + -- Metadata can change over time (unpublished packages, and new packages), so we need + -- to read it from file every time we have a fresh Registry let + metadataFromFile name = do + let metadataFilePath = Path.concat [ Paths.registryPath, Registry.Constants.metadataDirectory, PackageName.print name <> ".json" ] + logDebug $ "Reading metadata from file: " <> metadataFilePath + liftAff (FS.readJsonFile Metadata.codec metadataFilePath) + getMetadata :: PackageName -> Spago (LogEnv ()) (Either String Metadata) getMetadata name = do - metadataMap <- liftEffect (Ref.read metadataRef) - case Map.lookup name metadataMap of - Just meta -> pure (Right meta) - Nothing -> do + -- we first try reading it from the DB + liftEffect (Db.getMetadata db name) >>= case _ of + Just metadata | not fetchingFreshRegistry -> do + logDebug $ "Got metadata from DB: " <> PackageName.print name + pure (Right metadata) + _ -> do -- if we don't have it we try reading it from file - let metadataFilePath = Path.concat [ Paths.registryPath, Registry.Constants.metadataDirectory, PackageName.print name <> ".json" ] - logDebug $ "Reading metadata from file: " <> metadataFilePath - liftAff (FS.readJsonFile Metadata.codec metadataFilePath) >>= case _ of + metadataFromFile name >>= case _ of Left e -> pure (Left e) Right m -> do -- and memoize it - liftEffect (Ref.write (Map.insert name m metadataMap) metadataRef) + liftEffect (Db.insertMetadata db name m) pure (Right m) - { logOptions } <- ask - -- we keep track of how old the latest pull was - if the last pull was recent enough - -- we just move on, otherwise run the fibers - whenM shouldFetchRegistryRepos do - -- clone the registry and index repo, or update them - logInfo "Refreshing the Registry Index..." - runSpago { logOptions, git, offline } $ parallelise - [ Git.fetchRepo { git: "https://github.com/purescript/registry-index.git", ref: "main" } Paths.registryIndexPath >>= case _ of - Right _ -> pure unit - Left _err -> logWarn "Couldn't refresh the registry-index, will proceed anyways" - , Git.fetchRepo { git: "https://github.com/purescript/registry.git", ref: "main" } Paths.registryPath >>= case _ of - Right _ -> pure unit - Left _err -> logWarn "Couldn't refresh the registry, will proceed anyways" - ] - - -- Now that we are up to date with the Registry we init/refresh the database - db <- liftEffect $ Db.connect - { database: Db.databasePath - , logger: \str -> Reader.runReaderT (logDebug $ "DB: " <> str) { logOptions } - } - Registry.updatePackageSetsDb db - pure { getManifestFromIndex , getMetadata @@ -948,8 +1006,8 @@ mkLsEnv dependencies = do workspacePackages = Config.getWorkspacePackages workspace.packageSet in -- If there's only one package, select that one - case workspacePackages of - [ singlePkg ] -> pure singlePkg + case NEA.length workspacePackages of + 1 -> pure $ NEA.head workspacePackages _ -> do logDebug $ unsafeStringify workspacePackages die @@ -972,32 +1030,4 @@ mkDocsEnv args dependencies = do , open: args.open } -shouldFetchRegistryRepos :: forall a. Spago (LogEnv a) Boolean -shouldFetchRegistryRepos = do - let freshRegistryCanary = Path.concat [ Paths.globalCachePath, "fresh-registry-canary.txt" ] - FS.stat freshRegistryCanary >>= case _ of - Left err -> do - -- If the stat fails the file probably does not exist - logDebug [ "Could not stat " <> freshRegistryCanary, show err ] - -- in which case we touch it and fetch - touch freshRegistryCanary - pure true - Right (Stats { mtime }) -> do - -- it does exist here, see if it's old enough, and fetch if it is - now <- liftEffect $ JSDate.now - let minutes = 15.0 - let staleAfter = 1000.0 * 60.0 * minutes -- need this in millis - let isOldEnough = (JSDate.getTime now) > (JSDate.getTime mtime + staleAfter) - if isOldEnough then do - logDebug "Registry index is old, refreshing canary" - touch freshRegistryCanary - pure true - else do - logDebug "Registry index is fresh enough, moving on..." - pure false - where - touch path = do - FS.ensureFileSync path - FS.writeTextFile path "" - foreign import supportsColor :: Effect Boolean diff --git a/core/src/Config.purs b/core/src/Config.purs index 1e5303336..3229998cb 100644 --- a/core/src/Config.purs +++ b/core/src/Config.purs @@ -39,7 +39,6 @@ module Spago.Core.Config import Spago.Core.Prelude -import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty as NonEmptyArray import Data.Codec.Argonaut as CA import Data.Codec.Argonaut.Record as CAR @@ -57,6 +56,7 @@ import Registry.Range as Range import Registry.Sha256 as Sha256 import Registry.Version as Version import Spago.FS as FS +import Type.Proxy (Proxy(..)) type Config = { package :: Maybe PackageConfig @@ -64,10 +64,10 @@ type Config = } configCodec :: JsonCodec Config -configCodec = CAR.object "Config" - { package: CAR.optional packageConfigCodec - , workspace: CAR.optional workspaceConfigCodec - } +configCodec = CA.object "Config" + $ CA.recordPropOptional (Proxy :: _ "package") packageConfigCodec + $ CA.recordPropOptional (Proxy :: _ "workspace") workspaceConfigCodec + $ CA.record type PackageConfig = { name :: PackageName @@ -81,16 +81,16 @@ type PackageConfig = } packageConfigCodec :: JsonCodec PackageConfig -packageConfigCodec = CAR.object "PackageConfig" - { name: PackageName.codec - , description: CAR.optional CA.string - , dependencies: dependenciesCodec - , build: CAR.optional packageBuildOptionsCodec - , bundle: CAR.optional bundleConfigCodec - , run: CAR.optional runConfigCodec - , test: CAR.optional testConfigCodec - , publish: CAR.optional publishConfigCodec - } +packageConfigCodec = CA.object "PackageConfig" + $ CA.recordProp (Proxy :: _ "name") PackageName.codec + $ CA.recordPropOptional (Proxy :: _ "description") CA.string + $ CA.recordProp (Proxy :: _ "dependencies") dependenciesCodec + $ CA.recordPropOptional (Proxy :: _ "build") packageBuildOptionsCodec + $ CA.recordPropOptional (Proxy :: _ "bundle") bundleConfigCodec + $ CA.recordPropOptional (Proxy :: _ "run") runConfigCodec + $ CA.recordPropOptional (Proxy :: _ "test") testConfigCodec + $ CA.recordPropOptional (Proxy :: _ "publish") publishConfigCodec + $ CA.record type PublishConfig = { version :: Version @@ -101,28 +101,28 @@ type PublishConfig = } publishConfigCodec :: JsonCodec PublishConfig -publishConfigCodec = CAR.object "PublishConfig" - { version: Version.codec - , license: License.codec - , location: CAR.optional Location.codec - , include: CAR.optional (CA.array CA.string) - , exclude: CAR.optional (CA.array CA.string) - } +publishConfigCodec = CA.object "PublishConfig" + $ CA.recordProp (Proxy :: _ "version") Version.codec + $ CA.recordProp (Proxy :: _ "license") License.codec + $ CA.recordPropOptional (Proxy :: _ "location") Location.codec + $ CA.recordPropOptional (Proxy :: _ "include") (CA.array CA.string) + $ CA.recordPropOptional (Proxy :: _ "exclude") (CA.array CA.string) + $ CA.record type RunConfig = { main :: Maybe String - , execArgs :: Maybe (Array String) + , exec_args :: Maybe (Array String) } runConfigCodec :: JsonCodec RunConfig -runConfigCodec = CAR.object "RunConfig" - { main: CAR.optional CA.string - , execArgs: CAR.optional (CA.array CA.string) - } +runConfigCodec = CA.object "RunConfig" + $ CA.recordPropOptional (Proxy :: _ "main") CA.string + $ CA.recordPropOptional (Proxy :: _ "exec_args") (CA.array CA.string) + $ CA.record type TestConfig = { main :: String - , execArgs :: Maybe (Array String) + , exec_args :: Maybe (Array String) , dependencies :: Dependencies , censor_test_warnings :: Maybe CensorBuildWarnings , strict :: Maybe Boolean @@ -130,14 +130,14 @@ type TestConfig = } testConfigCodec :: JsonCodec TestConfig -testConfigCodec = CAR.object "TestConfig" - { main: CA.string - , execArgs: CAR.optional (CA.array CA.string) - , dependencies: dependenciesCodec - , censor_test_warnings: CAR.optional censorBuildWarningsCodec - , strict: CAR.optional CA.boolean - , pedantic_packages: CAR.optional CA.boolean - } +testConfigCodec = CA.object "TestConfig" + $ CA.recordProp (Proxy :: _ "main") CA.string + $ CA.recordPropOptional (Proxy :: _ "exec_args") (CA.array CA.string) + $ CA.recordPropOptional (Proxy :: _ "censor_test_warnings") censorBuildWarningsCodec + $ CA.recordPropOptional (Proxy :: _ "strict") CA.boolean + $ CA.recordPropOptional (Proxy :: _ "pedantic_packages") CA.boolean + $ CA.recordProp (Proxy :: _ "dependencies") dependenciesCodec + $ CA.record type BackendConfig = { cmd :: String @@ -145,10 +145,10 @@ type BackendConfig = } backendConfigCodec :: JsonCodec BackendConfig -backendConfigCodec = CAR.object "BackendConfig" - { cmd: CA.string - , args: CAR.optional (CA.array CA.string) - } +backendConfigCodec = CA.object "BackendConfig" + $ CA.recordProp (Proxy :: _ "cmd") CA.string + $ CA.recordPropOptional (Proxy :: _ "args") (CA.array CA.string) + $ CA.record type PackageBuildOptionsInput = { censor_project_warnings :: Maybe CensorBuildWarnings @@ -157,11 +157,11 @@ type PackageBuildOptionsInput = } packageBuildOptionsCodec :: JsonCodec PackageBuildOptionsInput -packageBuildOptionsCodec = CAR.object "PackageBuildOptionsInput" - { censor_project_warnings: CAR.optional censorBuildWarningsCodec - , strict: CAR.optional CA.boolean - , pedantic_packages: CAR.optional CA.boolean - } +packageBuildOptionsCodec = CA.object "PackageBuildOptionsInput" + $ CA.recordPropOptional (Proxy :: _ "censor_project_warnings") censorBuildWarningsCodec + $ CA.recordPropOptional (Proxy :: _ "strict") CA.boolean + $ CA.recordPropOptional (Proxy :: _ "pedantic_packages") CA.boolean + $ CA.record type BundleConfig = { minify :: Maybe Boolean @@ -173,14 +173,14 @@ type BundleConfig = } bundleConfigCodec :: JsonCodec BundleConfig -bundleConfigCodec = CAR.object "BundleConfig" - { minify: CAR.optional CA.boolean - , module: CAR.optional CA.string - , outfile: CAR.optional CA.string - , platform: CAR.optional bundlePlatformCodec - , type: CAR.optional bundleTypeCodec - , extra_args: CAR.optional (CA.array CA.string) - } +bundleConfigCodec = CA.object "BundleConfig" + $ CA.recordPropOptional (Proxy :: _ "minify") CA.boolean + $ CA.recordPropOptional (Proxy :: _ "module") CA.string + $ CA.recordPropOptional (Proxy :: _ "outfile") CA.string + $ CA.recordPropOptional (Proxy :: _ "platform") bundlePlatformCodec + $ CA.recordPropOptional (Proxy :: _ "type") bundleTypeCodec + $ CA.recordPropOptional (Proxy :: _ "extra_args") (CA.array CA.string) + $ CA.record data BundlePlatform = BundleNode | BundleBrowser @@ -292,13 +292,13 @@ type WorkspaceConfig = } workspaceConfigCodec :: JsonCodec WorkspaceConfig -workspaceConfigCodec = CAR.object "WorkspaceConfig" - { package_set: CAR.optional setAddressCodec - , extra_packages: CAR.optional (Internal.Codec.packageMap extraPackageCodec) - , backend: CAR.optional backendConfigCodec - , build_opts: CAR.optional buildOptionsCodec - , lock: CAR.optional CA.boolean - } +workspaceConfigCodec = CA.object "WorkspaceConfig" + $ CA.recordPropOptional (Proxy :: _ "lock") CA.boolean + $ CA.recordPropOptional (Proxy :: _ "package_set") setAddressCodec + $ CA.recordPropOptional (Proxy :: _ "backend") backendConfigCodec + $ CA.recordPropOptional (Proxy :: _ "build_opts") buildOptionsCodec + $ CA.recordPropOptional (Proxy :: _ "extra_packages") (Internal.Codec.packageMap extraPackageCodec) + $ CA.record type WorkspaceBuildOptionsInput = { output :: Maybe FilePath @@ -307,11 +307,11 @@ type WorkspaceBuildOptionsInput = } buildOptionsCodec :: JsonCodec WorkspaceBuildOptionsInput -buildOptionsCodec = CAR.object "WorkspaceBuildOptionsInput" - { output: CAR.optional CA.string - , censor_library_warnings: CAR.optional censorBuildWarningsCodec - , stat_verbosity: CAR.optional statVerbosityCodec - } +buildOptionsCodec = CA.object "WorkspaceBuildOptionsInput" + $ CA.recordPropOptional (Proxy :: _ "output") CA.string + $ CA.recordPropOptional (Proxy :: _ "censor_library_warnings") censorBuildWarningsCodec + $ CA.recordPropOptional (Proxy :: _ "stat_verbosity") statVerbosityCodec + $ CA.record data CensorBuildWarnings = CensorAllWarnings @@ -457,12 +457,12 @@ type GitPackage = } gitPackageCodec :: JsonCodec GitPackage -gitPackageCodec = CAR.object "GitPackage" - { git: CA.string - , ref: CA.string - , subdir: CAR.optional CA.string - , dependencies: CAR.optional dependenciesCodec - } +gitPackageCodec = CA.object "GitPackage" + $ CA.recordProp (Proxy :: _ "git") CA.string + $ CA.recordProp (Proxy :: _ "ref") CA.string + $ CA.recordPropOptional (Proxy :: _ "subdir") CA.string + $ CA.recordPropOptional (Proxy :: _ "dependencies") dependenciesCodec + $ CA.record -- | The format of a legacy packages.json package set entry for an individual -- | package. @@ -473,11 +473,11 @@ type LegacyPackageSetEntry = } legacyPackageSetEntryCodec :: JsonCodec LegacyPackageSetEntry -legacyPackageSetEntryCodec = CAR.object "LegacyPackageSetEntry" - { dependencies: CA.array PackageName.codec - , repo: CA.string - , version: CA.string - } +legacyPackageSetEntryCodec = CA.object "LegacyPackageSetEntry" + $ CA.recordProp (Proxy :: _ "repo") CA.string + $ CA.recordProp (Proxy :: _ "version") CA.string + $ CA.recordProp (Proxy :: _ "dependencies") (CA.array PackageName.codec) + $ CA.record readConfig :: forall a. FilePath -> Spago (LogEnv a) (Either String { doc :: YamlDoc Config, yaml :: Config }) readConfig path = do diff --git a/core/src/Log.purs b/core/src/Log.purs index f574b05d7..38c7a99f0 100644 --- a/core/src/Log.purs +++ b/core/src/Log.purs @@ -29,10 +29,18 @@ import Control.Monad.Reader (class MonadAsk) import Control.Monad.Reader as Reader import Data.Array ((:)) import Data.Array as Array +import Data.Array.NonEmpty (NonEmptyArray) +import Data.Array.NonEmpty (toArray) as NEA import Data.Codec.Argonaut (JsonCodec) +import Data.DateTime.Instant (Instant) +import Data.DateTime.Instant as Instant import Data.Either (Either(..)) +import Data.Int as Int import Data.Maybe (Maybe(..), fromMaybe) +import Data.Newtype (unwrap) import Data.String as String +import Data.String.Utils as Strings +import Data.Time.Duration (Milliseconds) import Data.Traversable (traverse) import Dodo (Doc, print, twoSpaces) import Dodo (indent, break) as DodoExport @@ -46,6 +54,7 @@ import Dodo.Box as Box import Effect.Class (class MonadEffect) import Effect.Class as Effect import Effect.Class.Console as Console +import Effect.Now as Now import Node.Process as Process import Registry.PackageName (PackageName) import Registry.PackageName as PackageName @@ -54,7 +63,11 @@ import Spago.Yaml as Yaml type LogEnv a = { logOptions :: LogOptions | a } -type LogOptions = { color :: Boolean, verbosity :: LogVerbosity } +type LogOptions = + { color :: Boolean + , verbosity :: LogVerbosity + , startingTime :: Instant + } data LogVerbosity = LogQuiet @@ -88,6 +101,9 @@ instance Loggable PackageName where instance Loggable a => Loggable (Array a) where toDoc = Log.lines <<< map toDoc +instance Loggable a => Loggable (NonEmptyArray a) where + toDoc = Log.lines <<< NEA.toArray <<< map toDoc + log :: forall a m. MonadEffect m => MonadAsk (LogEnv a) m => Log -> m Unit log { content, level } = do { logOptions } <- Reader.ask @@ -95,6 +111,13 @@ log { content, level } = do case logOptions.verbosity, level of LogQuiet, _ -> pure unit LogNormal, LogDebug -> pure unit + LogVerbose, _ -> do + now <- Effect.liftEffect $ Now.now + let + (timeDiff :: Milliseconds) = Instant.diff now logOptions.startingTime + millisDoc = Ansi.foreground Ansi.White $ Ansi.bold $ Log.text $ Strings.padStart 8 $ show $ Int.round $ unwrap timeDiff + contentWithTimeDiff = Log.text "[" <> millisDoc <> Log.text "ms]" <> Log.space <> content + Console.error $ printFn (Log.twoSpaces { pageWidth = 200 }) contentWithTimeDiff _, _ -> Console.error $ printFn (Log.twoSpaces { pageWidth = 200 }) content logInfo :: forall a b m. MonadEffect m => MonadAsk (LogEnv b) m => Loggable a => a -> m Unit diff --git a/core/src/Prelude.purs b/core/src/Prelude.purs index c7f5bc7cb..2ed332564 100644 --- a/core/src/Prelude.purs +++ b/core/src/Prelude.purs @@ -14,6 +14,7 @@ import Control.Monad.Reader (ask, asks) as Extra import Control.Monad.Reader (class MonadAsk, ReaderT, runReaderT) import Control.Monad.State (StateT) as Extra import Data.Array ((..)) as Extra +import Data.Array.NonEmpty (NonEmptyArray) as Extra import Data.Bifunctor (bimap, rmap, lmap) as Extra import Data.Codec.Argonaut (JsonCodec, JsonDecodeError) as Extra import Data.DateTime.Instant (Instant) as Extra diff --git a/flake.lock b/flake.lock index ba9650f99..d312da095 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1694499547, - "narHash": "sha256-R7xMz1Iia6JthWRHDn36s/E248WB1/je62ovC/dUVKI=", + "lastModified": 1697851979, + "narHash": "sha256-lJ8k4qkkwdvi+t/Xc6Fn74kUuobpu9ynPGxNZR6OwoA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e5f018cf150e29aac26c61dac0790ea023c46b24", + "rev": "5550a85a087c04ddcace7f892b0bdc9d8bb080c8", "type": "github" }, "original": { @@ -42,11 +42,11 @@ "slimlock": "slimlock" }, "locked": { - "lastModified": 1694484110, - "narHash": "sha256-QXxECuZwRl+Mr+7OfhQSmTWaKsI6RjvBqjqMOxMBtPg=", + "lastModified": 1697766752, + "narHash": "sha256-jTGJhKuv/KPsMOnfIUSqoTckf4jo3e+9e0XY2cvXbD4=", "owner": "thomashoneyman", "repo": "purescript-overlay", - "rev": "7591ffe2adb930305e5e7c4be19c3a7cc8a4ee7b", + "rev": "1a6676b94f064113980b142454e3eefeecce5dcc", "type": "github" }, "original": { @@ -70,11 +70,11 @@ ] }, "locked": { - "lastModified": 1688610262, - "narHash": "sha256-Wg0ViDotFWGWqKIQzyYCgayeH8s4U1OZcTiWTQYdAp4=", + "lastModified": 1688756706, + "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", "owner": "thomashoneyman", "repo": "slimlock", - "rev": "b5c6cdcaf636ebbebd0a1f32520929394493f1a6", + "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5f3a49582..e12d9d758 100644 --- a/flake.nix +++ b/flake.nix @@ -15,11 +15,13 @@ }; in { devShells.default = pkgs.mkShell { + name = "spago"; buildInputs = with pkgs; [ - purs-bin.purs-0_15_10 - # TODO: we need to get purescript-overlay to track the latest versions - # spago-unstable - purs-tidy-bin.purs-tidy-0_10_0 + purs + purs-tidy + purs-backend-es + spago-unstable + nodejs esbuild gh diff --git a/package-lock.json b/package-lock.json index 17c980d79..20e892b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "spago", - "version": "0.93.17", + "version": "0.93.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "spago", - "version": "0.93.17", + "version": "0.93.18", "license": "BSD-3-Clause", "dependencies": { "better-sqlite3": "^8.6.0", @@ -33,7 +33,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -44,14 +45,16 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -62,18 +65,21 @@ }, "node_modules/argparse": { "version": "2.0.1", - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/asn1": { "version": "0.2.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dependencies": { "safer-buffer": "~2.1.0" } }, "node_modules/balanced-match": { "version": "1.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -96,15 +102,16 @@ }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dependencies": { "tweetnacl": "^0.14.3" } }, "node_modules/better-sqlite3": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.6.0.tgz", - "integrity": "sha512-jwAudeiTMTSyby+/SfbHDebShbmC2MCH8mU2+DXi0WJfv13ypEJm47cd3kljmy/H130CazEvkf2Li//ewcMJ1g==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.7.0.tgz", + "integrity": "sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", @@ -113,7 +120,8 @@ }, "node_modules/big-integer": { "version": "1.6.51", - "license": "Unlicense", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "engines": { "node": ">=0.6" } @@ -138,7 +146,8 @@ }, "node_modules/bplist-parser": { "version": "0.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", "dependencies": { "big-integer": "^1.6.44" }, @@ -148,7 +157,8 @@ }, "node_modules/brace-expansion": { "version": "1.1.11", - "license": "MIT", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -156,7 +166,8 @@ }, "node_modules/braces": { "version": "3.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dependencies": { "fill-range": "^7.0.1" }, @@ -189,6 +200,8 @@ }, "node_modules/buildcheck": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", "optional": true, "engines": { "node": ">=10.0.0" @@ -196,7 +209,8 @@ }, "node_modules/bundle-name": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", "dependencies": { "run-applescript": "^5.0.0" }, @@ -209,17 +223,21 @@ }, "node_modules/chownr": { "version": "2.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "engines": { "node": ">=10" } }, "node_modules/concat-map": { "version": "0.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/cpu-features": { "version": "0.0.9", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", + "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -232,7 +250,8 @@ }, "node_modules/cross-spawn": { "version": "7.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -266,7 +285,8 @@ }, "node_modules/default-browser": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", "dependencies": { "bundle-name": "^3.0.0", "default-browser-id": "^3.0.0", @@ -282,7 +302,8 @@ }, "node_modules/default-browser-id": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", "dependencies": { "bplist-parser": "^0.2.0", "untildify": "^4.0.0" @@ -296,7 +317,8 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "engines": { "node": ">=12" }, @@ -322,14 +344,16 @@ }, "node_modules/entities": { "version": "2.1.0", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/env-paths": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -339,7 +363,8 @@ }, "node_modules/execa": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", @@ -368,7 +393,8 @@ }, "node_modules/fast-glob": { "version": "3.3.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -382,7 +408,8 @@ }, "node_modules/fastq": { "version": "1.15.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dependencies": { "reusify": "^1.0.4" } @@ -394,7 +421,8 @@ }, "node_modules/fill-range": { "version": "7.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -409,7 +437,8 @@ }, "node_modules/fs-extra": { "version": "10.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -421,7 +450,8 @@ }, "node_modules/fs-minipass": { "version": "2.1.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dependencies": { "minipass": "^3.0.0" }, @@ -431,7 +461,8 @@ }, "node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", - "license": "ISC", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dependencies": { "yallist": "^4.0.0" }, @@ -441,18 +472,21 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fuse.js": { "version": "6.6.2", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", "engines": { "node": ">=10" } }, "node_modules/get-stream": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "engines": { "node": ">=10" }, @@ -467,7 +501,8 @@ }, "node_modules/glob": { "version": "7.2.3", - "license": "ISC", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -485,7 +520,8 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dependencies": { "is-glob": "^4.0.1" }, @@ -495,11 +531,13 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "license": "ISC" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/human-signals": { "version": "4.3.1", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", "engines": { "node": ">=14.18.0" } @@ -525,7 +563,8 @@ }, "node_modules/inflight": { "version": "1.0.6", - "license": "ISC", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -533,7 +572,8 @@ }, "node_modules/inherits": { "version": "2.0.4", - "license": "ISC" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -542,7 +582,8 @@ }, "node_modules/is-docker": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "bin": { "is-docker": "cli.js" }, @@ -555,14 +596,16 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "engines": { "node": ">=0.10.0" } }, "node_modules/is-glob": { "version": "4.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { "is-extglob": "^2.1.1" }, @@ -572,7 +615,8 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dependencies": { "is-docker": "^3.0.0" }, @@ -588,14 +632,16 @@ }, "node_modules/is-number": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "engines": { "node": ">=0.12.0" } }, "node_modules/is-stream": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -605,7 +651,8 @@ }, "node_modules/is-wsl": { "version": "2.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dependencies": { "is-docker": "^2.0.0" }, @@ -615,7 +662,8 @@ }, "node_modules/is-wsl/node_modules/is-docker": { "version": "2.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "bin": { "is-docker": "cli.js" }, @@ -628,11 +676,13 @@ }, "node_modules/isexe": { "version": "2.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jsonfile": { "version": "6.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { "universalify": "^2.0.0" }, @@ -642,14 +692,16 @@ }, "node_modules/linkify-it": { "version": "3.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dependencies": { "uc.micro": "^1.0.1" } }, "node_modules/lru-cache": { "version": "6.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { "yallist": "^4.0.0" }, @@ -659,7 +711,8 @@ }, "node_modules/markdown-it": { "version": "12.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", @@ -673,22 +726,26 @@ }, "node_modules/mdurl": { "version": "1.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/merge-stream": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "node_modules/merge2": { "version": "1.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { "version": "4.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -699,7 +756,8 @@ }, "node_modules/mimic-fn": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "engines": { "node": ">=12" }, @@ -720,7 +778,8 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -738,14 +797,16 @@ }, "node_modules/minipass": { "version": "5.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "engines": { "node": ">=8" } }, "node_modules/minizlib": { "version": "2.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -756,7 +817,8 @@ }, "node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", - "license": "ISC", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dependencies": { "yallist": "^4.0.0" }, @@ -766,7 +828,8 @@ }, "node_modules/mkdirp": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "bin": { "mkdirp": "bin/cmd.js" }, @@ -781,7 +844,8 @@ }, "node_modules/nan": { "version": "2.18.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, "node_modules/napi-build-utils": { @@ -790,9 +854,9 @@ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, "node_modules/node-abi": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", - "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==", + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", + "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", "dependencies": { "semver": "^7.3.5" }, @@ -802,7 +866,8 @@ }, "node_modules/npm-run-path": { "version": "5.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", "dependencies": { "path-key": "^4.0.0" }, @@ -815,7 +880,8 @@ }, "node_modules/npm-run-path/node_modules/path-key": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "engines": { "node": ">=12" }, @@ -825,14 +891,16 @@ }, "node_modules/once": { "version": "1.4.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "6.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -845,7 +913,8 @@ }, "node_modules/open": { "version": "9.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", "dependencies": { "default-browser": "^4.0.0", "define-lazy-prop": "^3.0.0", @@ -861,21 +930,24 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "engines": { "node": ">=8" } }, "node_modules/picomatch": { "version": "2.3.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { "node": ">=8.6" }, @@ -927,6 +999,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -940,8 +1014,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/rc": { "version": "1.2.8", @@ -972,7 +1045,8 @@ }, "node_modules/reusify": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -980,7 +1054,8 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dependencies": { "glob": "^7.1.3" }, @@ -993,7 +1068,8 @@ }, "node_modules/run-applescript": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", "dependencies": { "execa": "^5.0.0" }, @@ -1006,7 +1082,8 @@ }, "node_modules/run-applescript/node_modules/execa": { "version": "5.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -1027,14 +1104,16 @@ }, "node_modules/run-applescript/node_modules/human-signals": { "version": "2.1.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "engines": { "node": ">=10.17.0" } }, "node_modules/run-applescript/node_modules/is-stream": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "engines": { "node": ">=8" }, @@ -1044,14 +1123,16 @@ }, "node_modules/run-applescript/node_modules/mimic-fn": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "engines": { "node": ">=6" } }, "node_modules/run-applescript/node_modules/npm-run-path": { "version": "4.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dependencies": { "path-key": "^3.0.0" }, @@ -1061,7 +1142,8 @@ }, "node_modules/run-applescript/node_modules/onetime": { "version": "5.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -1074,13 +1156,16 @@ }, "node_modules/run-applescript/node_modules/strip-final-newline": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "engines": { "node": ">=6" } }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -1095,7 +1180,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -1121,11 +1205,13 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { "version": "7.5.4", - "license": "ISC", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -1138,7 +1224,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -1148,14 +1235,16 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "engines": { "node": ">=8" } }, "node_modules/signal-exit": { "version": "3.0.7", - "license": "ISC" + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -1202,22 +1291,27 @@ }, "node_modules/spdx-exceptions": { "version": "2.3.0", - "license": "CC-BY-3.0" + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.13", - "license": "CC0-1.0" + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==" }, "node_modules/ssh2": { "version": "1.14.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz", + "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.6", @@ -1241,7 +1335,8 @@ }, "node_modules/strip-final-newline": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "engines": { "node": ">=12" }, @@ -1259,7 +1354,8 @@ }, "node_modules/supports-color": { "version": "9.4.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", "engines": { "node": ">=12" }, @@ -1269,7 +1365,8 @@ }, "node_modules/tar": { "version": "6.2.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -1315,7 +1412,8 @@ }, "node_modules/titleize": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", "engines": { "node": ">=12" }, @@ -1325,7 +1423,8 @@ }, "node_modules/tmp": { "version": "0.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dependencies": { "rimraf": "^3.0.0" }, @@ -1335,7 +1434,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": { "is-number": "^7.0.0" }, @@ -1356,22 +1456,26 @@ }, "node_modules/tweetnacl": { "version": "0.14.5", - "license": "Unlicense" + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "node_modules/uc.micro": { "version": "1.0.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/universalify": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "engines": { "node": ">= 10.0.0" } }, "node_modules/untildify": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "engines": { "node": ">=8" } @@ -1383,7 +1487,8 @@ }, "node_modules/which": { "version": "2.0.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dependencies": { "isexe": "^2.0.0" }, @@ -1396,22 +1501,26 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "license": "ISC" + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xhr2": { "version": "0.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", "engines": { "node": ">= 6" } }, "node_modules/yallist": { "version": "4.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "2.3.2", - "license": "ISC", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", "engines": { "node": ">= 14" } diff --git a/package.json b/package.json index f8185cf45..4c5331b7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spago", - "version": "0.93.17", + "version": "0.93.18", "license": "BSD-3-Clause", "description": "🍝 PureScript package manager and build tool", "keywords": [ diff --git a/spago.lock b/spago.lock index 73d55fb5e..2f4214c6a 100644 --- a/spago.lock +++ b/spago.lock @@ -138,6 +138,7 @@ workspace: - registry-lib - spago-core - strings + - stringutils - transformers - tuples - unsafe-coerce @@ -330,6 +331,7 @@ workspace: - tuples git: https://github.com/natefaubion/purescript-node-glob-basic.git ref: v1.2.2 + ordered-collections: 3.1.1 registry-foreign: git: https://github.com/purescript/registry-dev.git ref: 6a803c37577af368caa221a2a06d6be2079d32da @@ -1575,8 +1577,8 @@ packages: - tuples ordered-collections: type: registry - version: 3.0.0 - integrity: sha256-R9WddNBRPkY37gw8XkDCLX2vJ5eI9qdaWDdCp61r2+M= + version: 3.1.1 + integrity: sha256-boSYHmlz4aSbwsNN4VxiwCStc0t+y1F7BXmBS+1JNtI= dependencies: - arrays - foldable-traversable @@ -1975,6 +1977,17 @@ packages: - tuples - unfoldable - unsafe-coerce + stringutils: + type: registry + version: 0.0.12 + integrity: sha256-t63QWBlp49U0nRqUcFryKflSJsNKGTQAHKjn24/+ooI= + dependencies: + - arrays + - integers + - maybe + - partial + - prelude + - strings tailrec: type: registry version: 6.1.0 diff --git a/spago.yaml b/spago.yaml index d75387596..7f7385100 100644 --- a/spago.yaml +++ b/spago.yaml @@ -1,7 +1,7 @@ package: name: spago publish: - version: 0.93.17 + version: 0.93.18 license: BSD-3-Clause location: githubOwner: purescript @@ -43,6 +43,7 @@ package: - registry-lib - spago-core - strings + - stringutils - transformers - tuples - unsafe-coerce @@ -269,3 +270,4 @@ workspace: - refs - strings - tuples + ordered-collections: "3.1.1" diff --git a/src/Spago/BuildInfo.purs b/src/Spago/BuildInfo.purs index 25bcbaa1c..69e68d1dc 100644 --- a/src/Spago/BuildInfo.purs +++ b/src/Spago/BuildInfo.purs @@ -2,6 +2,7 @@ module Spago.BuildInfo where import Spago.Prelude +import Data.Array.NonEmpty as NEA import Data.String as String import Node.Path as Path import Registry.PackageName as PackageName @@ -40,7 +41,7 @@ writeBuildInfo = do { pursVersion: Version.print purs.version , packages: map mkPackageBuildInfo case workspace.selected of Just p -> [ p ] - Nothing -> Config.getWorkspacePackages workspace.packageSet + Nothing -> NEA.toUnfoldable $ Config.getWorkspacePackages workspace.packageSet } buildInfoString = mkBuildInfo buildInfo writeIt = FS.writeTextFile buildInfoPath buildInfoString diff --git a/src/Spago/Command/Build.purs b/src/Spago/Command/Build.purs index 6f9d5dcca..c071b503b 100644 --- a/src/Spago/Command/Build.purs +++ b/src/Spago/Command/Build.purs @@ -6,9 +6,9 @@ module Spago.Command.Build import Spago.Prelude -import Control.Monad.ST as ST +import Control.Alternative as Alternative import Data.Array as Array -import Data.Array.ST as STA +import Data.Array.NonEmpty as NEA import Data.Map as Map import Data.Set as Set import Data.Traversable (sequence) @@ -99,7 +99,7 @@ run opts = do let allDependencies = Fetch.toAllDependencies dependencies selectedPackages = case workspace.selected of - Just p -> [ p ] + Just p -> NEA.singleton p Nothing -> Config.getWorkspacePackages workspace.packageSet globs = getBuildGlobs { dependencies: allDependencies @@ -109,7 +109,7 @@ run opts = do } pathDecisions <- liftEffect $ sequence $ Psa.toPathDecisions { allDependencies - , selectedPackages + , selectedPackages: NEA.toArray selectedPackages , psaCliFlags , censorLibWarnings: workspace.buildOptions.censorLibWarnings } @@ -140,34 +140,28 @@ run opts = do logSuccess "Backend build succeeded." let - pedanticPkgs = ST.run do - arr <- STA.new - ST.foreach selectedPackages \p -> do - let reportSrc = pedanticPackages || (fromMaybe false $ p.package.build >>= _.pedantic_packages) - let reportTest = pedanticPackages || (fromMaybe false $ p.package.test >>= _.pedantic_packages) - when (reportSrc || reportTest) do - void $ STA.push (Tuple p { reportSrc, reportTest }) arr - STA.unsafeFreeze arr + pedanticPkgs = NEA.toArray selectedPackages # Array.mapMaybe \p -> do + let reportSrc = pedanticPackages || (fromMaybe false $ p.package.build >>= _.pedantic_packages) + let reportTest = pedanticPackages || (fromMaybe false $ p.package.test >>= _.pedantic_packages) + Alternative.guard (reportSrc || reportTest) + pure $ Tuple p { reportSrc, reportTest } unless (Array.null pedanticPkgs) do logInfo $ "Looking for unused and undeclared transitive dependencies..." eitherGraph <- Graph.runGraph globs opts.pursArgs graph <- either die pure eitherGraph env <- ask - -- TODO: when the entire workspace is built, - -- here we could go through all the workspace packages and run the check for each - -- The complication is that "dependencies" includes all the dependencies for all packages errors <- map Array.fold $ for pedanticPkgs \(Tuple selected options) -> do - Graph.toImportErrors selected options <$> runSpago (Record.union { graph, selected } env) Graph.checkImports + Graph.toImportErrors selected options <$> runSpago (Record.union { selected } env) (Graph.checkImports graph) unless (Array.null errors) do die' errors -- TODO: if we are building with all the packages (i.e. selected = Nothing), --- then we can use the graph to remove outdated modules from `output`! +-- then we could use the graph to remove outdated modules from `output`! type BuildGlobsOptions = { withTests :: Boolean , depsOnly :: Boolean - , selected :: Array WorkspacePackage + , selected :: NonEmptyArray WorkspacePackage , dependencies :: PackageMap } @@ -180,7 +174,7 @@ getBuildGlobs { selected, dependencies, withTests, depsOnly } = true -> [] false -> -- We just select all the workspace package globs, because it's (1) intuitive and (2) backwards compatible - workspacePackageGlob =<< selected + workspacePackageGlob =<< NEA.toArray selected testGlobs = case withTests of true -> WithTestGlobs diff --git a/src/Spago/Command/Bundle.purs b/src/Spago/Command/Bundle.purs index e52c59435..2a7bc0dab 100644 --- a/src/Spago/Command/Bundle.purs +++ b/src/Spago/Command/Bundle.purs @@ -58,8 +58,12 @@ run = do -- TODO: we might need to use `Path.relative selected.path output` instead of just output there mainPath = withForwardSlashes $ Path.concat [ output, opts.module, "index.js" ] + shebang = case opts.platform of + BundleNode -> "#!/usr/bin/env node\n\n" + _ -> "" + { input, entrypoint } = case opts.type of - BundleApp -> { entrypoint: [], input: Cmd.StdinWrite ("#!/usr/bin/env node\n\nimport { main } from './" <> mainPath <> "'; main();") } + BundleApp -> { entrypoint: [], input: Cmd.StdinWrite (shebang <> "import { main } from './" <> mainPath <> "'; main();") } BundleModule -> { entrypoint: [ mainPath ], input: Cmd.StdinNewPipe } execOptions = Cmd.defaultExecOptions { pipeStdin = input } diff --git a/src/Spago/Command/Fetch.purs b/src/Spago/Command/Fetch.purs index 2feff26a8..5dec62b3b 100644 --- a/src/Spago/Command/Fetch.purs +++ b/src/Spago/Command/Fetch.purs @@ -73,17 +73,17 @@ run :: forall a . FetchOpts -> Spago (FetchEnv a) PackageTransitiveDeps -run { packages, ensureRanges, isTest } = do - logDebug $ "Requested to install these packages: " <> printJson (CA.array PackageName.codec) packages +run { packages: packagesToInstall, ensureRanges, isTest } = do + logDebug $ "Requested to install these packages: " <> printJson (CA.array PackageName.codec) packagesToInstall { getMetadata, logOptions, workspace, offline } <- ask let - installingPackages = not $ Array.null packages + installingPackages = not $ Array.null packagesToInstall getSelectedPackageTransitiveDeps :: WorkspacePackage -> Spago (FetchEnv a) PackageMap getSelectedPackageTransitiveDeps selected = - getTransitiveDeps $ getWorkspacePackageDeps selected <> Dependencies (Map.fromFoldable $ map (_ /\ Nothing) packages) + getTransitiveDeps $ getWorkspacePackageDeps selected <> Dependencies (Map.fromFoldable $ map (_ /\ Nothing) packagesToInstall) -- lookup the dependencies in the package set, so we get their version numbers. { dependencies, transitiveDeps } <- case workspace.selected of @@ -118,6 +118,8 @@ run { packages, ensureRanges, isTest } = do when installingPackages do { configPath, package, yamlDoc } <- getPackageConfigPath "to install your packages in." let packageDependencies = Map.keys $ unwrap package.dependencies + -- Prevent users from installing a circular dependency + let packages = Array.filter (\p -> p /= package.name) packagesToInstall let overlappingPackages = Set.intersection packageDependencies (Set.fromFoldable packages) unless (Set.isEmpty overlappingPackages) do logWarn @@ -402,42 +404,45 @@ getTransitiveDepsFromPackageSet packageSet deps = do init :: TransitiveDepsResult init = { packages: (Map.empty :: Map PackageName Package), errors: mempty } - mergeResults :: TransitiveDepsResult -> TransitiveDepsResult -> TransitiveDepsResult - mergeResults r1 r2 = - { packages: Map.union r1.packages r2.packages - , errors: r1.errors <> r2.errors - } - - go :: Set PackageName -> PackageName -> StateT (Map PackageName (Map PackageName Package)) (Spago (FetchEnv a)) TransitiveDepsResult - go seen dep = - if (Set.member dep seen) then do - pure (init { errors { cycle = Set.singleton dep } }) + go :: Set PackageName -> PackageName -> StateT TransitiveDepsResult (Spago (FetchEnv a)) Unit + go seen dep = do + -- We stash packages that we encountered along the way in `seen`, + -- so if we see it again we have a cycle + if Set.member dep seen then do + State.modify_ $ cycleError dep else do - cache <- State.get - case Map.lookup dep cache of - Just allDeps -> - pure (init { packages = allDeps }) - Nothing -> - -- First look for the package in the set to get a version number out, - -- then use that version to look it up in the index and get the dependencies - case Map.lookup dep packageSet of - Nothing -> pure (init { errors { notInPackageSet = Set.singleton dep } }) - Just package -> do - maybeDeps <- State.lift $ memoisedGetPackageDependencies dep package - case maybeDeps of - Nothing -> pure (init { errors { notInIndex = Set.singleton dep } }) - Just dependenciesMap -> do - -- recur here, as we need to get the transitive tree, not just the first level - { packages: childDeps, errors } <- - for (Map.toUnfoldable dependenciesMap :: Array (Tuple PackageName Range)) (\(Tuple d _) -> go (Set.insert dep seen) d) - >>= (pure <<< foldl mergeResults init) - let allDeps = Map.insert dep package childDeps - when (Set.isEmpty (errors.cycle <> errors.notInIndex <> errors.notInPackageSet)) do - State.modify_ $ Map.insert dep allDeps - pure { packages: allDeps, errors } + -- If the package is a transitive dependency of some other package that + -- we already met, then we don't need to do the work again + alreadyRun <- Map.member dep <$> State.gets _.packages + when (not alreadyRun) + -- If we need to compute the dependencies from scratch instead, we first look + -- in the package set to get a version number out, then use that version to + -- look it up in the index and get the dependencies + case Map.lookup dep packageSet of + Nothing -> State.modify_ $ notInPackageSetError dep + Just package -> do + maybeDeps <- State.lift $ memoisedGetPackageDependencies dep package + case maybeDeps of + Nothing -> State.modify_ $ notInIndexError dep + Just dependenciesMap -> do + -- Compare errors before and after recursively running transitive deps + errors <- State.gets _.errors + + -- recur here, as we need to get the transitive tree, not just the first level + void $ forWithIndex dependenciesMap + (\dependency _ -> go (Set.insert dep seen) dependency) + + -- Errors may have changed after running through the child deps + errorsAfterTransitiveDeps <- State.gets _.errors + + -- Do not include the package if any child deps fail + when (errors == errorsAfterTransitiveDeps) do + State.modify_ \st -> st { packages = Map.insert dep package st.packages } { packages, errors } <- - for deps (\d -> State.evalStateT (go mempty d) Map.empty) >>= (pure <<< foldl mergeResults init) + State.execStateT + (for deps (go mempty)) + init when (not (Set.isEmpty errors.cycle)) do die $ "The following packages have circular dependencies:\n" <> foldMap printPackageError (Set.toUnfoldable errors.cycle :: Array PackageName) @@ -458,3 +463,16 @@ getVersionFromPackage :: Package -> Version getVersionFromPackage = case _ of RegistryVersion v -> v _ -> unsafeFromRight $ Version.parse "0.0.0" + +notInPackageSetError :: PackageName -> TransitiveDepsResult -> TransitiveDepsResult +notInPackageSetError dep result = result + { errors { notInPackageSet = Set.insert dep result.errors.notInPackageSet } } + +notInIndexError :: PackageName -> TransitiveDepsResult -> TransitiveDepsResult +notInIndexError dep result = result + { errors { notInIndex = Set.insert dep result.errors.notInIndex } } + +cycleError :: PackageName -> TransitiveDepsResult -> TransitiveDepsResult +cycleError dep result = result + { errors { cycle = Set.insert dep result.errors.cycle } } + diff --git a/src/Spago/Command/Graph.purs b/src/Spago/Command/Graph.purs new file mode 100644 index 000000000..c215e07b9 --- /dev/null +++ b/src/Spago/Command/Graph.purs @@ -0,0 +1,126 @@ +module Spago.Command.Graph where + +import Spago.Prelude + +import Data.Array as Array +import Data.Array.NonEmpty as NEA +import Data.Codec.Argonaut.Common as CA +import Data.Graph as Data.Graph +import Data.List as List +import Data.Map as Map +import Record as Record +import Registry.PackageName as PackageName +import Spago.Command.Build as Build +import Spago.Command.Fetch as Fetch +import Spago.Config (Workspace, WorkspacePackage) +import Spago.Config as Config +import Spago.Purs (Purs) +import Spago.Purs.Graph (ModuleGraphWithPackage, PackageGraph, ModuleGraphWithPackageNode) +import Spago.Purs.Graph as Graph + +type GraphEnv a = + { dependencies :: Fetch.PackageTransitiveDeps + , logOptions :: LogOptions + , workspace :: Workspace + , purs :: Purs + | a + } + +type GraphModulesArgs = + { dot :: Boolean + , json :: Boolean + , topo :: Boolean + } + +type GraphPackagesArgs = + { dot :: Boolean + , json :: Boolean + , topo :: Boolean + } + +graphModules :: forall a. GraphModulesArgs -> Spago (GraphEnv a) Unit +graphModules { dot, json, topo } = do + env@{ dependencies, workspace } <- ask + let allDependencies = Fetch.toAllDependencies dependencies + let selected = Config.getWorkspacePackages workspace.packageSet + let globs = Build.getBuildGlobs { selected, withTests: false, dependencies: allDependencies, depsOnly: false } + eitherGraph <- Graph.runGraph globs [] + graph <- either die pure eitherGraph + + moduleGraph <- runSpago (Record.union { selected } env) (Graph.getModuleGraphWithPackage graph) + case topo of + false -> output case dot, json of + true, _ -> OutputLines $ modulesToDot selected moduleGraph + _, true -> OutputJson Graph.moduleGraphCodec moduleGraph + _, _ -> OutputYaml Graph.moduleGraphCodec moduleGraph + true -> + let + list = List.reverse + $ Data.Graph.topologicalSort + $ Data.Graph.fromMap + $ map (\{ depends } -> Tuple unit $ List.fromFoldable depends) moduleGraph + in + output case json of + true -> OutputJson (CA.list CA.string) list + false -> OutputLines $ Array.fromFoldable list + +graphPackages :: forall a. GraphPackagesArgs -> Spago (GraphEnv a) Unit +graphPackages { dot, json, topo } = do + env@{ dependencies, workspace } <- ask + let allDependencies = Fetch.toAllDependencies dependencies + let selected = Config.getWorkspacePackages workspace.packageSet + let globs = Build.getBuildGlobs { selected, withTests: false, dependencies: allDependencies, depsOnly: false } + eitherGraph <- Graph.runGraph globs [] + graph <- either die pure eitherGraph + + packageGraph <- runSpago (Record.union { selected } env) (Graph.getPackageGraph graph) + case topo of + false -> output case dot, json of + true, _ -> OutputLines $ packagesToDot selected packageGraph + _, true -> OutputJson Graph.packageGraphCodec packageGraph + _, _ -> OutputYaml Graph.packageGraphCodec packageGraph + true -> + let + list = List.reverse + $ Data.Graph.topologicalSort + $ Data.Graph.fromMap + $ map (\{ depends } -> Tuple unit $ List.fromFoldable depends) packageGraph + in + output case json of + true -> OutputJson (CA.list PackageName.codec) list + false -> OutputLines $ map PackageName.print $ Array.fromFoldable list + +packagesToDot :: NonEmptyArray WorkspacePackage -> PackageGraph -> Array String +packagesToDot selected graph = + [ "strict digraph deps {", "node[shape=rect]", "splines=ortho" ] + <> workspacePackages + <> Array.foldMap packageToDot (Map.toUnfoldable graph) + <> [ "}" ] + where + -- Workspace packages get a dashed border + -- Note: we are calling `show` in a few places here because dot wants strings in quotes + workspacePackages = NEA.toArray selected # map \{ package: { name } } -> show (PackageName.print name) <> " [style=dashed];" + + -- Each package gets a line for each dependency + packageToDot :: Tuple PackageName { depends :: Set PackageName } -> Array String + packageToDot (Tuple name { depends }) = Array.fromFoldable depends + # map \dep -> show (PackageName.print name) <> " -> " <> show (PackageName.print dep) <> ";" + +modulesToDot :: NonEmptyArray WorkspacePackage -> ModuleGraphWithPackage -> Array String +modulesToDot selected graph = + [ "strict digraph modules {", "node[shape=rect]", "splines=ortho" ] + <> workspaceModules + <> Array.foldMap moduleToDot (Map.toUnfoldable graph) + <> [ "}" ] + where + -- Workspace modules get a dashed border + isInWorskpace :: ModuleGraphWithPackageNode -> Boolean + isInWorskpace node = NEA.any (\{ package: { name } } -> name == node.package) selected + workspaceModules = Map.filter isInWorskpace graph + # Map.toUnfoldable + # map \(Tuple moduleName _) -> show moduleName <> " [style=dashed];" + + -- Each module gets a line for each dependency + moduleToDot :: Tuple String ModuleGraphWithPackageNode -> Array String + moduleToDot (Tuple name { depends }) = Array.fromFoldable depends + # map \dep -> show name <> " -> " <> show dep <> ";" diff --git a/src/Spago/Command/Init.purs b/src/Spago/Command/Init.purs index 9d6114b76..5cc2adeb1 100644 --- a/src/Spago/Command/Init.purs +++ b/src/Spago/Command/Init.purs @@ -161,7 +161,7 @@ defaultConfig' opts = , run: Nothing , test: test <#> \{ moduleMain, censorTestWarnings, strict, pedanticPackages, dependencies: testDeps } -> { dependencies: fromMaybe (Dependencies Map.empty) testDeps - , execArgs: Nothing + , exec_args: Nothing , main: moduleMain , censor_test_warnings: censorTestWarnings , strict diff --git a/src/Spago/Command/Ls.purs b/src/Spago/Command/Ls.purs index 029a9be78..3439b053f 100644 --- a/src/Spago/Command/Ls.purs +++ b/src/Spago/Command/Ls.purs @@ -1,10 +1,19 @@ -module Spago.Command.Ls (listPackages, listPackageSet, LsEnv(..), LsDepsArgs, LsPackagesArgs) where +module Spago.Command.Ls + ( listPaths + , listPackages + , listPackageSet + , LsEnv(..) + , LsPathsArgs + , LsDepsArgs + , LsPackagesArgs + ) where import Spago.Prelude import Data.Codec.Argonaut as CA +import Data.Codec.Argonaut.Common as CAC import Data.Codec.Argonaut.Record as CAR -import Data.Foldable (elem) +import Data.Foldable (elem, traverse_) import Data.Map (filterKeys) import Data.Map as Map import Data.Tuple.Nested (type (/\)) @@ -15,6 +24,7 @@ import Registry.Version as Version import Spago.Command.Fetch as Fetch import Spago.Config (Package(..), PackageSet(..), Workspace, WorkspacePackage) import Spago.Config as Config +import Spago.Paths as Paths import Type.Proxy (Proxy(..)) type LsPackagesArgs = @@ -32,6 +42,10 @@ type LsDepsOpts = , transitive :: Boolean } +type LsPathsArgs = + { json :: Boolean + } + type LsSetEnv = { dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions @@ -45,6 +59,28 @@ type LsEnv = , selected :: WorkspacePackage } +listPaths :: LsPathsArgs -> Spago { logOptions :: LogOptions } Unit +listPaths { json } = do + logDebug "Running `listPaths`" + case json of + true -> + output $ OutputJson (CAC.map CA.string CA.string) $ Map.fromFoldable keyValuePairs + false -> + output $ OutputTable + { titles: [ "Name", "Path" ] + , rows: (\(Tuple k v) -> [ k, v ]) <$> keyValuePairs + } + where + keyValuePairs = + [ Tuple "Global cache path" Paths.globalCachePath + , Tuple "Global registry path" Paths.registryPath + , Tuple "Global registry index path" Paths.registryIndexPath + , Tuple "Global package sets path" Paths.packageSetsPath + , Tuple "Global database path" Paths.databasePath + , Tuple "Local cache path" Paths.localCachePath + , Tuple "Local cache packages path" Paths.localCachePackagesPath + ] + -- TODO: add LICENSE field listPackageSet :: LsPackagesArgs -> Spago LsSetEnv Unit diff --git a/src/Spago/Command/Publish.purs b/src/Spago/Command/Publish.purs index e67823de8..503e2952b 100644 --- a/src/Spago/Command/Publish.purs +++ b/src/Spago/Command/Publish.purs @@ -7,6 +7,7 @@ import Affjax.RequestBody as RequestBody import Affjax.ResponseFormat as ResponseFormat import Affjax.StatusCode (StatusCode(..)) import Data.Array as Array +import Data.Array.NonEmpty as NEA import Data.Codec.Argonaut as CA import Data.DateTime (DateTime) import Data.Formatter.DateTime as DateTime @@ -125,11 +126,11 @@ publish _args = do -- We then need to check that the dependency graph is accurate. If not, queue the errors let allDependencies = Fetch.toAllDependencies dependencies - let globs = Build.getBuildGlobs { selected: [ selected ], withTests: false, dependencies: allDependencies, depsOnly: false } + let globs = Build.getBuildGlobs { selected: NEA.singleton selected, withTests: false, dependencies: allDependencies, depsOnly: false } eitherGraph <- Graph.runGraph globs [] case eitherGraph of Right graph -> do - graphCheckErrors <- Graph.toImportErrors selected { reportSrc: true, reportTest: false } <$> runSpago (Record.union { graph, selected } env) Graph.checkImports + graphCheckErrors <- Graph.toImportErrors selected { reportSrc: true, reportTest: false } <$> runSpago (Record.union { selected } env) (Graph.checkImports graph) for_ graphCheckErrors addError Left err -> die err diff --git a/src/Spago/Command/Repl.purs b/src/Spago/Command/Repl.purs index e04a9e783..d3ab77bce 100644 --- a/src/Spago/Command/Repl.purs +++ b/src/Spago/Command/Repl.purs @@ -19,7 +19,7 @@ type ReplEnv a = , depsOnly :: Boolean , logOptions :: LogOptions , pursArgs :: Array String - , selected :: Array WorkspacePackage + , selected :: NonEmptyArray WorkspacePackage | a } diff --git a/src/Spago/Command/Run.purs b/src/Spago/Command/Run.purs index 3ca8ac883..813647785 100644 --- a/src/Spago/Command/Run.purs +++ b/src/Spago/Command/Run.purs @@ -9,6 +9,7 @@ module Spago.Command.Run import Spago.Prelude import Data.Array as Array +import Data.Array.NonEmpty as NEA import Data.Codec.Argonaut as CA import Data.Map as Map import Node.FS.Perms as Perms @@ -102,7 +103,7 @@ run = do , depsOnly: false -- Here we include tests as well, because we use this code for `spago run` and `spago test` , withTests: true - , selected: [ selected ] + , selected: NEA.singleton selected } Purs.graph globs [] >>= case _ of Left err -> logWarn $ "Could not decode the output of `purs graph`, error: " <> CA.printJsonDecodeError err diff --git a/src/Spago/Command/Sources.purs b/src/Spago/Command/Sources.purs index 6a3796921..240e84cb4 100644 --- a/src/Spago/Command/Sources.purs +++ b/src/Spago/Command/Sources.purs @@ -3,6 +3,7 @@ module Spago.Command.Sources where import Spago.Prelude import Data.Array as Array +import Data.Array.NonEmpty as NEA import Data.Codec.Argonaut as CA import Data.Map as Map import Spago.Command.Fetch (FetchEnv) @@ -18,7 +19,7 @@ run { json } = do -- lookup the dependencies in the package set, so we get their version numbers let selectedPackages = case workspace.selected of - Just selected -> [ selected ] + Just selected -> NEA.singleton selected Nothing -> Config.getWorkspacePackages workspace.packageSet deps = foldMap Fetch.getWorkspacePackageDeps selectedPackages diff --git a/src/Spago/Command/Test.purs b/src/Spago/Command/Test.purs index dcb7e5827..32355cfcf 100644 --- a/src/Spago/Command/Test.purs +++ b/src/Spago/Command/Test.purs @@ -2,7 +2,6 @@ module Spago.Command.Test where import Spago.Prelude -import Data.Array.NonEmpty (NonEmptyArray) import Registry.PackageName as PackageName import Spago.Command.Fetch as Fetch import Spago.Command.Run (Node) diff --git a/src/Spago/Config.js b/src/Spago/Config.js index 9e4460dd3..d722b4bac 100644 --- a/src/Spago/Config.js +++ b/src/Spago/Config.js @@ -12,7 +12,7 @@ export function addPackagesToConfigImpl(doc, isTest, newPkgs) { const deps = (() => { if (isTest) { - const test = getOrElse(pkg, "test", doc.createNode({ dependencies: [], main: "Test.Main" })); + const test = getOrElse(pkg, "test", doc.createNode({ main: "Test.Main", dependencies: [] })); return getOrElse(test, "dependencies", doc.createNode([])); } else { return getOrElse(pkg, "dependencies", doc.createNode([])) diff --git a/src/Spago/Config.purs b/src/Spago/Config.purs index 0837f5dea..5350c9a3c 100644 --- a/src/Spago/Config.purs +++ b/src/Spago/Config.purs @@ -26,6 +26,7 @@ import Affjax.Node as Http import Affjax.ResponseFormat as Response import Affjax.StatusCode (StatusCode(..)) import Data.Array as Array +import Data.Array.NonEmpty as NEA import Data.CodePoint.Unicode as Unicode import Data.Codec.Argonaut.Record as CAR import Data.Enum as Enum @@ -189,7 +190,8 @@ readWorkspace maybeSelectedPackage = do Git.isIgnored path >>= case _ of true -> pure $ Left path false -> pure $ Right path - { right: newSucceeded, left: ignored } <- partitionMap identity <$> traverse filterGitignored result.succeeded + { right: newSucceeded, left: ignored } <- partitionMap identity + <$> parTraverseSpago filterGitignored result.succeeded pure { succeeded: newSucceeded, failed: result.failed, ignored } unless (Array.null otherConfigPaths) do logDebug $ [ toDoc "Found packages at these paths:", Log.indent $ Log.lines (map toDoc otherConfigPaths) ] @@ -457,8 +459,9 @@ srcGlob = "src/**/*.purs" testGlob :: String testGlob = "test/**/*.purs" -getWorkspacePackages :: PackageSet -> Array WorkspacePackage -getWorkspacePackages = Array.mapMaybe extractWorkspacePackage <<< Map.toUnfoldable <<< case _ of +-- We can afford an unsafe here, if it's empty we have bigger problems +getWorkspacePackages :: PackageSet -> NonEmptyArray WorkspacePackage +getWorkspacePackages = unsafeFromJust <<< NEA.fromFoldable <<< Array.mapMaybe extractWorkspacePackage <<< Map.toUnfoldable <<< case _ of PackageSet m -> m Registry m -> m where diff --git a/src/Spago/Db.js b/src/Spago/Db.js index 3b71e22ae..e55d817c6 100644 --- a/src/Spago/Db.js +++ b/src/Spago/Db.js @@ -4,7 +4,7 @@ export const connectImpl = (path, logger) => { logger("Connecting to database at " + path); let db = new Database(path, { fileMustExist: false, - verbose: logger, + // verbose: logger, }); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); @@ -19,16 +19,24 @@ export const connectImpl = (path, logger) => { , packageName TEXT NOT NULL , packageVersion TEXT NOT NULL , PRIMARY KEY (packageSetVersion, packageName, packageVersion) - , FOREIGN KEY (packageSetVersion) REFERENCES package_sets(version))`).run(); - // TODO: this is here as a placeholder, but not settled yet - // db.prepare(`CREATE TABLE IF NOT EXISTS package_versions - // ( name TEXT NOT NULL - // , version TEXT NOT NULL - // , published INTEGER NOT NULL - // , date TEXT NOT NULL - // , manifest TEXT NOT NULL - // , location TEXT NOT NULL - // , PRIMARY KEY (name, version))`).run(); + , FOREIGN KEY (packageSetVersion) REFERENCES package_sets(version) + )`).run(); + db.prepare(`CREATE TABLE IF NOT EXISTS last_git_pull + ( key TEXT PRIMARY KEY NOT NULL + , date TEXT NOT NULL + )`).run(); + db.prepare(`CREATE TABLE IF NOT EXISTS package_metadata + ( name TEXT PRIMARY KEY NOT NULL + , metadata TEXT NOT NULL + )`).run(); + // it would be lovely if we'd have a foreign key on package_metadata, but that would + // require reading metadatas before manifests, which we can't always guarantee + db.prepare(`CREATE TABLE IF NOT EXISTS package_manifests + ( name TEXT NOT NULL + , version TEXT NOT NULL + , manifest TEXT NOT NULL + , PRIMARY KEY (name, version) + )`).run(); return db; }; @@ -38,12 +46,6 @@ export const insertPackageSetImpl = (db, packageSet) => { ).run(packageSet); }; -export const insertPackageVersionImpl = (db, packageVersion) => { - db.prepare( - "INSERT INTO package_versions (name, version, published, date, manifest, location) VALUES (@name, @version, @published, @date, @manifest, @location)" - ).run(packageVersion); -} - export const insertPackageSetEntryImpl = (db, packageSetEntry) => { db.prepare( "INSERT INTO package_set_entries (packageSetVersion, packageName, packageVersion) VALUES (@packageSetVersion, @packageName, @packageVersion)" @@ -64,17 +66,6 @@ export const selectPackageSetsImpl = (db) => { return row; } -export const selectPackageVersionImpl = (db, name, version) => { - const row = db - .prepare("SELECT * FROM package_versions WHERE name = ? AND version = ? LIMIT 1") - .get(name, version); - return row; -} - -export const unpublishPackageVersionImpl = (db, name, version) => { - db.prepare("UPDATE package_versions SET published = 0 WHERE name = ? AND version = ?").run(name, version); -} - export const selectPackageSetEntriesBySetImpl = (db, packageSetVersion) => { const row = db .prepare("SELECT * FROM package_set_entries WHERE packageSetVersion = ?") @@ -88,3 +79,40 @@ export const selectPackageSetEntriesByPackageImpl = (db, packageName, packageVer .all(packageName, packageVersion); return row; } + +export const getLastPullImpl = (db, key) => { + const row = db + .prepare("SELECT * FROM last_git_pull WHERE key = ? LIMIT 1") + .get(key); + return row?.date; +} + +export const updateLastPullImpl = (db, key, date) => { + db.prepare("INSERT OR REPLACE INTO last_git_pull (key, date) VALUES (@key, @date)").run({ key, date }); +} + +export const getManifestImpl = (db, name, version) => { + const row = db + .prepare("SELECT * FROM package_manifests WHERE name = ? AND version = ? LIMIT 1") + .get(name, version); + return row?.manifest; +} + +export const insertManifestImpl = (db, name, version, manifest) => { + db.prepare("INSERT OR IGNORE INTO package_manifests (name, version, manifest) VALUES (@name, @version, @manifest)").run({ name, version, manifest }); +} + +export const removeManifestImpl = (db, name, version) => { + db.prepare("DELETE FROM package_manifests WHERE name = ? AND version = ?").run(name, version); +} + +export const getMetadataImpl = (db, name) => { + const row = db + .prepare("SELECT * FROM package_metadata WHERE name = ? LIMIT 1") + .get(name); + return row?.metadata; +} + +export const insertMetadataImpl = (db, name, metadata) => { + db.prepare("INSERT OR REPLACE INTO package_metadata (name, metadata) VALUES (@name, @metadata)").run({ name, metadata }); +} diff --git a/src/Spago/Db.purs b/src/Spago/Db.purs index 9e51c4f2a..ba6bfaaab 100644 --- a/src/Spago/Db.purs +++ b/src/Spago/Db.purs @@ -5,46 +5,42 @@ module Spago.Db , PackageSetEntry , PackageVersion , connect - , databasePath - , databaseVersion - , selectPackageSets - , selectLatestPackageSetByCompiler + , getLastPull + , getManifest + , getMetadata + , insertManifest + , insertMetadata , insertPackageSet , insertPackageSetEntry , packageSetCodec + , selectLatestPackageSetByCompiler + , selectPackageSets + , updateLastPull ) where import Spago.Prelude import Data.Array as Array -import Data.Codec.Argonaut as Json import Data.Codec.Argonaut.Record as CA.Record import Data.DateTime (Date, DateTime(..)) import Data.DateTime as Date +import Data.Either as Either import Data.Formatter.DateTime as DateTime +import Data.Map as Map import Data.Nullable (Nullable) import Data.Nullable as Nullable -import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4) import Effect.Uncurried as Uncurried -import Node.Path as Path import Registry.Internal.Codec as Internal.Codec import Registry.Internal.Format as Internal.Format -import Registry.Location as Location import Registry.Manifest as Manifest +import Registry.Metadata as Metadata import Registry.PackageName as PackageName import Registry.Version as Version -import Spago.Paths as Paths -------------------------------------------------------------------------------- -- API --- | We should bump this number every time we change the database schema in a breaking way -databaseVersion :: Int -databaseVersion = 1 - -databasePath :: FilePath -databasePath = Path.concat [ Paths.globalCachePath, "spago.v" <> show databaseVersion <> ".sqlite" ] - type ConnectOptions = { database :: FilePath , logger :: String -> Effect Unit @@ -56,9 +52,6 @@ connect { database, logger } = Uncurried.runEffectFn2 connectImpl database (Uncu insertPackageSet :: Db -> PackageSet -> Effect Unit insertPackageSet db = Uncurried.runEffectFn2 insertPackageSetImpl db <<< packageSetToJs -insertPackageVersion :: Db -> PackageVersion -> Effect Unit -insertPackageVersion db = Uncurried.runEffectFn2 insertPackageVersionImpl db <<< packageVersionToJs - insertPackageSetEntry :: Db -> PackageSetEntry -> Effect Unit insertPackageSetEntry db = Uncurried.runEffectFn2 insertPackageSetEntryImpl db <<< packageSetEntryToJs @@ -72,14 +65,6 @@ selectLatestPackageSetByCompiler db compiler = do maybePackageSet <- Nullable.toMaybe <$> Uncurried.runEffectFn2 selectLatestPackageSetByCompilerImpl db (Version.print compiler) pure $ packageSetFromJs =<< maybePackageSet -selectPackageVersion :: Db -> PackageName -> Version -> Effect (Maybe PackageVersion) -selectPackageVersion db packageName version = do - maybePackageVersion <- Nullable.toMaybe <$> Uncurried.runEffectFn3 selectPackageVersionImpl db (PackageName.print packageName) (Version.print version) - pure $ packageVersionFromJs =<< maybePackageVersion - -unpublishPackageVersion :: Db -> PackageName -> Version -> Effect Unit -unpublishPackageVersion db packageName version = Uncurried.runEffectFn3 unpublishPackageVersionImpl db (PackageName.print packageName) (Version.print version) - selectPackageSetEntriesBySet :: Db -> Version -> Effect (Array PackageSetEntry) selectPackageSetEntriesBySet db packageSetVersion = do packageSetEntries <- Uncurried.runEffectFn2 selectPackageSetEntriesBySetImpl db (Version.print packageSetVersion) @@ -90,9 +75,38 @@ selectPackageSetEntriesByPackage db packageName version = do packageSetEntries <- Uncurried.runEffectFn3 selectPackageSetEntriesByPackageImpl db (PackageName.print packageName) (Version.print version) pure $ Array.mapMaybe packageSetEntryFromJs packageSetEntries +getLastPull :: Db -> String -> Effect (Maybe DateTime) +getLastPull db key = do + maybePull <- Nullable.toMaybe <$> Uncurried.runEffectFn2 getLastPullImpl db key + pure $ (Either.hush <<< DateTime.unformat Internal.Format.iso8601DateTime) =<< maybePull + +updateLastPull :: Db -> String -> DateTime -> Effect Unit +updateLastPull db key date = Uncurried.runEffectFn3 updateLastPullImpl db key (DateTime.format Internal.Format.iso8601DateTime date) + +getManifest :: Db -> PackageName -> Version -> Effect (Maybe Manifest) +getManifest db packageName version = do + maybeManifest <- Nullable.toMaybe <$> Uncurried.runEffectFn3 getManifestImpl db (PackageName.print packageName) (Version.print version) + pure $ (Either.hush <<< parseJson Manifest.codec) =<< maybeManifest + +insertManifest :: Db -> PackageName -> Version -> Manifest -> Effect Unit +insertManifest db packageName version manifest = Uncurried.runEffectFn4 insertManifestImpl db (PackageName.print packageName) (Version.print version) (printJson Manifest.codec manifest) + +getMetadata :: Db -> PackageName -> Effect (Maybe Metadata) +getMetadata db packageName = do + maybeMetadata <- Nullable.toMaybe <$> Uncurried.runEffectFn2 getMetadataImpl db (PackageName.print packageName) + pure $ (Either.hush <<< parseJson Metadata.codec) =<< maybeMetadata + +insertMetadata :: Db -> PackageName -> Metadata -> Effect Unit +insertMetadata db packageName metadata@(Metadata { unpublished }) = do + Uncurried.runEffectFn3 insertMetadataImpl db (PackageName.print packageName) (printJson Metadata.codec metadata) + -- we also do a pass of removing the cached manifests that have been unpublished + for_ (Map.toUnfoldable unpublished :: Array _) \(Tuple version _) -> do + Uncurried.runEffectFn3 removeManifestImpl db (PackageName.print packageName) (Version.print version) + -------------------------------------------------------------------------------- -- Table types and conversions +-- Note: bump `Paths.databaseVersion` every time we change the database schema in a breaking way data Db type PackageSetJs = @@ -151,25 +165,6 @@ packageSetFromJs p = hush do date <- map Date.date $ DateTime.unformat Internal.Format.iso8601Date p.date pure $ { version, compiler, date } -packageVersionToJs :: PackageVersion -> PackageVersionJs -packageVersionToJs { name, version, published, date, manifest, location } = - { name: PackageName.print name - , version: Version.print version - , published: if published then 1 else 0 - , date: DateTime.format Internal.Format.iso8601DateTime date - , manifest: printJson Manifest.codec manifest - , location: printJson Location.codec location - } - -packageVersionFromJs :: PackageVersionJs -> Maybe PackageVersion -packageVersionFromJs p = hush do - name <- PackageName.parse p.name - version <- Version.parse p.version - date <- DateTime.unformat Internal.Format.iso8601DateTime p.date - manifest <- lmap Json.printJsonDecodeError $ parseJson Manifest.codec p.manifest - location <- lmap Json.printJsonDecodeError $ parseJson Location.codec p.location - pure $ { name, version, published: p.published == 1, date, manifest, location } - packageSetEntryToJs :: PackageSetEntry -> PackageSetEntryJs packageSetEntryToJs { packageSetVersion, packageName, packageVersion } = { packageSetVersion: Version.print packageSetVersion @@ -201,18 +196,26 @@ foreign import connectImpl :: EffectFn2 FilePath (EffectFn1 String Unit) Db foreign import insertPackageSetImpl :: EffectFn2 Db PackageSetJs Unit -foreign import insertPackageVersionImpl :: EffectFn2 Db PackageVersionJs Unit - foreign import insertPackageSetEntryImpl :: EffectFn2 Db PackageSetEntryJs Unit foreign import selectLatestPackageSetByCompilerImpl :: EffectFn2 Db String (Nullable PackageSetJs) foreign import selectPackageSetsImpl :: EffectFn1 Db (Array PackageSetJs) -foreign import selectPackageVersionImpl :: EffectFn3 Db String String (Nullable PackageVersionJs) - -foreign import unpublishPackageVersionImpl :: EffectFn3 Db String String Unit - foreign import selectPackageSetEntriesBySetImpl :: EffectFn2 Db String (Array PackageSetEntryJs) foreign import selectPackageSetEntriesByPackageImpl :: EffectFn3 Db String String (Array PackageSetEntryJs) + +foreign import getLastPullImpl :: EffectFn2 Db String (Nullable String) + +foreign import updateLastPullImpl :: EffectFn3 Db String String Unit + +foreign import getManifestImpl :: EffectFn3 Db String String (Nullable String) + +foreign import insertManifestImpl :: EffectFn4 Db String String String Unit + +foreign import removeManifestImpl :: EffectFn3 Db String String Unit + +foreign import getMetadataImpl :: EffectFn2 Db String (Nullable String) + +foreign import insertMetadataImpl :: EffectFn3 Db String String Unit diff --git a/src/Spago/Git.purs b/src/Spago/Git.purs index 6f3edf1da..a89d7a16d 100644 --- a/src/Spago/Git.purs +++ b/src/Spago/Git.purs @@ -108,7 +108,7 @@ tagCheckedOut :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc Strin tagCheckedOut cwd = do let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd } { git } <- ask - Cmd.exec git.cmd [ "describe", "--tags" ] opts >>= case _ of + Cmd.exec git.cmd [ "describe", "--tags", "--exact-match" ] opts >>= case _ of Left err -> pure $ Left $ toDoc "The git ref currently checked out is not a tag." Right res' -> pure $ Right res'.stdout diff --git a/src/Spago/Paths.purs b/src/Spago/Paths.purs index 38bbf0832..348e7efd6 100644 --- a/src/Spago/Paths.purs +++ b/src/Spago/Paths.purs @@ -41,11 +41,9 @@ registryIndexPath = Path.concat [ globalCachePath, "registry-index" ] packageSetsPath :: FilePath packageSetsPath = Path.concat [ registryPath, "package-sets" ] -localCachePersistedWarningsPath :: FilePath -localCachePersistedWarningsPath = Path.concat [ localCachePath, "persisted-warnings" ] +-- | We should bump this number every time we change the database schema in a breaking way +databaseVersion :: Int +databaseVersion = 1 -localCachesPersistedWarningsEntireWorkspace :: FilePath -localCachesPersistedWarningsEntireWorkspace = mkLocalCachesPersistentWarningsFile "entire-workspace" - -mkLocalCachesPersistentWarningsFile :: String -> FilePath -mkLocalCachesPersistentWarningsFile fileName = Path.concat [ localCachePersistedWarningsPath, fileName <> ".stash" ] +databasePath :: FilePath +databasePath = Path.concat [ globalCachePath, "spago.v" <> show databaseVersion <> ".sqlite" ] diff --git a/src/Spago/Prelude.purs b/src/Spago/Prelude.purs index f1419d05e..ceeaadaa6 100644 --- a/src/Spago/Prelude.purs +++ b/src/Spago/Prelude.purs @@ -1,9 +1,12 @@ module Spago.Prelude - ( module Spago.Core.Prelude - , HexString(..) + ( HexString(..) , OnlineStatus(..) - , parseLenientVersion + , mkTemp + , mkTemp' + , module Spago.Core.Prelude , parallelise + , parTraverseSpago + , parseLenientVersion , parseUrl , partitionEithers , shaToHex @@ -11,8 +14,6 @@ module Spago.Prelude , unsafeLog , unsafeStringify , withBackoff' - , mkTemp - , mkTemp' , withForwardSlashes ) where @@ -28,6 +29,7 @@ import Data.Int as Int import Data.Maybe as Maybe import Data.String (Pattern(..), Replacement(..)) import Data.String as String +import Data.Traversable (class Traversable) import Effect.Aff as Aff import Effect.Now as Now import Node.Buffer as Buffer @@ -59,6 +61,11 @@ parallelise actions = do fibers <- liftAff $ Parallel.parSequence (map (Aff.forkAff <<< runSpago env) actions :: Array _) liftAff $ for_ fibers Aff.joinFiber +parTraverseSpago :: forall env a b t. Traversable t => (a -> Spago env b) -> t a -> Spago env (t b) +parTraverseSpago action t = do + env <- ask + liftAff $ Parallel.parTraverse (runSpago env <<< action) t + shaToHex :: Sha256 -> Effect HexString shaToHex s = do (buffer :: Buffer.Buffer) <- Buffer.fromString (Registry.Sha256.print s) UTF8 diff --git a/src/Spago/Purs/Graph.purs b/src/Spago/Purs/Graph.purs index 0307d8aa5..0bff6dfeb 100644 --- a/src/Spago/Purs/Graph.purs +++ b/src/Spago/Purs/Graph.purs @@ -4,16 +4,27 @@ module Spago.Purs.Graph , checkImports , toImportErrors , runGraph + , PackageGraph + , packageGraphCodec + , getPackageGraph + , ModuleGraphWithPackage + , ModuleGraphWithPackageNode + , moduleGraphCodec + , getModuleGraphWithPackage ) where import Spago.Prelude import Data.Array as Array -import Data.Codec.Argonaut as CA +import Data.Array.NonEmpty as NEA +import Data.Codec.Argonaut.Common as CA +import Data.Codec.Argonaut.Record as CAR import Data.Map as Map import Data.Set as Set import Data.String as String +import Record as Record import Registry.Foreign.FastGlob as Glob +import Registry.Internal.Codec as Internal.Codec import Registry.PackageName as PackageName import Spago.Command.Fetch as Fetch import Spago.Config (Package(..), WithTestGlobs(..), WorkspacePackage) @@ -24,12 +35,8 @@ import Spago.Purs (ModuleGraph(..), ModuleGraphNode, ModuleName, Purs) import Spago.Purs as Purs import Unsafe.Coerce (unsafeCoerce) -type ImportCheckResult = - { unused :: Set PackageName - , unusedTest :: Set PackageName - , transitive :: ImportedPackages - , transitiveTest :: ImportedPackages - } +-------------------------------------------------------------------------------- +-- Basics type PreGraphEnv a = { dependencies :: Fetch.PackageTransitiveDeps @@ -38,75 +45,64 @@ type PreGraphEnv a = | a } -type GraphEnv a = - { selected :: WorkspacePackage - , graph :: Purs.ModuleGraph +runGraph :: forall a. Set FilePath -> Array String -> Spago (PreGraphEnv a) (Either String Purs.ModuleGraph) +runGraph globs pursArgs = map (lmap toErrorMessage) $ Purs.graph globs pursArgs + where + toErrorMessage = append "Could not decode the output of `purs graph`, error: " <<< CA.printJsonDecodeError + +-------------------------------------------------------------------------------- +-- Graph enriched with the package names + +type PackageGraphEnv a = + { selected :: NonEmptyArray WorkspacePackage , dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions | a } -type PackageGraph = Map ModuleName PackageGraphNode - -type PackageGraphNode = +type ModuleGraphWithPackageNode = { path :: String , depends :: Array ModuleName , package :: PackageName } --- | For every Package that we depend on, we note which Module we are depending on, --- | and for each of them, we note from which Module we are importing it. --- | --- | Given code like --- | ``` --- | module MyModule --- | --- | import Prelude -- from the 'prelude' package --- | ``` --- | This value would be --- | `Map.singleton "prelude" $ Map.singleton "Prelude" $ Set.singleton "MyModule"`` -type ImportedPackages = Map PackageName (Map ModuleName (Set ModuleName)) +moduleGraphWithPackageNodeCodec :: JsonCodec ModuleGraphWithPackageNode +moduleGraphWithPackageNodeCodec = CAR.object "ModuleGraphNode" + { path: CA.string + , depends: CA.array CA.string + , package: PackageName.codec + } + +type ModuleGraphWithPackage = Map ModuleName ModuleGraphWithPackageNode -checkImports :: forall a. Spago (GraphEnv a) ImportCheckResult -checkImports = do - { graph: ModuleGraph graph, selected, dependencies } <- ask +moduleGraphCodec :: JsonCodec ModuleGraphWithPackage +moduleGraphCodec = Internal.Codec.strMap "ModuleGraphWithPackage" Just identity moduleGraphWithPackageNodeCodec - let declaredDependencies = unwrap selected.package.dependencies - let declaredTestDependencies = maybe Map.empty (unwrap <<< _.dependencies) selected.package.test - let packageName = selected.package.name - let testPackageName = unsafeCoerce (PackageName.print packageName <> ":test") +getModuleGraphWithPackage :: forall a. Purs.ModuleGraph -> Spago (PackageGraphEnv a) ModuleGraphWithPackage +getModuleGraphWithPackage (ModuleGraph graph) = do + { selected, dependencies } <- ask -- First compile the globs for each package, we get out a set of the modules that a package contains -- and we can have a map from path to a PackageName let + mkPackageEntry p = Tuple p.package.name (WorkspacePackage p) + mkTestPackageEntry p = Tuple (unsafeCoerce (PackageName.print p.package.name <> ":test")) (WorkspacePackage p) + testPackages = Map.fromFoldable $ map mkTestPackageEntry selected allDependencies = Fetch.toAllDependencies dependencies allPackages = allDependencies - # Map.insert testPackageName (WorkspacePackage selected) - # Map.insert packageName (WorkspacePackage selected) + # Map.union (Map.fromFoldable $ map mkPackageEntry selected) + # Map.union testPackages pathToPackage :: Map FilePath PackageName <- map (Map.fromFoldable <<< Array.fold) $ for (Map.toUnfoldable allPackages) \(Tuple name package) -> do -- Basically partition the modules of the current package by in src and test packages - let withTestGlobs = if name == testPackageName then OnlyTestGlobs else NoTestGlobs + let withTestGlobs = if (Set.member name (Map.keys testPackages)) then OnlyTestGlobs else NoTestGlobs globMatches :: Array FilePath <- map Array.fold $ traverse compileGlob (Config.sourceGlob withTestGlobs name package) pure $ map (\p -> Tuple p name) globMatches - -- Compile the globs for the project, we get the set of source files in the project - projectGlob :: Set FilePath <- map Set.fromFoldable do - map Array.fold $ traverse compileGlob (Config.sourceGlob NoTestGlobs packageName (WorkspacePackage selected)) - - -- Same but for tests - projectTestsGlob :: Set FilePath <- map Set.fromFoldable do - map Array.fold $ traverse compileGlob (Config.sourceGlob OnlyTestGlobs packageName (WorkspacePackage selected)) - let - -- The direct dependencies specified in the config - dependencyPackages = Map.mapMaybe (const (Just Map.empty)) declaredDependencies - dependencyTestPackages = Map.mapMaybe (const (Just Map.empty)) - $ Map.union declaredDependencies declaredTestDependencies - -- Using `pathToPackage`, add the PackageName to each module entry in the graph - addPackageInfo :: PackageGraph -> Tuple ModuleName ModuleGraphNode -> PackageGraph + addPackageInfo :: ModuleGraphWithPackage -> Tuple ModuleName ModuleGraphNode -> ModuleGraphWithPackage addPackageInfo pkgGraph (Tuple moduleName { path, depends }) = let -- Windows paths will need a conversion to forward slashes to be matched to globs @@ -118,55 +114,169 @@ checkImports = do maybe pkgGraph (\v -> Map.insert moduleName v pkgGraph) newVal packageGraph = foldl addPackageInfo Map.empty (Map.toUnfoldable graph :: Array _) - -- Filter this improved graph to only have the project modules - projectGraph = Map.filterWithKey (\_ { path } -> Set.member path projectGlob) packageGraph - projectTestsGraph = Map.filterWithKey (\_ { path } -> Set.member path projectTestsGlob) packageGraph + pure packageGraph - -- Go through all the modules in the project graph, figure out which packages each module depends on, - -- accumulate all of that in a single place - accumulateImported importedPkgs' (Tuple moduleName { depends }) = - let - accumulateDep importedPkgs importedModule = case Map.lookup importedModule packageGraph of - Nothing -> importedPkgs - -- Skip dependencies on modules in the same package, we are not interested in that - Just { package } | package == packageName -> importedPkgs - Just { package } | package == testPackageName -> importedPkgs - Just { package: importedPackage } -> Map.alter - ( case _ of - Nothing -> Just $ Map.singleton moduleName (Set.singleton importedModule) - Just p -> Just $ Map.alter - ( case _ of - Nothing -> Just $ Set.singleton importedModule - Just set -> Just $ Set.insert importedModule set - ) - moduleName - p - ) - importedPackage - importedPkgs - in - foldl accumulateDep importedPkgs' depends +compileGlob :: forall a. FilePath -> Spago (LogEnv a) (Array FilePath) +compileGlob sourcePath = do + { succeeded, failed } <- Glob.match Paths.cwd [ withForwardSlashes sourcePath ] + unless (Array.null failed) do + logDebug [ toDoc "Encountered some globs that are not in cwd, proceeding anyways:", indent $ toDoc failed ] + pure (succeeded <> failed) + +-------------------------------------------------------------------------------- +-- Package graph + +type PackageGraph = Map PackageName { depends :: Set PackageName } + +packageGraphCodec :: JsonCodec PackageGraph +packageGraphCodec = Internal.Codec.packageMap (CAR.object "PackageGraphNode" { depends: CA.set PackageName.codec }) + +getPackageGraph :: forall a. Purs.ModuleGraph -> Spago (PackageGraphEnv a) PackageGraph +getPackageGraph graph = do + moduleGraphWithPackage <- getModuleGraphWithPackage graph + let + moduleToPackage moduleName = _.package <$> Map.lookup moduleName moduleGraphWithPackage + + moduleTupleToPackage { package: currentPackage, depends } = + Map.singleton currentPackage + -- No self-loops + { depends: Set.filter (\p -> p /= currentPackage) + $ Set.fromFoldable + $ Array.mapMaybe moduleToPackage depends + } + + packageGraph = + -- No psci-support in the graph + Map.filterKeys (\k -> Right k /= PackageName.parse "psci-support") + $ foldl (Map.unionWith (\v1 v2 -> { depends: Set.union v1.depends v2.depends })) Map.empty + $ map moduleTupleToPackage + $ Map.values moduleGraphWithPackage + + pure packageGraph - importedPackages :: ImportedPackages - importedPackages = foldl accumulateImported Map.empty (Map.toUnfoldable projectGraph :: Array _) - importedTestPackages = foldl accumulateImported Map.empty (Map.toUnfoldable projectTestsGraph :: Array _) +-------------------------------------------------------------------------------- +-- Graph of imported packages/modules - unused = Map.keys $ Map.difference dependencyPackages importedPackages - transitive = Map.difference importedPackages dependencyPackages - unusedTest = - if Set.isEmpty projectTestsGlob then Set.empty - else Map.keys $ Map.difference (Map.difference dependencyTestPackages dependencyPackages) importedTestPackages - transitiveTest = - if Set.isEmpty projectTestsGlob then Map.empty - else Map.difference importedTestPackages dependencyTestPackages +-- | For every Package that we depend on, we note which Module we are depending on, +-- | and for each of them, we note from which Module we are importing it. +-- | +-- | Given code like +-- | ``` +-- | module MyModule +-- | +-- | import Prelude -- from the 'prelude' package +-- | ``` +-- | This value would be +-- | `Map.singleton "prelude" $ Map.singleton "Prelude" $ Set.singleton "MyModule"`` +type ImportedPackages = Map PackageName (Map ModuleName (Set ModuleName)) + +type ImportsGraphEnv a = + { selected :: WorkspacePackage + , dependencies :: Fetch.PackageTransitiveDeps + , logOptions :: LogOptions + | a + } + +checkImports :: forall a. ModuleGraph -> Spago (ImportsGraphEnv a) ImportCheckResult +checkImports graph = do + env@{ selected } <- ask + packageGraph <- runSpago (Record.union { selected: NEA.singleton selected } env) $ getModuleGraphWithPackage graph + + let + dropValues = Map.mapMaybe (const (Just Map.empty)) + srcDeps = unwrap selected.package.dependencies + srcResult <- getUsedUnusedTransitiveFor + { selected + , packageName: selected.package.name + , dependencyPackages: dropValues srcDeps + , isSrc: true + , packageGraph + } + let srcDepsUsed = Map.filterKeys (flip Map.member srcResult.used) srcDeps + testResult <- getUsedUnusedTransitiveFor + { selected + , packageName: selected.package.name + , dependencyPackages: + dropValues + -- add the used source dependencies, not the declared ones. + $ Map.unionWith const srcDepsUsed + -- get test deps + $ maybe Map.empty (unwrap <<< _.dependencies) selected.package.test + , isSrc: false + , packageGraph + } + + pure + { unused: srcResult.unused + , transitive: srcResult.transitive + , unusedTest: Set.difference testResult.unused $ Map.keys srcResult.used + , transitiveTest: differenceAll testResult.transitive [ srcResult.used, srcResult.transitive ] + } + where + differenceAll sourceMap removalsArray = foldl Map.difference sourceMap removalsArray + getUsedUnusedTransitiveFor { selected, packageName, dependencyPackages, isSrc, packageGraph } = do + let + testPackageName = unsafeCoerce (PackageName.print packageName <> ":test") + + testGlobOption + | isSrc = NoTestGlobs + | otherwise = OnlyTestGlobs + + -- Compile the globs for the project, we get the set of source files in the project + glob :: Set FilePath <- map Set.fromFoldable do + map Array.fold $ traverse compileGlob (Config.sourceGlob testGlobOption packageName (WorkspacePackage selected)) + + let + -- Filter this improved graph to only have the project modules + projectGraph = Map.filterWithKey (\_ { path } -> Set.member path glob) packageGraph - pure { unused, transitive, unusedTest, transitiveTest } + -- Go through all the modules in the project graph, figure out which packages each module depends on, + -- accumulate all of that in a single place + accumulateImported importedPkgs' (Tuple moduleName { depends }) = + let + accumulateDep importedPkgs importedModule = case Map.lookup importedModule packageGraph of + Nothing -> importedPkgs + -- Skip dependencies on modules in the same package, we are not interested in that + Just { package } | package == packageName -> importedPkgs + Just { package } | package == testPackageName -> importedPkgs + Just { package: importedPackage } -> Map.alter + ( case _ of + Nothing -> Just $ Map.singleton moduleName (Set.singleton importedModule) + Just p -> Just $ Map.alter + ( case _ of + Nothing -> Just $ Set.singleton importedModule + Just set -> Just $ Set.insert importedModule set + ) + moduleName + p + ) + importedPackage + importedPkgs + in + foldl accumulateDep importedPkgs' depends + + importedPackages :: ImportedPackages + importedPackages = foldl accumulateImported Map.empty (Map.toUnfoldable projectGraph :: Array _) + + pure + { used: if isSrc then Map.intersection dependencyPackages importedPackages else Map.empty + , unused: Map.keys $ Map.difference dependencyPackages importedPackages + , transitive: Map.difference importedPackages dependencyPackages + } + +-------------------------------------------------------------------------------- +-- Errors + +type ImportCheckResult = + { unused :: Set PackageName + , unusedTest :: Set PackageName + , transitive :: ImportedPackages + , transitiveTest :: ImportedPackages + } toImportErrors :: WorkspacePackage - -> { reportSrc :: Boolean - , reportTest :: Boolean - } + -> { reportSrc :: Boolean, reportTest :: Boolean } -> ImportCheckResult -> Array Docc toImportErrors selected opts { unused, unusedTest, transitive, transitiveTest } = join @@ -176,18 +286,6 @@ toImportErrors selected opts { unused, unusedTest, transitive, transitiveTest } , if opts.reportTest && (not $ Map.isEmpty transitiveTest) then [ transitiveError true selected transitiveTest ] else [] ] -compileGlob :: forall a. FilePath -> Spago (LogEnv a) (Array FilePath) -compileGlob sourcePath = do - { succeeded, failed } <- Glob.match Paths.cwd [ withForwardSlashes sourcePath ] - unless (Array.null failed) do - logDebug [ toDoc "Encountered some globs that are not in cwd, proceeding anyways:", indent $ toDoc failed ] - pure (succeeded <> failed) - -runGraph :: forall a. Set FilePath -> Array String -> Spago (PreGraphEnv a) (Either String Purs.ModuleGraph) -runGraph globs pursArgs = map (lmap toErrorMessage) $ Purs.graph globs pursArgs - where - toErrorMessage = append "Could not decode the output of `purs graph`, error: " <<< CA.printJsonDecodeError - unusedError :: Boolean -> WorkspacePackage -> Set PackageName -> Docc unusedError isTest selected unused = toDoc [ toDoc $ (if isTest then "Tests for package '" else "Sources for package '") diff --git a/src/Spago/Registry.purs b/src/Spago/Registry.purs index db95e1c96..d723baf19 100644 --- a/src/Spago/Registry.purs +++ b/src/Spago/Registry.purs @@ -3,10 +3,13 @@ module Spago.Registry where import Spago.Prelude import Data.Array as Array +import Data.DateTime as DateTime import Data.Map as Map import Data.Set as Set import Data.String (Pattern(..)) import Data.String as String +import Data.Time.Duration (Minutes(..)) +import Effect.Now as Now import Node.Path as Path import Registry.PackageSet (PackageSet(..)) import Registry.PackageSet as PackageSet @@ -118,3 +121,28 @@ isVersionCompatible installedVersion minVersion = [ 0, b, c ], [ 0, y, z ] | b == y && c >= z -> true [ a, b, _c ], [ x, y, _z ] | a /= 0 && a == x && b >= y -> true _, _ -> false + +-- | Check if we have fetched the registry recently enough, so we don't hit the net all the time +shouldFetchRegistryRepos :: forall a. Db -> Spago (LogEnv a) Boolean +shouldFetchRegistryRepos db = do + now <- liftEffect $ Now.nowDateTime + let registryKey = "registry" + maybeLastRegistryFetch <- liftEffect $ Db.getLastPull db registryKey + case maybeLastRegistryFetch of + -- No record, so we have to fetch + Nothing -> do + logDebug "No record of last registry pull, will fetch" + liftEffect $ Db.updateLastPull db registryKey now + pure true + -- We have a record, so we check if it's old enough + Just lastRegistryFetch -> do + let staleAfter = Minutes 15.0 + let (timeDiff :: Minutes) = DateTime.diff now lastRegistryFetch + let isOldEnough = timeDiff > staleAfter + if isOldEnough then do + logDebug "Registry is old, refreshing" + liftEffect $ Db.updateLastPull db registryKey now + pure true + else do + logDebug "Registry is fresh enough, moving on..." + pure false diff --git a/test-fixtures/bundle-app-map.js b/test-fixtures/bundle-app-browser-map.js similarity index 78% rename from test-fixtures/bundle-app-map.js rename to test-fixtures/bundle-app-browser-map.js index 1d0e283fc..7f0244068 100755 --- a/test-fixtures/bundle-app-map.js +++ b/test-fixtures/bundle-app-browser-map.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node (() => { // output/Effect.Console/foreign.js var log = function(s) { @@ -13,4 +12,4 @@ // main(); })(); -//# sourceMappingURL=bundle-app-map.js.map +//# sourceMappingURL=bundle-app-browser-map.js.map diff --git a/test-fixtures/bundle-app-browser-map.js.map b/test-fixtures/bundle-app-browser-map.js.map new file mode 100644 index 000000000..2b20ce63d --- /dev/null +++ b/test-fixtures/bundle-app-browser-map.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["output/Effect.Console/foreign.js", "src/Main.purs", ""], + "sourcesContent": ["export const log = function (s) {\n return function () {\n console.log(s);\n };\n};\n\nexport const warn = function (s) {\n return function () {\n console.warn(s);\n };\n};\n\nexport const error = function (s) {\n return function () {\n console.error(s);\n };\n};\n\nexport const info = function (s) {\n return function () {\n console.info(s);\n };\n};\n\nexport const debug = function (s) {\n return function () {\n console.debug(s);\n };\n};\n\nexport const time = function (s) {\n return function () {\n console.time(s);\n };\n};\n\nexport const timeLog = function (s) {\n return function () {\n console.timeLog(s);\n };\n};\n\nexport const timeEnd = function (s) {\n return function () {\n console.timeEnd(s);\n };\n};\n\nexport const clear = function () {\n console.clear();\n};\n\nexport const group = function (s) {\n return function () {\n console.group(s);\n };\n};\n\nexport const groupCollapsed = function (s) {\n return function () {\n console.groupCollapsed(s);\n };\n};\n\nexport const groupEnd = function () {\n console.groupEnd();\n};\n", "module Main where\n\nimport Prelude\n\nimport Effect (Effect)\nimport Effect.Console (log)\n\nmain :: Effect Unit\nmain = do\n log \"\uD83C\uDF5D\"\n\n", "import { main } from './output/Main/index.js'; main();"], + "mappings": ";;AAAO,MAAM,MAAM,SAAU,GAAG;AAC9B,WAAO,WAAY;AACjB,cAAQ,IAAI,CAAC;AAAA,IACf;AAAA,EACF;;;ACGA,MAAA,OAAA,gBAAA,IAEM,WAAA;;;ACTyC,OAAK;", + "names": [] +} diff --git a/test-fixtures/bundle-app.js b/test-fixtures/bundle-app-browser.js similarity index 92% rename from test-fixtures/bundle-app.js rename to test-fixtures/bundle-app-browser.js index 2bc1320d3..fef3eca9a 100755 --- a/test-fixtures/bundle-app.js +++ b/test-fixtures/bundle-app-browser.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node (() => { // output/Effect.Console/foreign.js var log = function(s) { diff --git a/test-fixtures/bundle-app-node-map.js b/test-fixtures/bundle-app-node-map.js new file mode 100755 index 000000000..9afe7d224 --- /dev/null +++ b/test-fixtures/bundle-app-node-map.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node +import __module from 'module';import __path from 'path';import __url from 'url';const require = __module.createRequire(import.meta.url);const __dirname = __path.dirname(__url.fileURLToPath(import.meta.url));const __filename=new URL(import.meta.url).pathname + +// output/Effect.Console/foreign.js +var log = function(s) { + return function() { + console.log(s); + }; +}; + +// output/Main/index.js +var main = /* @__PURE__ */ log("\u{1F35D}"); + +// +main(); +//# sourceMappingURL=bundle-app-node-map.js.map diff --git a/test-fixtures/bundle-app-map.js.map b/test-fixtures/bundle-app-node-map.js.map similarity index 90% rename from test-fixtures/bundle-app-map.js.map rename to test-fixtures/bundle-app-node-map.js.map index e552335f2..50625c293 100644 --- a/test-fixtures/bundle-app-map.js.map +++ b/test-fixtures/bundle-app-node-map.js.map @@ -2,6 +2,6 @@ "version": 3, "sources": ["output/Effect.Console/foreign.js", "src/Main.purs", ""], "sourcesContent": ["export const log = function (s) {\n return function () {\n console.log(s);\n };\n};\n\nexport const warn = function (s) {\n return function () {\n console.warn(s);\n };\n};\n\nexport const error = function (s) {\n return function () {\n console.error(s);\n };\n};\n\nexport const info = function (s) {\n return function () {\n console.info(s);\n };\n};\n\nexport const debug = function (s) {\n return function () {\n console.debug(s);\n };\n};\n\nexport const time = function (s) {\n return function () {\n console.time(s);\n };\n};\n\nexport const timeLog = function (s) {\n return function () {\n console.timeLog(s);\n };\n};\n\nexport const timeEnd = function (s) {\n return function () {\n console.timeEnd(s);\n };\n};\n\nexport const clear = function () {\n console.clear();\n};\n\nexport const group = function (s) {\n return function () {\n console.group(s);\n };\n};\n\nexport const groupCollapsed = function (s) {\n return function () {\n console.groupCollapsed(s);\n };\n};\n\nexport const groupEnd = function () {\n console.groupEnd();\n};\n", "module Main where\n\nimport Prelude\n\nimport Effect (Effect)\nimport Effect.Console (log)\n\nmain :: Effect Unit\nmain = do\n log \"\uD83C\uDF5D\"\n\n", "#!/usr/bin/env node\n\nimport { main } from './output/Main/index.js'; main();"], - "mappings": ";;;AAAO,MAAM,MAAM,SAAU,GAAG;AAC9B,WAAO,WAAY;AACjB,cAAQ,IAAI,CAAC;AAAA,IACf;AAAA,EACF;;;ACGA,MAAA,OAAA,gBAAA,IAEM,WAAA;;;ACPyC,OAAK;", + "mappings": ";;;;AAAO,IAAM,MAAM,SAAU,GAAG;AAC9B,SAAO,WAAY;AACjB,YAAQ,IAAI,CAAC;AAAA,EACf;AACF;;;ACGA,IAAA,OAAA,gBAAA,IAEM,WAAA;;;ACPyC,KAAK;", "names": [] } diff --git a/test-fixtures/bundle-app-node.js b/test-fixtures/bundle-app-node.js new file mode 100755 index 000000000..960ab5a3b --- /dev/null +++ b/test-fixtures/bundle-app-node.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import __module from 'module';import __path from 'path';import __url from 'url';const require = __module.createRequire(import.meta.url);const __dirname = __path.dirname(__url.fileURLToPath(import.meta.url));const __filename=new URL(import.meta.url).pathname + +// output/Effect.Console/foreign.js +var log = function(s) { + return function() { + console.log(s); + }; +}; + +// output/Main/index.js +var main = /* @__PURE__ */ log("\u{1F35D}"); + +// +main(); diff --git a/test-fixtures/check-direct-import-transitive-dependency.txt b/test-fixtures/check-direct-import-transitive-dependency.txt deleted file mode 100644 index e77ac94da..000000000 --- a/test-fixtures/check-direct-import-transitive-dependency.txt +++ /dev/null @@ -1,28 +0,0 @@ -Reading Spago workspace configuration... -Read the package set from the registry - -✅ Selecting package to build: 7368613235362d34312f4e59746b7869335477336d33414d72 - -Downloading dependencies... -Building... - Src Lib All -Warnings 0 0 0 -Errors 0 0 0 - -✅ Build succeeded. - -Looking for unused and undeclared transitive dependencies... - -❌ Sources for package '7368613235362d34312f4e59746b7869335477336d33414d72' declares unused dependencies - please remove them from the project config: - - console - - effect - - -❌ Sources for package '7368613235362d34312f4e59746b7869335477336d33414d72' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: - control - from `Main`, which imports: - Control.Alt - - -Run the following command to install them all: - spago install -p 7368613235362d34312f4e59746b7869335477336d33414d72 control diff --git a/test-fixtures/check-strict.yaml b/test-fixtures/check-strict.yaml index d64bf8e32..62841a9b9 100644 --- a/test-fixtures/check-strict.yaml +++ b/test-fixtures/check-strict.yaml @@ -7,6 +7,6 @@ package: build: strict: true workspace: - extra_packages: {} package_set: registry: 41.5.0 + extra_packages: {} diff --git a/test-fixtures/graph-modules-topo.txt b/test-fixtures/graph-modules-topo.txt new file mode 100644 index 000000000..9dd8b4b93 --- /dev/null +++ b/test-fixtures/graph-modules-topo.txt @@ -0,0 +1,57 @@ +Control.Semigroupoid +Control.Category +Data.Boolean +Type.Proxy +Data.Symbol +Data.Unit +Record.Unsafe +Data.HeytingAlgebra +Data.Void +Data.Eq +Data.Semigroup +Data.Show +Data.Ordering +Data.Semiring +Data.Ring +Data.Ord +Data.Function +Data.Functor +Control.Apply +Control.Applicative +Control.Bind +Control.Monad +Data.BooleanAlgebra +Data.Bounded +Data.Generic.Rep +Data.Bounded.Generic +Data.CommutativeRing +Data.EuclideanRing +Data.DivisionRing +Data.Field +Data.Monoid +Data.NaturalTransformation +Prelude +Data.Eq.Generic +Data.HeytingAlgebra.Generic +Data.Monoid.Additive +Data.Monoid.Conj +Data.Monoid.Disj +Data.Monoid.Dual +Data.Monoid.Endo +Data.Monoid.Generic +Data.Monoid.Multiplicative +Data.Ord.Generic +Data.Reflectable +Data.Ring.Generic +Data.Semigroup.First +Data.Semigroup.Generic +Data.Semigroup.Last +Data.Semiring.Generic +Data.Show.Generic +Effect +Effect.Class +Effect.Console +Effect.Class.Console +Effect.Uncurried +Effect.Unsafe +Main diff --git a/test-fixtures/graph-modules.dot b/test-fixtures/graph-modules.dot new file mode 100644 index 000000000..5fde7e62a --- /dev/null +++ b/test-fixtures/graph-modules.dot @@ -0,0 +1,211 @@ +strict digraph modules { +node[shape=rect] +splines=ortho +"Main" [style=dashed]; +"Control.Applicative" -> "Control.Apply"; +"Control.Applicative" -> "Data.Functor"; +"Control.Applicative" -> "Data.Unit"; +"Control.Applicative" -> "Type.Proxy"; +"Control.Apply" -> "Data.Functor"; +"Control.Apply" -> "Data.Function"; +"Control.Apply" -> "Control.Category"; +"Control.Apply" -> "Type.Proxy"; +"Control.Bind" -> "Control.Applicative"; +"Control.Bind" -> "Control.Apply"; +"Control.Bind" -> "Control.Category"; +"Control.Bind" -> "Data.Function"; +"Control.Bind" -> "Data.Functor"; +"Control.Bind" -> "Data.Unit"; +"Control.Bind" -> "Type.Proxy"; +"Control.Category" -> "Control.Semigroupoid"; +"Control.Monad" -> "Control.Applicative"; +"Control.Monad" -> "Control.Apply"; +"Control.Monad" -> "Control.Bind"; +"Control.Monad" -> "Data.Functor"; +"Control.Monad" -> "Data.Unit"; +"Control.Monad" -> "Type.Proxy"; +"Data.BooleanAlgebra" -> "Data.HeytingAlgebra"; +"Data.BooleanAlgebra" -> "Data.Symbol"; +"Data.BooleanAlgebra" -> "Data.Unit"; +"Data.BooleanAlgebra" -> "Type.Proxy"; +"Data.Bounded" -> "Data.Ord"; +"Data.Bounded" -> "Data.Symbol"; +"Data.Bounded" -> "Data.Unit"; +"Data.Bounded" -> "Record.Unsafe"; +"Data.Bounded" -> "Type.Proxy"; +"Data.Bounded.Generic" -> "Data.Generic.Rep"; +"Data.Bounded.Generic" -> "Data.Bounded"; +"Data.CommutativeRing" -> "Data.Ring"; +"Data.CommutativeRing" -> "Data.Semiring"; +"Data.CommutativeRing" -> "Data.Symbol"; +"Data.CommutativeRing" -> "Data.Unit"; +"Data.CommutativeRing" -> "Type.Proxy"; +"Data.DivisionRing" -> "Data.EuclideanRing"; +"Data.DivisionRing" -> "Data.Ring"; +"Data.DivisionRing" -> "Data.Semiring"; +"Data.Eq" -> "Data.HeytingAlgebra"; +"Data.Eq" -> "Data.Symbol"; +"Data.Eq" -> "Data.Unit"; +"Data.Eq" -> "Data.Void"; +"Data.Eq" -> "Record.Unsafe"; +"Data.Eq" -> "Type.Proxy"; +"Data.Eq.Generic" -> "Prelude"; +"Data.Eq.Generic" -> "Data.Generic.Rep"; +"Data.EuclideanRing" -> "Data.BooleanAlgebra"; +"Data.EuclideanRing" -> "Data.CommutativeRing"; +"Data.EuclideanRing" -> "Data.Eq"; +"Data.EuclideanRing" -> "Data.Ring"; +"Data.EuclideanRing" -> "Data.Semiring"; +"Data.Field" -> "Data.DivisionRing"; +"Data.Field" -> "Data.CommutativeRing"; +"Data.Field" -> "Data.EuclideanRing"; +"Data.Field" -> "Data.Ring"; +"Data.Field" -> "Data.Semiring"; +"Data.Function" -> "Control.Category"; +"Data.Function" -> "Data.Boolean"; +"Data.Function" -> "Data.Ord"; +"Data.Function" -> "Data.Ring"; +"Data.Functor" -> "Data.Function"; +"Data.Functor" -> "Data.Unit"; +"Data.Functor" -> "Type.Proxy"; +"Data.Generic.Rep" -> "Data.Semigroup"; +"Data.Generic.Rep" -> "Data.Show"; +"Data.Generic.Rep" -> "Data.Symbol"; +"Data.Generic.Rep" -> "Data.Void"; +"Data.Generic.Rep" -> "Type.Proxy"; +"Data.HeytingAlgebra" -> "Data.Symbol"; +"Data.HeytingAlgebra" -> "Data.Unit"; +"Data.HeytingAlgebra" -> "Record.Unsafe"; +"Data.HeytingAlgebra" -> "Type.Proxy"; +"Data.HeytingAlgebra.Generic" -> "Prelude"; +"Data.HeytingAlgebra.Generic" -> "Data.Generic.Rep"; +"Data.HeytingAlgebra.Generic" -> "Data.HeytingAlgebra"; +"Data.Monoid" -> "Data.Boolean"; +"Data.Monoid" -> "Data.Eq"; +"Data.Monoid" -> "Data.EuclideanRing"; +"Data.Monoid" -> "Data.Ord"; +"Data.Monoid" -> "Data.Ordering"; +"Data.Monoid" -> "Data.Semigroup"; +"Data.Monoid" -> "Data.Symbol"; +"Data.Monoid" -> "Data.Unit"; +"Data.Monoid" -> "Record.Unsafe"; +"Data.Monoid" -> "Type.Proxy"; +"Data.Monoid.Additive" -> "Prelude"; +"Data.Monoid.Additive" -> "Data.Eq"; +"Data.Monoid.Additive" -> "Data.Ord"; +"Data.Monoid.Conj" -> "Prelude"; +"Data.Monoid.Conj" -> "Data.Eq"; +"Data.Monoid.Conj" -> "Data.HeytingAlgebra"; +"Data.Monoid.Conj" -> "Data.Ord"; +"Data.Monoid.Disj" -> "Prelude"; +"Data.Monoid.Disj" -> "Data.Eq"; +"Data.Monoid.Disj" -> "Data.HeytingAlgebra"; +"Data.Monoid.Disj" -> "Data.Ord"; +"Data.Monoid.Dual" -> "Prelude"; +"Data.Monoid.Dual" -> "Data.Eq"; +"Data.Monoid.Dual" -> "Data.Ord"; +"Data.Monoid.Endo" -> "Prelude"; +"Data.Monoid.Generic" -> "Data.Monoid"; +"Data.Monoid.Generic" -> "Data.Generic.Rep"; +"Data.Monoid.Multiplicative" -> "Prelude"; +"Data.Monoid.Multiplicative" -> "Data.Eq"; +"Data.Monoid.Multiplicative" -> "Data.Ord"; +"Data.Ord" -> "Data.Eq"; +"Data.Ord" -> "Data.Symbol"; +"Data.Ord" -> "Data.Ordering"; +"Data.Ord" -> "Data.Ring"; +"Data.Ord" -> "Data.Unit"; +"Data.Ord" -> "Data.Void"; +"Data.Ord" -> "Record.Unsafe"; +"Data.Ord" -> "Type.Proxy"; +"Data.Ord.Generic" -> "Prelude"; +"Data.Ord.Generic" -> "Data.Generic.Rep"; +"Data.Ordering" -> "Data.Eq"; +"Data.Ordering" -> "Data.Semigroup"; +"Data.Ordering" -> "Data.Show"; +"Data.Reflectable" -> "Data.Ord"; +"Data.Reflectable" -> "Type.Proxy"; +"Data.Ring" -> "Data.Semiring"; +"Data.Ring" -> "Data.Symbol"; +"Data.Ring" -> "Data.Unit"; +"Data.Ring" -> "Record.Unsafe"; +"Data.Ring" -> "Type.Proxy"; +"Data.Ring.Generic" -> "Prelude"; +"Data.Ring.Generic" -> "Data.Generic.Rep"; +"Data.Semigroup" -> "Data.Symbol"; +"Data.Semigroup" -> "Data.Unit"; +"Data.Semigroup" -> "Data.Void"; +"Data.Semigroup" -> "Record.Unsafe"; +"Data.Semigroup" -> "Type.Proxy"; +"Data.Semigroup.First" -> "Prelude"; +"Data.Semigroup.First" -> "Data.Eq"; +"Data.Semigroup.First" -> "Data.Ord"; +"Data.Semigroup.Generic" -> "Prelude"; +"Data.Semigroup.Generic" -> "Data.Generic.Rep"; +"Data.Semigroup.Last" -> "Prelude"; +"Data.Semigroup.Last" -> "Data.Eq"; +"Data.Semigroup.Last" -> "Data.Ord"; +"Data.Semiring" -> "Data.Symbol"; +"Data.Semiring" -> "Data.Unit"; +"Data.Semiring" -> "Record.Unsafe"; +"Data.Semiring" -> "Type.Proxy"; +"Data.Semiring.Generic" -> "Prelude"; +"Data.Semiring.Generic" -> "Data.Generic.Rep"; +"Data.Show" -> "Data.Semigroup"; +"Data.Show" -> "Data.Symbol"; +"Data.Show" -> "Data.Unit"; +"Data.Show" -> "Data.Void"; +"Data.Show" -> "Record.Unsafe"; +"Data.Show" -> "Type.Proxy"; +"Data.Show.Generic" -> "Prelude"; +"Data.Show.Generic" -> "Data.Generic.Rep"; +"Data.Show.Generic" -> "Data.Symbol"; +"Data.Show.Generic" -> "Type.Proxy"; +"Data.Symbol" -> "Type.Proxy"; +"Effect" -> "Prelude"; +"Effect" -> "Control.Apply"; +"Effect.Class" -> "Control.Category"; +"Effect.Class" -> "Control.Monad"; +"Effect.Class" -> "Effect"; +"Effect.Class.Console" -> "Data.Function"; +"Effect.Class.Console" -> "Data.Show"; +"Effect.Class.Console" -> "Data.Unit"; +"Effect.Class.Console" -> "Effect.Class"; +"Effect.Class.Console" -> "Effect.Console"; +"Effect.Console" -> "Effect"; +"Effect.Console" -> "Data.Show"; +"Effect.Console" -> "Data.Unit"; +"Effect.Uncurried" -> "Data.Monoid"; +"Effect.Uncurried" -> "Effect"; +"Effect.Unsafe" -> "Effect"; +"Main" -> "Prelude"; +"Main" -> "Effect"; +"Main" -> "Effect.Console"; +"Prelude" -> "Control.Applicative"; +"Prelude" -> "Control.Apply"; +"Prelude" -> "Control.Bind"; +"Prelude" -> "Control.Category"; +"Prelude" -> "Control.Monad"; +"Prelude" -> "Control.Semigroupoid"; +"Prelude" -> "Data.Boolean"; +"Prelude" -> "Data.BooleanAlgebra"; +"Prelude" -> "Data.Bounded"; +"Prelude" -> "Data.CommutativeRing"; +"Prelude" -> "Data.DivisionRing"; +"Prelude" -> "Data.Eq"; +"Prelude" -> "Data.EuclideanRing"; +"Prelude" -> "Data.Field"; +"Prelude" -> "Data.Function"; +"Prelude" -> "Data.Functor"; +"Prelude" -> "Data.HeytingAlgebra"; +"Prelude" -> "Data.Monoid"; +"Prelude" -> "Data.NaturalTransformation"; +"Prelude" -> "Data.Ord"; +"Prelude" -> "Data.Ordering"; +"Prelude" -> "Data.Ring"; +"Prelude" -> "Data.Semigroup"; +"Prelude" -> "Data.Semiring"; +"Prelude" -> "Data.Show"; +"Prelude" -> "Data.Unit"; +"Prelude" -> "Data.Void"; +} diff --git a/test-fixtures/graph-modules.json b/test-fixtures/graph-modules.json new file mode 100644 index 000000000..771326b17 --- /dev/null +++ b/test-fixtures/graph-modules.json @@ -0,0 +1,543 @@ +{ + "Control.Applicative": { + "depends": [ + "Control.Apply", + "Data.Functor", + "Data.Unit", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Control/Applicative.purs" + }, + "Control.Apply": { + "depends": [ + "Data.Functor", + "Data.Function", + "Control.Category", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Control/Apply.purs" + }, + "Control.Bind": { + "depends": [ + "Control.Applicative", + "Control.Apply", + "Control.Category", + "Data.Function", + "Data.Functor", + "Data.Unit", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Control/Bind.purs" + }, + "Control.Category": { + "depends": [ + "Control.Semigroupoid" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Control/Category.purs" + }, + "Control.Monad": { + "depends": [ + "Control.Applicative", + "Control.Apply", + "Control.Bind", + "Data.Functor", + "Data.Unit", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Control/Monad.purs" + }, + "Control.Semigroupoid": { + "depends": [], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Control/Semigroupoid.purs" + }, + "Data.Boolean": { + "depends": [], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Boolean.purs" + }, + "Data.BooleanAlgebra": { + "depends": [ + "Data.HeytingAlgebra", + "Data.Symbol", + "Data.Unit", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/BooleanAlgebra.purs" + }, + "Data.Bounded": { + "depends": [ + "Data.Ord", + "Data.Symbol", + "Data.Unit", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Bounded.purs" + }, + "Data.Bounded.Generic": { + "depends": [ + "Data.Generic.Rep", + "Data.Bounded" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Bounded/Generic.purs" + }, + "Data.CommutativeRing": { + "depends": [ + "Data.Ring", + "Data.Semiring", + "Data.Symbol", + "Data.Unit", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/CommutativeRing.purs" + }, + "Data.DivisionRing": { + "depends": [ + "Data.EuclideanRing", + "Data.Ring", + "Data.Semiring" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/DivisionRing.purs" + }, + "Data.Eq": { + "depends": [ + "Data.HeytingAlgebra", + "Data.Symbol", + "Data.Unit", + "Data.Void", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Eq.purs" + }, + "Data.Eq.Generic": { + "depends": [ + "Prelude", + "Data.Generic.Rep" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Eq/Generic.purs" + }, + "Data.EuclideanRing": { + "depends": [ + "Data.BooleanAlgebra", + "Data.CommutativeRing", + "Data.Eq", + "Data.Ring", + "Data.Semiring" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/EuclideanRing.purs" + }, + "Data.Field": { + "depends": [ + "Data.DivisionRing", + "Data.CommutativeRing", + "Data.EuclideanRing", + "Data.Ring", + "Data.Semiring" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Field.purs" + }, + "Data.Function": { + "depends": [ + "Control.Category", + "Data.Boolean", + "Data.Ord", + "Data.Ring" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Function.purs" + }, + "Data.Functor": { + "depends": [ + "Data.Function", + "Data.Unit", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Functor.purs" + }, + "Data.Generic.Rep": { + "depends": [ + "Data.Semigroup", + "Data.Show", + "Data.Symbol", + "Data.Void", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Generic/Rep.purs" + }, + "Data.HeytingAlgebra": { + "depends": [ + "Data.Symbol", + "Data.Unit", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/HeytingAlgebra.purs" + }, + "Data.HeytingAlgebra.Generic": { + "depends": [ + "Prelude", + "Data.Generic.Rep", + "Data.HeytingAlgebra" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/HeytingAlgebra/Generic.purs" + }, + "Data.Monoid": { + "depends": [ + "Data.Boolean", + "Data.Eq", + "Data.EuclideanRing", + "Data.Ord", + "Data.Ordering", + "Data.Semigroup", + "Data.Symbol", + "Data.Unit", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid.purs" + }, + "Data.Monoid.Additive": { + "depends": [ + "Prelude", + "Data.Eq", + "Data.Ord" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid/Additive.purs" + }, + "Data.Monoid.Conj": { + "depends": [ + "Prelude", + "Data.Eq", + "Data.HeytingAlgebra", + "Data.Ord" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid/Conj.purs" + }, + "Data.Monoid.Disj": { + "depends": [ + "Prelude", + "Data.Eq", + "Data.HeytingAlgebra", + "Data.Ord" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid/Disj.purs" + }, + "Data.Monoid.Dual": { + "depends": [ + "Prelude", + "Data.Eq", + "Data.Ord" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid/Dual.purs" + }, + "Data.Monoid.Endo": { + "depends": [ + "Prelude" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid/Endo.purs" + }, + "Data.Monoid.Generic": { + "depends": [ + "Data.Monoid", + "Data.Generic.Rep" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid/Generic.purs" + }, + "Data.Monoid.Multiplicative": { + "depends": [ + "Prelude", + "Data.Eq", + "Data.Ord" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Monoid/Multiplicative.purs" + }, + "Data.NaturalTransformation": { + "depends": [], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/NaturalTransformation.purs" + }, + "Data.Ord": { + "depends": [ + "Data.Eq", + "Data.Symbol", + "Data.Ordering", + "Data.Ring", + "Data.Unit", + "Data.Void", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Ord.purs" + }, + "Data.Ord.Generic": { + "depends": [ + "Prelude", + "Data.Generic.Rep" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Ord/Generic.purs" + }, + "Data.Ordering": { + "depends": [ + "Data.Eq", + "Data.Semigroup", + "Data.Show" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Ordering.purs" + }, + "Data.Reflectable": { + "depends": [ + "Data.Ord", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Reflectable.purs" + }, + "Data.Ring": { + "depends": [ + "Data.Semiring", + "Data.Symbol", + "Data.Unit", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Ring.purs" + }, + "Data.Ring.Generic": { + "depends": [ + "Prelude", + "Data.Generic.Rep" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Ring/Generic.purs" + }, + "Data.Semigroup": { + "depends": [ + "Data.Symbol", + "Data.Unit", + "Data.Void", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Semigroup.purs" + }, + "Data.Semigroup.First": { + "depends": [ + "Prelude", + "Data.Eq", + "Data.Ord" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Semigroup/First.purs" + }, + "Data.Semigroup.Generic": { + "depends": [ + "Prelude", + "Data.Generic.Rep" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Semigroup/Generic.purs" + }, + "Data.Semigroup.Last": { + "depends": [ + "Prelude", + "Data.Eq", + "Data.Ord" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Semigroup/Last.purs" + }, + "Data.Semiring": { + "depends": [ + "Data.Symbol", + "Data.Unit", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Semiring.purs" + }, + "Data.Semiring.Generic": { + "depends": [ + "Prelude", + "Data.Generic.Rep" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Semiring/Generic.purs" + }, + "Data.Show": { + "depends": [ + "Data.Semigroup", + "Data.Symbol", + "Data.Unit", + "Data.Void", + "Record.Unsafe", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Show.purs" + }, + "Data.Show.Generic": { + "depends": [ + "Prelude", + "Data.Generic.Rep", + "Data.Symbol", + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Show/Generic.purs" + }, + "Data.Symbol": { + "depends": [ + "Type.Proxy" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Symbol.purs" + }, + "Data.Unit": { + "depends": [], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Unit.purs" + }, + "Data.Void": { + "depends": [], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Data/Void.purs" + }, + "Effect": { + "depends": [ + "Prelude", + "Control.Apply" + ], + "package": "effect", + "path": ".spago/packages/effect-4.0.0/src/Effect.purs" + }, + "Effect.Class": { + "depends": [ + "Control.Category", + "Control.Monad", + "Effect" + ], + "package": "effect", + "path": ".spago/packages/effect-4.0.0/src/Effect/Class.purs" + }, + "Effect.Class.Console": { + "depends": [ + "Data.Function", + "Data.Show", + "Data.Unit", + "Effect.Class", + "Effect.Console" + ], + "package": "console", + "path": ".spago/packages/console-6.0.0/src/Effect/Class/Console.purs" + }, + "Effect.Console": { + "depends": [ + "Effect", + "Data.Show", + "Data.Unit" + ], + "package": "console", + "path": ".spago/packages/console-6.0.0/src/Effect/Console.purs" + }, + "Effect.Uncurried": { + "depends": [ + "Data.Monoid", + "Effect" + ], + "package": "effect", + "path": ".spago/packages/effect-4.0.0/src/Effect/Uncurried.purs" + }, + "Effect.Unsafe": { + "depends": [ + "Effect" + ], + "package": "effect", + "path": ".spago/packages/effect-4.0.0/src/Effect/Unsafe.purs" + }, + "Main": { + "depends": [ + "Prelude", + "Effect", + "Effect.Console" + ], + "package": "my-project", + "path": "src/Main.purs" + }, + "Prelude": { + "depends": [ + "Control.Applicative", + "Control.Apply", + "Control.Bind", + "Control.Category", + "Control.Monad", + "Control.Semigroupoid", + "Data.Boolean", + "Data.BooleanAlgebra", + "Data.Bounded", + "Data.CommutativeRing", + "Data.DivisionRing", + "Data.Eq", + "Data.EuclideanRing", + "Data.Field", + "Data.Function", + "Data.Functor", + "Data.HeytingAlgebra", + "Data.Monoid", + "Data.NaturalTransformation", + "Data.Ord", + "Data.Ordering", + "Data.Ring", + "Data.Semigroup", + "Data.Semiring", + "Data.Show", + "Data.Unit", + "Data.Void" + ], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Prelude.purs" + }, + "Record.Unsafe": { + "depends": [], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Record/Unsafe.purs" + }, + "Type.Proxy": { + "depends": [], + "package": "prelude", + "path": ".spago/packages/prelude-6.0.1/src/Type/Proxy.purs" + } +} diff --git a/test-fixtures/graph-packages-topo.txt b/test-fixtures/graph-packages-topo.txt new file mode 100644 index 000000000..add6dc185 --- /dev/null +++ b/test-fixtures/graph-packages-topo.txt @@ -0,0 +1,4 @@ +prelude +effect +console +my-project diff --git a/test-fixtures/graph-packages.dot b/test-fixtures/graph-packages.dot new file mode 100644 index 000000000..57088cece --- /dev/null +++ b/test-fixtures/graph-packages.dot @@ -0,0 +1,11 @@ +strict digraph deps { +node[shape=rect] +splines=ortho +"my-project" [style=dashed]; +"console" -> "effect"; +"console" -> "prelude"; +"effect" -> "prelude"; +"my-project" -> "console"; +"my-project" -> "effect"; +"my-project" -> "prelude"; +} diff --git a/test-fixtures/graph-packages.json b/test-fixtures/graph-packages.json new file mode 100644 index 000000000..f5d9fcde8 --- /dev/null +++ b/test-fixtures/graph-packages.json @@ -0,0 +1,23 @@ +{ + "console": { + "depends": [ + "effect", + "prelude" + ] + }, + "effect": { + "depends": [ + "prelude" + ] + }, + "my-project": { + "depends": [ + "console", + "effect", + "prelude" + ] + }, + "prelude": { + "depends": [] + } +} diff --git a/test-fixtures/graph.png b/test-fixtures/graph.png new file mode 100644 index 000000000..307066987 Binary files /dev/null and b/test-fixtures/graph.png differ diff --git a/test-fixtures/list-dependencies.json b/test-fixtures/list-dependencies.json index c06ed2cc6..be20bd440 100644 --- a/test-fixtures/list-dependencies.json +++ b/test-fixtures/list-dependencies.json @@ -4,15 +4,15 @@ "value": { "hasTests": true, "package": { + "name": "aaa2", "dependencies": [ "console", "effect", "prelude" ], - "name": "aaa2", "test": { - "dependencies": [], - "main": "Subpackage.Test.Main" + "main": "Subpackage.Test.Main", + "dependencies": [] } }, "path": "subpackage" diff --git a/test-fixtures/list-packages.json b/test-fixtures/list-packages.json index 536edfa3a..1f578df65 100644 --- a/test-fixtures/list-packages.json +++ b/test-fixtures/list-packages.json @@ -4,16 +4,16 @@ "value": { "hasTests": true, "package": { + "name": "aaa", "dependencies": [ "aaa2", "console", "effect", "prelude" ], - "name": "aaa", "test": { - "dependencies": [], - "main": "Test.Main" + "main": "Test.Main", + "dependencies": [] } }, "path": "./" @@ -24,15 +24,15 @@ "value": { "hasTests": true, "package": { + "name": "aaa2", "dependencies": [ "console", "effect", "prelude" ], - "name": "aaa2", "test": { - "dependencies": [], - "main": "Subpackage.Test.Main" + "main": "Subpackage.Test.Main", + "dependencies": [] } }, "path": "subpackage" diff --git a/test-fixtures/local-package-set-config.yaml b/test-fixtures/local-package-set-config.yaml index fa14dd29a..0dde24b4a 100644 --- a/test-fixtures/local-package-set-config.yaml +++ b/test-fixtures/local-package-set-config.yaml @@ -1,13 +1,13 @@ package: + name: aaaa dependencies: - console - effect - prelude - name: aaaa test: - dependencies: [] main: Test.Main + dependencies: [] workspace: - extra_packages: {} package_set: path: local-package-set.json + extra_packages: {} diff --git a/test-fixtures/local-package-set-config2.yaml b/test-fixtures/local-package-set-config2.yaml index cd5a5b2a3..f7649e376 100644 --- a/test-fixtures/local-package-set-config2.yaml +++ b/test-fixtures/local-package-set-config2.yaml @@ -1,13 +1,13 @@ package: + name: aaaa dependencies: - console - effect - prelude - name: aaaa test: - dependencies: [] main: Test.Main + dependencies: [] workspace: - extra_packages: {} package_set: path: ../local-package-set.json + extra_packages: {} diff --git a/test-fixtures/no-test-section.yaml b/test-fixtures/no-test-section.yaml index 37d23d92f..4d519b943 100644 --- a/test-fixtures/no-test-section.yaml +++ b/test-fixtures/no-test-section.yaml @@ -1,10 +1,10 @@ package: + name: aaa dependencies: - console - effect - prelude - name: aaa workspace: - extra_packages: {} package_set: registry: 29.3.0 + extra_packages: {} diff --git a/test-fixtures/older-package-set-tag.yaml b/test-fixtures/older-package-set-tag.yaml index 9f7dbab2d..7cc3fa03e 100644 --- a/test-fixtures/older-package-set-tag.yaml +++ b/test-fixtures/older-package-set-tag.yaml @@ -1,13 +1,13 @@ package: + name: 7368613235362d47665357393342584955783641314b70674c dependencies: - console - effect - prelude - name: 7368613235362d47665357393342584955783641314b70674c test: - dependencies: [] main: Test.Main + dependencies: [] workspace: - extra_packages: {} package_set: registry: 9.0.0 + extra_packages: {} diff --git a/test-fixtures/pedantic/check-direct-import-transitive-dependency-both.txt b/test-fixtures/pedantic/check-direct-import-transitive-dependency-both.txt new file mode 100644 index 000000000..23001f4f7 --- /dev/null +++ b/test-fixtures/pedantic/check-direct-import-transitive-dependency-both.txt @@ -0,0 +1,33 @@ +Reading Spago workspace configuration... +Read the package set from the registry + +✅ Selecting package to build: pedantic + +Downloading dependencies... +Building... + Src Lib All +Warnings 0 0 0 +Errors 0 0 0 + +✅ Build succeeded. + +Looking for unused and undeclared transitive dependencies... + +❌ Sources for package 'pedantic' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: + maybe + from `Main`, which imports: + Data.Maybe + + +Run the following command to install them all: + spago install -p pedantic maybe + + +❌ Tests for package 'pedantic' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: + control + from `Test.Main`, which imports: + Control.Alt + + +Run the following command to install them all: + spago install --test-deps -p pedantic control diff --git a/test-fixtures/pedantic/check-direct-import-transitive-dependency-test.txt b/test-fixtures/pedantic/check-direct-import-transitive-dependency-test.txt new file mode 100644 index 000000000..1c3c96cfd --- /dev/null +++ b/test-fixtures/pedantic/check-direct-import-transitive-dependency-test.txt @@ -0,0 +1,23 @@ +Reading Spago workspace configuration... +Read the package set from the registry + +✅ Selecting package to build: pedantic + +Downloading dependencies... +Building... + Src Lib All +Warnings 0 0 0 +Errors 0 0 0 + +✅ Build succeeded. + +Looking for unused and undeclared transitive dependencies... + +❌ Tests for package 'pedantic' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: + control + from `Test.Main`, which imports: + Control.Alt + + +Run the following command to install them all: + spago install --test-deps -p pedantic control diff --git a/test-fixtures/pedantic/check-direct-import-transitive-dependency.txt b/test-fixtures/pedantic/check-direct-import-transitive-dependency.txt new file mode 100644 index 000000000..8131ae6c4 --- /dev/null +++ b/test-fixtures/pedantic/check-direct-import-transitive-dependency.txt @@ -0,0 +1,23 @@ +Reading Spago workspace configuration... +Read the package set from the registry + +✅ Selecting package to build: pedantic + +Downloading dependencies... +Building... + Src Lib All +Warnings 0 0 0 +Errors 0 0 0 + +✅ Build succeeded. + +Looking for unused and undeclared transitive dependencies... + +❌ Sources for package 'pedantic' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: + control + from `Main`, which imports: + Control.Alt + + +Run the following command to install them all: + spago install -p pedantic control diff --git a/test-fixtures/pedantic/check-pedantic-packages.txt b/test-fixtures/pedantic/check-pedantic-packages.txt new file mode 100644 index 000000000..7af052601 --- /dev/null +++ b/test-fixtures/pedantic/check-pedantic-packages.txt @@ -0,0 +1,43 @@ +Reading Spago workspace configuration... +Read the package set from the registry + +✅ Selecting package to build: pedantic + +Downloading dependencies... +Building... + Src Lib All +Warnings 0 0 0 +Errors 0 0 0 + +✅ Build succeeded. + +Looking for unused and undeclared transitive dependencies... + +❌ Sources for package 'pedantic' declares unused dependencies - please remove them from the project config: + - console + - effect + - either + + +❌ Sources for package 'pedantic' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: + newtype + from `Main`, which imports: + Data.Newtype + + +Run the following command to install them all: + spago install -p pedantic newtype + + +❌ Tests for package 'pedantic' declares unused dependencies - please remove them from the project config: + - tuples + + +❌ Tests for package 'pedantic' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: + either + from `Test.Main`, which imports: + Data.Either + + +Run the following command to install them all: + spago install --test-deps -p pedantic either diff --git a/test-fixtures/pedantic/check-unused-dependency-in-source.txt b/test-fixtures/pedantic/check-unused-dependency-in-source.txt new file mode 100644 index 000000000..99365f26d --- /dev/null +++ b/test-fixtures/pedantic/check-unused-dependency-in-source.txt @@ -0,0 +1,31 @@ +Reading Spago workspace configuration... +Read the package set from the registry + +✅ Selecting package to build: pedantic + +Downloading dependencies... +Building... + Src Lib All +Warnings 0 0 0 +Errors 0 0 0 + +✅ Build succeeded. + +Looking for unused and undeclared transitive dependencies... + +❌ Sources for package 'pedantic' declares unused dependencies - please remove them from the project config: + - console + - effect + + +❌ Tests for package 'pedantic' import the following transitive dependencies - please add them to the project dependencies, or remove the imports: + console + from `Test.Main`, which imports: + Effect.Class.Console + effect + from `Test.Main`, which imports: + Effect + + +Run the following command to install them all: + spago install --test-deps -p pedantic console effect diff --git a/test-fixtures/check-unused-dependency.txt b/test-fixtures/pedantic/check-unused-dependency.txt similarity index 56% rename from test-fixtures/check-unused-dependency.txt rename to test-fixtures/pedantic/check-unused-dependency.txt index 042aa88ce..83f7d1b1c 100644 --- a/test-fixtures/check-unused-dependency.txt +++ b/test-fixtures/pedantic/check-unused-dependency.txt @@ -1,7 +1,7 @@ Reading Spago workspace configuration... Read the package set from the registry -✅ Selecting package to build: 7368613235362d2f444a2b4f56375435646a59726b53586548 +✅ Selecting package to build: pedantic Downloading dependencies... Building... @@ -13,6 +13,6 @@ Errors 0 0 0 Looking for unused and undeclared transitive dependencies... -❌ Sources for package '7368613235362d2f444a2b4f56375435646a59726b53586548' declares unused dependencies - please remove them from the project config: +❌ Sources for package 'pedantic' declares unused dependencies - please remove them from the project config: - console - effect diff --git a/test-fixtures/pedantic/check-unused-source-and-test-dependency.txt b/test-fixtures/pedantic/check-unused-source-and-test-dependency.txt new file mode 100644 index 000000000..c762edab8 --- /dev/null +++ b/test-fixtures/pedantic/check-unused-source-and-test-dependency.txt @@ -0,0 +1,23 @@ +Reading Spago workspace configuration... +Read the package set from the registry + +✅ Selecting package to build: pedantic + +Downloading dependencies... +Building... + Src Lib All +Warnings 0 0 0 +Errors 0 0 0 + +✅ Build succeeded. + +Looking for unused and undeclared transitive dependencies... + +❌ Sources for package 'pedantic' declares unused dependencies - please remove them from the project config: + - console + - effect + + +❌ Tests for package 'pedantic' declares unused dependencies - please remove them from the project config: + - console + - effect diff --git a/test-fixtures/check-unused-test-dependency.txt b/test-fixtures/pedantic/check-unused-test-dependency.txt similarity index 56% rename from test-fixtures/check-unused-test-dependency.txt rename to test-fixtures/pedantic/check-unused-test-dependency.txt index 706b06844..136eefc3b 100644 --- a/test-fixtures/check-unused-test-dependency.txt +++ b/test-fixtures/pedantic/check-unused-test-dependency.txt @@ -1,7 +1,7 @@ Reading Spago workspace configuration... Read the package set from the registry -✅ Selecting package to build: 7368613235362d2f444a2b4f56375435646a59726b53586548 +✅ Selecting package to build: pedantic Downloading dependencies... Building... @@ -13,5 +13,5 @@ Errors 0 0 0 Looking for unused and undeclared transitive dependencies... -❌ Tests for package '7368613235362d2f444a2b4f56375435646a59726b53586548' declares unused dependencies - please remove them from the project config: +❌ Tests for package 'pedantic' declares unused dependencies - please remove them from the project config: - newtype diff --git a/test-fixtures/spago-install-failure.yaml b/test-fixtures/spago-install-failure.yaml index e240a3b22..ee4cdbf39 100644 --- a/test-fixtures/spago-install-failure.yaml +++ b/test-fixtures/spago-install-failure.yaml @@ -1,13 +1,13 @@ package: + name: aaaa dependencies: - console - effect - prelude - name: aaaa test: - dependencies: [] main: Test.Main + dependencies: [] workspace: - extra_packages: {} package_set: registry: 29.3.0 + extra_packages: {} diff --git a/test-fixtures/spago-install-success.yaml b/test-fixtures/spago-install-success.yaml index 6f8cec6b8..f3130cbd6 100644 --- a/test-fixtures/spago-install-success.yaml +++ b/test-fixtures/spago-install-success.yaml @@ -1,14 +1,14 @@ package: + name: aaa dependencies: - console - effect - foreign - prelude - name: aaa test: - dependencies: [] main: Test.Main + dependencies: [] workspace: - extra_packages: {} package_set: registry: 29.3.0 + extra_packages: {} diff --git a/test-fixtures/spago-install-test-deps-success.yaml b/test-fixtures/spago-install-test-deps-success.yaml index 8b006dd08..653b26de6 100644 --- a/test-fixtures/spago-install-test-deps-success.yaml +++ b/test-fixtures/spago-install-test-deps-success.yaml @@ -1,14 +1,14 @@ package: + name: aaa dependencies: - console - effect - prelude - name: aaa test: + main: Test.Main dependencies: - foreign - main: Test.Main workspace: - extra_packages: {} package_set: registry: 29.3.0 + extra_packages: {} diff --git a/test-fixtures/spago-publish.yaml b/test-fixtures/spago-publish.yaml index 0d9c1d0fd..bd1e89f90 100644 --- a/test-fixtures/spago-publish.yaml +++ b/test-fixtures/spago-publish.yaml @@ -1,12 +1,12 @@ package: + name: aaa dependencies: - console: ">=6.0.0 <7.0.0" - effect: ">=4.0.0 <5.0.0" - prelude: ">=6.0.1 <7.0.0" - name: aaa test: - dependencies: [] main: Test.Main + dependencies: [] publish: version: 0.0.1 license: MIT @@ -14,7 +14,7 @@ package: githubOwner: purescript githubRepo: aaa workspace: - extra_packages: - console: "6.1.0" package_set: registry: 28.1.1 + extra_packages: + console: "6.1.0" diff --git a/test-fixtures/spago-subpackage-install-success.yaml b/test-fixtures/spago-subpackage-install-success.yaml index b5097c803..c1b602bcd 100644 --- a/test-fixtures/spago-subpackage-install-success.yaml +++ b/test-fixtures/spago-subpackage-install-success.yaml @@ -1,10 +1,10 @@ package: + name: subpackage dependencies: - console - effect - either - prelude - name: subpackage test: - dependencies: [] main: Subpackage.Test.Main + dependencies: [] diff --git a/test-fixtures/spago-with-hash.yaml b/test-fixtures/spago-with-hash.yaml index 31be11fff..99cc7d074 100644 --- a/test-fixtures/spago-with-hash.yaml +++ b/test-fixtures/spago-with-hash.yaml @@ -1,14 +1,14 @@ package: + name: aaa dependencies: - console - effect - prelude - name: aaa test: - dependencies: [] main: Test.Main + dependencies: [] workspace: - extra_packages: {} package_set: url: https://raw.githubusercontent.com/purescript/registry/main/package-sets/29.3.0.json hash: sha256-LJoXQjRcY0IkP1YJMwvhKAtb4NpxoJW5A6DxapTI+as= + extra_packages: {} diff --git a/test/Prelude.purs b/test/Prelude.purs index 8dac87f14..b67ad22c0 100644 --- a/test/Prelude.purs +++ b/test/Prelude.purs @@ -5,6 +5,7 @@ module Test.Prelude import Spago.Prelude +import Data.Array as Array import Data.Map as Map import Data.String (Pattern(..), Replacement(..)) import Data.String as String @@ -17,7 +18,9 @@ import Registry.Version as Version import Spago.Cmd (ExecError, ExecResult) as X import Spago.Cmd (ExecError, ExecResult, StdinConfig(..)) import Spago.Cmd as Cmd +import Spago.Command.Init as Init import Spago.Core.Config (Dependencies(..), Config) +import Spago.Core.Config as Config import Spago.FS as FS import Spago.Prelude as X import Test.Spec.Assertions (fail) @@ -39,7 +42,9 @@ withTempDir = Aff.bracket createTempDir cleanupTempDir temp <- mkTemp' $ Just "spago-test-" FS.mkdirp temp liftEffect $ Process.chdir temp - log $ "Running test in " <> temp + isDebug <- liftEffect $ map isJust $ Process.lookupEnv "SPAGO_TEST_DEBUG" + when isDebug do + log $ "Running test in " <> temp let fixturesPath = oldCwd <> Path.sep <> "test-fixtures" @@ -75,13 +80,23 @@ shouldEqual -> m Unit shouldEqual v1 v2 = when (v1 /= v2) do - fail $ show v1 <> "\n\n≠\n\n" <> show v2 + fail $ show v1 <> "\n\n ≠\n\n " <> show v2 + +shouldEqualStr + :: forall m + . MonadThrow Error m + => String + -> String + -> m Unit +shouldEqualStr v1 v2 = + when (v1 /= v2) do + fail $ "\n=====\n" <> v1 <> "\n=====\n ≠\n=====\n " <> show v2 <> "\n=====\n" checkFixture :: String -> String -> Aff Unit checkFixture filepath fixturePath = do filecontent <- FS.readTextFile filepath fixturecontent <- FS.readTextFile fixturePath - filecontent `shouldEqual` fixturecontent + filecontent `shouldEqualStr` fixturecontent plusDependencies :: Array String -> Config -> Config plusDependencies deps config = config @@ -181,3 +196,141 @@ mkPackageName = unsafeFromRight <<< PackageName.parse mkVersion :: String -> Version mkVersion = unsafeFromRight <<< Version.parse + +writeMain :: Array String -> String +writeMain rest = writePursFile { moduleName: "Main", rest } + +writeTestMain :: Array String -> String +writeTestMain rest = writePursFile { moduleName: "Test.Main", rest } + +writePursFile :: { moduleName :: String, rest :: Array String } -> String +writePursFile { moduleName, rest } = + Array.intercalate "\n" $ Array.cons modNameLine $ rest + where + modNameLine = "module " <> moduleName <> " where" + +mkDependencies :: Array String -> Config.Dependencies +mkDependencies = Config.Dependencies <<< Map.fromFoldable <<< map (flip Tuple Nothing <<< mkPackageName) + +-- | packageToModuleName "package-name" = "PACKAGE.NAME" +packageToModuleName ∷ String → String +packageToModuleName packageName = + String.toUpper (String.replaceAll (Pattern "-") (Replacement ".") packageName) + +mkSrcModuleName ∷ String → String +mkSrcModuleName packageName = "Src." <> packageToModuleName packageName + +mkTestModuleName ∷ String → String +mkTestModuleName packageName = "Test." <> packageToModuleName packageName + +-- See `config*` functions for transforms below this binding +mkPackageOnlyConfig + :: { packageName :: String, srcDependencies :: Array String } + -> Array (Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions) + -> Config.Config +mkPackageOnlyConfig initialOptions transforms = do + Init.defaultConfig' $ Init.PackageOnly $ configurePackageSection initialOptions transforms + +mkPackageAndWorkspaceConfig + :: { package :: { packageName :: String, srcDependencies :: Array String } + , workspace :: { setVersion :: Maybe Version } + } + -> Array (Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions) + -> Config.Config +mkPackageAndWorkspaceConfig initOptions transforms = do + Init.defaultConfig' $ Init.PackageAndWorkspace (configurePackageSection initOptions.package transforms) initOptions.workspace + +configurePackageSection + :: { packageName :: String, srcDependencies :: Array String } + -> Array (Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions) + -> Init.DefaultConfigPackageOptions +configurePackageSection initialOptions = snd <<< Array.foldl (\c f -> f c) + ( Tuple initialOptions.packageName $ + { name: mkPackageName initialOptions.packageName + , dependencies: initialOptions.srcDependencies + , build: Nothing + , test: Nothing + } + ) + +configAddSrcStrict :: Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddSrcStrict = map \r -> r + { build = Just + { strict: Just true + , censorProjectWarnings: r.build >>= _.censorProjectWarnings + , pedanticPackages: r.build >>= _.pedanticPackages + } + } + +configAddSrcPedantic :: Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddSrcPedantic = map \r -> r + { build = Just + { strict: r.build >>= _.strict + , censorProjectWarnings: r.build >>= _.censorProjectWarnings + , pedanticPackages: Just true + } + } + +configAddSrcCensor :: Config.CensorBuildWarnings -> Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddSrcCensor censors = map \r -> r + { build = Just + { strict: r.build >>= _.strict + , censorProjectWarnings: Just censors + , pedanticPackages: r.build >>= _.pedanticPackages + } + } + +configAddTestMain :: Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddTestMain (Tuple packageName r) = Tuple packageName $ r + { test = Just + { moduleMain: mkTestModuleName packageName + , strict: r.test >>= _.strict + , censorTestWarnings: r.test >>= _.censorTestWarnings + , pedanticPackages: r.test >>= _.pedanticPackages + , dependencies: r.test >>= _.dependencies + } + } + +configAddTestStrict :: Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddTestStrict (Tuple packageName r) = Tuple packageName $ r + { test = Just + { moduleMain: mkTestModuleName packageName + , strict: Just true + , censorTestWarnings: r.test >>= _.censorTestWarnings + , pedanticPackages: r.test >>= _.pedanticPackages + , dependencies: r.test >>= _.dependencies + } + } + +configAddTestPedantic :: Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddTestPedantic (Tuple packageName r) = Tuple packageName $ r + { test = Just + { moduleMain: mkTestModuleName packageName + , strict: r.test >>= _.strict + , censorTestWarnings: r.test >>= _.censorTestWarnings + , pedanticPackages: Just true + , dependencies: r.test >>= _.dependencies + } + } + +configAddTestCensor :: Config.CensorBuildWarnings -> Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddTestCensor censors (Tuple packageName r) = Tuple packageName $ r + { test = Just + { moduleMain: mkTestModuleName packageName + , strict: r.test >>= _.strict + , censorTestWarnings: Just censors + , pedanticPackages: r.test >>= _.pedanticPackages + , dependencies: r.test >>= _.dependencies + } + } + +configAddTestDependencies :: Array String -> Tuple String Init.DefaultConfigPackageOptions -> Tuple String Init.DefaultConfigPackageOptions +configAddTestDependencies deps (Tuple packageName r) = Tuple packageName $ r + { test = Just + { moduleMain: mkTestModuleName packageName + , strict: r.test >>= _.strict + , censorTestWarnings: r.test >>= _.censorTestWarnings + , pedanticPackages: r.test >>= _.pedanticPackages + , dependencies: Just $ maybe (mkDependencies deps) (append (mkDependencies deps)) $ r.test >>= _.dependencies + } + } diff --git a/test/Spago.purs b/test/Spago.purs index e5f9ebb5d..1daec3b68 100644 --- a/test/Spago.purs +++ b/test/Spago.purs @@ -11,6 +11,7 @@ import Effect.Aff as Aff import Test.Spago.Build as Build import Test.Spago.Bundle as Bundle import Test.Spago.Docs as Docs +import Test.Spago.Graph as Graph import Test.Spago.Init as Init import Test.Spago.Install as Install import Test.Spago.Lock as Lock @@ -49,6 +50,7 @@ main = Aff.launchAff_ $ void $ un Identity $ Spec.Runner.runSpecT testConfig [ S Docs.spec Upgrade.spec Publish.spec + Graph.spec Spec.describe "miscellaneous" do Lock.spec Unit.spec diff --git a/test/Spago/Build.purs b/test/Spago/Build.purs index d37c7b0de..2bc2b105c 100644 --- a/test/Spago/Build.purs +++ b/test/Spago/Build.purs @@ -2,14 +2,13 @@ module Test.Spago.Build where import Test.Prelude -import Data.Map as Map import Node.FS.Aff as FSA import Node.Path as Path import Node.Process as Process import Spago.Command.Init as Init -import Spago.Core.Config (configCodec) import Spago.Core.Config as Config import Spago.FS as FS +import Test.Spago.Build.Pedantic as Pedantic import Test.Spago.Build.Polyrepo as BuildPolyrepo import Test.Spec (Spec) import Test.Spec as Spec @@ -79,87 +78,6 @@ spec = Spec.around withTempDir do FS.exists "output" `Assert.shouldReturn` true FS.exists (Path.concat [ "subpackage", "output" ]) `Assert.shouldReturn` false - Spec.describe "pedantic packages" do - - let - modifyPackageConfig f = do - content <- FS.readYamlDocFile configCodec "spago.yaml" - case content of - Left err -> - Assert.fail $ "Failed to decode spago.yaml file\n" <> err - Right { yaml: config } -> - FS.writeYamlFile configCodec "spago.yaml" $ f config - - addPedanticPackagesToSrc = modifyPackageConfig \config -> - config - { package = config.package <#> \r -> r - { build = Just - { pedantic_packages: Just true - , strict: Nothing - , censor_project_warnings: Nothing - } - } - } - - Spec.describe "fails when there are imports from transitive dependencies and" do - let - setupSrcTransitiveTests spago = do - spago [ "init", "--name", "7368613235362d34312f4e59746b7869335477336d33414d72" ] >>= shouldBeSuccess - spago [ "install", "maybe" ] >>= shouldBeSuccess - FS.writeTextFile (Path.concat [ "src", "Main.purs" ]) "module Main where\nimport Prelude\nimport Data.Maybe\nimport Control.Alt\nmain = unit" - -- get rid of "Compiling ..." messages and other compiler warnings - spago [ "build" ] >>= shouldBeSuccess - - Spec.it "passed --pedantic-packages CLI flag" \{ spago, fixture } -> do - setupSrcTransitiveTests spago - spago [ "build", "--pedantic-packages" ] >>= shouldBeFailureErr (fixture "check-direct-import-transitive-dependency.txt") - - Spec.it "package config has 'pedantic_packages: true'" \{ spago, fixture } -> do - setupSrcTransitiveTests spago - addPedanticPackagesToSrc - spago [ "build" ] >>= shouldBeFailureErr (fixture "check-direct-import-transitive-dependency.txt") - - Spec.describe "warns about unused dependencies when" do - let - setupSrcUnusedDeps spago = do - spago [ "init", "--name", "7368613235362d2f444a2b4f56375435646a59726b53586548" ] >>= shouldBeSuccess - FS.writeTextFile (Path.concat [ "src", "Main.purs" ]) "module Main where\nimport Prelude\nmain = unit" - -- get rid of "Compiling ..." messages and other compiler warnings - spago [ "build" ] >>= shouldBeSuccess - - Spec.it "passing --pedantic-packages CLI flag" \{ spago, fixture } -> do - setupSrcUnusedDeps spago - spago [ "build", "--pedantic-packages" ] >>= shouldBeFailureErr (fixture "check-unused-dependency.txt") - - Spec.it "package config has 'pedantic_packages: true'" \{ spago, fixture } -> do - setupSrcUnusedDeps spago - addPedanticPackagesToSrc - spago [ "build" ] >>= shouldBeFailureErr (fixture "check-unused-dependency.txt") - - Spec.it "package's test config has 'pedantic_packages: true' and test code has unused dependencies" \{ spago, fixture } -> do - spago [ "init", "--name", "7368613235362d2f444a2b4f56375435646a59726b53586548" ] >>= shouldBeSuccess - FS.writeTextFile (Path.concat [ "test", "Test", "Main.purs" ]) "module Test.Main where\nimport Prelude\nmain = unit" - let - modifyPkgTestConfig pedantic = modifyPackageConfig \config -> - config - { package = config.package <#> \r -> r - { test = Just - { main: "Test.Main" - , pedantic_packages: Just pedantic - , strict: Nothing - , censor_test_warnings: Nothing - , dependencies: Config.Dependencies $ Map.fromFoldable $ map (flip Tuple Nothing <<< mkPackageName) [ "newtype" ] - , execArgs: Nothing - } - } - } - modifyPkgTestConfig false - -- get rid of "Compiling ..." messages and other compiler warnings - spago [ "build" ] >>= shouldBeSuccess - -- now add pedantic - modifyPkgTestConfig true - spago [ "build" ] >>= shouldBeFailureErr (fixture "check-unused-test-dependency.txt") - Spec.it "--strict causes build to fail if there are warnings" \{ spago, fixture } -> do spago [ "init" ] >>= shouldBeSuccess let srcMain = Path.concat [ "src", "Main.purs" ] @@ -218,6 +136,8 @@ spec = Spec.around withTempDir do spagoYaml <- FS.readTextFile "spago.yaml" spagoYaml `shouldContain` "- prelude: \">=6.0.1 <7.0.0\"" + Pedantic.spec + BuildPolyrepo.spec -- Spec.it "runs a --before command" \{ spago } -> do diff --git a/test/Spago/Build/Pedantic.purs b/test/Spago/Build/Pedantic.purs new file mode 100644 index 000000000..a832f84a1 --- /dev/null +++ b/test/Spago/Build/Pedantic.purs @@ -0,0 +1,253 @@ +module Test.Spago.Build.Pedantic (spec) where + +import Test.Prelude + +import Data.Array as Array +import Data.Map as Map +import Node.Path as Path +import Spago.Core.Config (Dependencies(..), Config) +import Spago.Core.Config as Core +import Spago.FS as FS +import Test.Spec (SpecT) +import Test.Spec as Spec +import Test.Spec.Assertions as Assert + +spec :: SpecT Aff TestDirs Identity Unit +spec = + Spec.describe "pedantic packages" do + + Spec.describe "fails when imports from transitive dependencies" do + + -- Here we are importing the `Control.Alt` module, which is in the `control` + -- package, which comes through `maybe` but we are not importing directly + Spec.it "appear in the source package" \{ spago, fixture } -> do + setup spago + ( defaultSetupConfig + { installSourcePackages = [ "maybe" ] + , main = Just + [ "import Prelude" + , "import Data.Maybe as Maybe" + , "import Effect as Effect" + , "import Effect.Console as Console" + , "import Control.Alt as Alt" + , "main = unit" + ] + } + ) + spago [ "build", "--pedantic-packages" ] + >>= shouldBeFailureErr (fixture "pedantic/check-direct-import-transitive-dependency.txt") + editSpagoYaml addPedanticFlagToSrc + spago [ "build" ] + >>= shouldBeFailureErr (fixture "pedantic/check-direct-import-transitive-dependency.txt") + + -- We are importing `Control.Alt` in the test package, which is in the `control` + -- package, which comes through `maybe` but we are not importing directly, so we + -- should get a transitivity warning about that + Spec.it "appear in the test package" \{ spago, fixture } -> do + setup spago + ( defaultSetupConfig + { installTestPackages = [ "maybe" ] + , main = Just + [ "import Prelude" + , "import Data.Maybe as Maybe" + , "import Effect as Effect" + , "import Effect.Console as Console" + , "main = unit" + ] + , testMain = Just + [ "import Prelude" + , "import Data.Maybe as Maybe" + , "import Effect as Effect" + , "import Effect.Console as Console" + , "import Control.Alt as Alt" + , "main = unit" + ] + } + ) + -- Here we install `maybe` only in test, so there should be a transitive + -- warning to install `maybe` in source, since we use it there and the flag + -- will turn it on for both source and test + spago [ "build", "--pedantic-packages" ] + >>= shouldBeFailureErr (fixture "pedantic/check-direct-import-transitive-dependency-both.txt") + editSpagoYaml addPedanticFlagToTest + -- Otherwise we can just turn it on for the test, and it will complain only about `control` + spago [ "build" ] + >>= shouldBeFailureErr (fixture "pedantic/check-direct-import-transitive-dependency-test.txt") + + Spec.describe "fails with warnings about unused dependencies" do + + -- Here we install `effect` and `console` in the test package, and we don't use them + -- in the source, so we should get an "unused" warning about them + Spec.it "in a source package" \{ spago, fixture } -> do + setup spago + ( defaultSetupConfig + { installTestPackages = [ "effect", "console" ] + , main = Just + [ "import Prelude" + , "main = unit" + ] + } + ) + spago [ "build", "--pedantic-packages" ] >>= shouldBeFailureErr (fixture "pedantic/check-unused-dependency.txt") + editSpagoYaml addPedanticFlagToSrc + spago [ "build" ] >>= shouldBeFailureErr (fixture "pedantic/check-unused-dependency.txt") + + -- Here we do not install `effect` and `console` in the test package, and we don't use them + -- in the source, so we should get an "unused" warning about them for the source, and a prompt + -- to install them in test + Spec.it "in a source package, but they are used in test" \{ spago, fixture } -> do + setup spago + ( defaultSetupConfig + { main = Just + [ "import Prelude" + , "main = unit" + ] + } + ) + spago [ "build", "--pedantic-packages" ] >>= shouldBeFailureErr (fixture "pedantic/check-unused-dependency-in-source.txt") + editSpagoYaml addPedanticFlagToSrc + spago [ "build" ] >>= shouldBeFailureErr (fixture "pedantic/check-unused-dependency.txt") + + -- Complain about the unused `newtype` dependency in the test package + Spec.it "in a test package" \{ spago, fixture } -> do + setup spago + ( defaultSetupConfig + { installTestPackages = [ "newtype" ] + , testMain = Just + [ "import Prelude" + , "main = unit" + ] + } + ) + -- first just add the flag + spago [ "build", "--pedantic-packages" ] + >>= shouldBeFailureErr (fixture "pedantic/check-unused-test-dependency.txt") + -- then prove that it also works when using it from the config + editSpagoYaml addPedanticFlagToTest + spago [ "build" ] + >>= shouldBeFailureErr (fixture "pedantic/check-unused-test-dependency.txt") + + -- `console` and `effect` are going to be unused for both source and test packages + Spec.it "in both the source and test packages" \{ spago, fixture } -> do + setup spago + ( defaultSetupConfig + { installSourcePackages = [ "prelude", "effect", "console" ] + , installTestPackages = [ "prelude", "effect", "console" ] + , main = Just + [ "import Prelude" + , "main = unit" + ] + , testMain = Just + [ "import Prelude" + , "main = unit" + ] + } + ) + spago [ "build", "--pedantic-packages" ] + >>= shouldBeFailureErr (fixture "pedantic/check-unused-source-and-test-dependency.txt") + editSpagoYaml (addPedanticFlagToSrc >>> addPedanticFlagToTest) + spago [ "build" ] + >>= shouldBeFailureErr (fixture "pedantic/check-unused-source-and-test-dependency.txt") + + -- The source package adds `control` and `either`, but: + -- * `either` is unused + -- * `control` is used, and that's fine + -- * `newtype` is used but not declare, so we get a "transitive" warning + -- The test package adds `tuples`, but: + -- * `tuples` is unused + -- * `newtype` is transitively imported, but we already warned about that in the source + -- so consider that fixed + -- * `either` is transitively imported, and it's going to be removed from the source + -- dependencies, so we get a "transitive" warning to install it in test + Spec.it "fails to build and reports deduplicated src and test unused/transitive dependencies" \{ spago, fixture } -> do + setup spago + ( defaultSetupConfig + { installSourcePackages = [ "prelude", "control", "either" ] + , installTestPackages = [ "tuples" ] + , main = Just + [ "import Prelude" + , "import Data.Newtype as Newtype" + , "import Control.Alt as Alt" + , "" + , "foo :: Int" + , "foo = 1" + ] + , testMain = Just + [ "import Prelude" + , "import Data.Newtype (class Newtype)" + , "import Data.Either (Either(..))" + , "" + , "newtype Bar = Bar Int" + , "derive instance Newtype Bar _" + , "" + , "foo :: Either Bar Int" + , "foo = Right 1" + ] + } + ) + spago [ "build", "--pedantic-packages" ] >>= shouldBeFailureErr (fixture "pedantic/check-pedantic-packages.txt") + editSpagoYaml (addPedanticFlagToSrc >>> addPedanticFlagToTest) + spago [ "build" ] >>= shouldBeFailureErr (fixture "pedantic/check-pedantic-packages.txt") + +editSpagoYaml :: (Config -> Config) -> Aff Unit +editSpagoYaml f = do + content <- FS.readYamlDocFile Core.configCodec "spago.yaml" + case content of + Left err -> + Assert.fail $ "Failed to decode spago.yaml file\n" <> err + Right { yaml: config } -> + FS.writeYamlFile Core.configCodec "spago.yaml" $ f config + +addPedanticFlagToSrc :: Config -> Config +addPedanticFlagToSrc config = config + { package = config.package <#> \r -> r + { build = Just + { pedantic_packages: Just true + , strict: Nothing + , censor_project_warnings: Nothing + } + } + } + +addPedanticFlagToTest :: Config -> Config +addPedanticFlagToTest config = config + { package = config.package <#> \r -> r + { test = Just + { main: "Test.Main" + , pedantic_packages: Just true + , strict: Nothing + , censor_test_warnings: Nothing + , dependencies: maybe (Dependencies Map.empty) _.dependencies r.test + , exec_args: r.test >>= _.exec_args + } + } + } + +type SetupConfig = + { installSourcePackages :: Array String + , installTestPackages :: Array String + , main :: Maybe (Array String) + , testMain :: Maybe (Array String) + } + +defaultSetupConfig :: SetupConfig +defaultSetupConfig = + { installSourcePackages: [] + , installTestPackages: [] + , main: Nothing + , testMain: Nothing + } + +setup :: (Array String -> Aff (Either ExecError ExecResult)) -> SetupConfig -> Aff Unit +setup spago config = do + spago [ "init", "--name", "pedantic" ] >>= shouldBeSuccess + unless (Array.null config.installSourcePackages) do + spago ([ "install" ] <> config.installSourcePackages) >>= shouldBeSuccess + unless (Array.null config.installTestPackages) do + spago ([ "install", "--test-deps" ] <> config.installTestPackages) >>= shouldBeSuccess + for_ config.main \main -> + FS.writeTextFile (Path.concat [ "src", "Main.purs" ]) $ writeMain main + for_ config.testMain \testMain -> + FS.writeTextFile (Path.concat [ "test", "Test", "Main.purs" ]) $ writeTestMain testMain + -- get rid of "Compiling ..." messages and other compiler warnings + spago [ "build" ] >>= shouldBeSuccess diff --git a/test/Spago/Build/Polyrepo.purs b/test/Spago/Build/Polyrepo.purs index 77085e9be..fd74561f1 100644 --- a/test/Spago/Build/Polyrepo.purs +++ b/test/Spago/Build/Polyrepo.purs @@ -8,8 +8,7 @@ module Test.Spago.Build.Polyrepo where import Test.Prelude import Data.Array as Array -import Data.Map as Map -import Data.String (Pattern(..), Replacement(..)) +import Data.String (Pattern(..)) import Data.String as String import Node.Path as Path import Node.Platform as Platform @@ -32,18 +31,15 @@ spec = Spec.describe "polyrepo" do escapePathInErrMsg = case Process.platform of Just Platform.Win32 -> Array.intercalate "\\" _ -> Array.intercalate "/" + + -- | `spago [ "init" ]` will create files that we will immediately + -- | delete (i.e. `src/Main.purs` and `test/Main.purs`) + -- | or overwrite (i.e. `spago.yaml`). So, we don't call it here. writeWorkspaceOnlySpagoYamlFile = do - -- `spago [ "init" ]` will create files that we will immediately - -- delete (i.e. `src/Main.purs` and `test/Main.purs`) - -- or overwrite (i.e. `spago.yaml`). So, we don't call it here. FS.writeYamlFile Config.configCodec "spago.yaml" $ Init.defaultConfig' $ WorkspaceOnly { setVersion: Just $ unsafeFromRight $ Version.parse "0.0.1" } - -- | packageToModuleName "package-name" = "PACKAGE.NAME" - packageToModuleName packageName = - String.toUpper (String.replaceAll (Pattern "-") (Replacement ".") packageName) - setupDir { packageName, spagoYaml, srcMain, testMain } = do let src = Path.concat [ packageName, "src" ] @@ -57,7 +53,6 @@ spec = Spec.describe "polyrepo" do FS.writeYamlFile Config.configCodec (Path.concat [ packageName, "spago.yaml" ]) spagoYaml copyTemplate srcMain [ "src", "Main.purs" ] copyTemplate testMain [ "test", "Main.purs" ] - pure { src, test } mkModuleContent { modName, imports, body } = Just $ Array.intercalate "\n" $ @@ -68,97 +63,87 @@ spec = Spec.describe "polyrepo" do <> imports <> body - mkMainModuleContent { modName, imports, logStatement, packageName } = - mkModuleContent - { modName - , imports: - [ "" - , "import Effect (Effect)" - , "import Effect.Console (log)" - ] - <> imports - , body: - [ "" - , "main :: Effect Unit" - , "main = do" - , " " <> logStatement - ] - <> - ( packageName # maybe [] \pkgName -> + Spec.describe "inter-workspace package dependencies" do + + let + setupPackageWithDeps + :: { packageName :: String + , hasTest :: Boolean + , deps :: Array { dep :: String, import :: String, alias :: String } + } + -> Aff { dep :: String, import :: String, alias :: String } + setupPackageWithDeps { packageName, hasTest, deps } = do + let + packageAlias = packageToModuleName packageName + packageNameValues + | Array.null deps = "\"no deps\"" + | otherwise = Array.intercalate " <> " $ map (\r -> r.alias <> ".packageNameValue") deps + setupDir + { packageName: packageName + , spagoYaml: mkPackageOnlyConfig + { packageName, srcDependencies: [ "prelude" ] <> map _.dep deps } + [ configAddTestMain + , configAddTestDependencies [ "prelude", "console", "effect" ] + ] + , srcMain: mkModuleContent + { modName: mkSrcModuleName packageName + , imports: map _.import deps + , body: + [ "" + , "libraryUsage :: String" + , "libraryUsage = packageNameValue <> " <> packageNameValues + , "" + , "packageNameValue :: String" + , "packageNameValue = \"package name \" <> \"" <> packageName <> "\"" + ] + } + , testMain: + if not hasTest then Nothing + else mkModuleContent + { modName: mkTestModuleName packageName + , imports: [ "" - , "packageName :: String" - , "packageName = \"" <> pkgName <> "\"" + , "import Effect (Effect)" + , "import Effect.Console (log)" + , "import " <> mkSrcModuleName packageName <> " as " <> packageAlias ] - ) - } - - Spec.describe "inter-workspace package dependencies" do + <> (map _.import deps) + , body: + [ "" + , "main :: Effect Unit" + , "main = do" + , " log $ \"Test for \" <> " <> packageAlias <> ".packageNameValue <> " <> packageNameValues + ] + } + } + pure + { dep: packageName + , import: "import " <> mkSrcModuleName packageName <> " as " <> packageAlias + , alias: packageAlias + } {- ```mermaid flowchart TD subgraph "Case 1" - A ---> Dep0["effect, console, prelude"] + A ---> Dep0["prelude"] B ---> Dep0 end ``` -} Spec.it "Case 1 (independent packages) builds" \{ spago } -> do writeWorkspaceOnlySpagoYamlFile - void $ setupDir - { packageName: "package-a" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-one-package-a" - , dependencies: [ "console", "effect", "prelude" ] - , test: Just { moduleMain: "Subpackage.A.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.A.Main" - , imports: [] - , logStatement: "log packageName" - , packageName: Just "packageA" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.A.Test.Main" - , imports: [ "import Subpackage.A.Main as Package" ] - , logStatement: "log $ \"Test for \" <> Package.packageName" - , packageName: Nothing - } - } - void $ setupDir - { packageName: "package-b" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-one-package-b" - , dependencies: [ "console", "effect", "prelude" ] - , test: Just { moduleMain: "Subpackage.B.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.B.Main" - , imports: [] - , logStatement: "log packageName" - , packageName: Just "packageB" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.B.Test.Main" - , imports: [ "import Subpackage.B.Main as Package" ] - , logStatement: "log $ \"Test for \" <> Package.packageName" - , packageName: Nothing - } - } + void $ setupPackageWithDeps { packageName: "package-a", hasTest: true, deps: [] } + void $ setupPackageWithDeps { packageName: "package-b", hasTest: true, deps: [] } + void $ setupPackageWithDeps { packageName: "package-c", hasTest: true, deps: [] } spago [ "build" ] >>= shouldBeSuccess {- ```mermaid flowchart TD subgraph "Case 2" - A2 ---> effect2 - A2 ---> console2 A2 ---> prelude2 A2 ---> Shared2 - B2 ---> effect2 - B2 ---> console2 B2 ---> prelude2 B2 ---> Shared2 Shared2 ---> prelude2 @@ -167,86 +152,18 @@ spec = Spec.describe "polyrepo" do -} Spec.it "Case 2 (shared dependencies packages) builds" \{ spago } -> do writeWorkspaceOnlySpagoYamlFile - void $ setupDir - { packageName: "package-shared" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-two-package-shared" - , dependencies: [ "prelude" ] - , test: Nothing - , build: Nothing - } - , srcMain: mkModuleContent - { modName: "Subpackage.Shared.Lib" - , imports: [] - , body: - [ "" - , "packageName :: String" - , "packageName = \"package\" <> \"Shared\"" - ] - } - , testMain: Nothing - } - void $ setupDir - { packageName: "package-a" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-two-package-a" - , dependencies: [ "console", "effect", "prelude", "case-two-package-shared" ] - , test: Just { moduleMain: "Subpackage.A.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.A.Main" - , imports: [ "import Subpackage.Shared.Lib as Shared" ] - , logStatement: "log packageName" - , packageName: Just "packageA" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.A.Test.Main" - , imports: - [ "import Subpackage.A.Main as Package" - , "import Subpackage.Shared.Lib as Shared" - ] - , logStatement: "log $ \"Test for \" <> Package.packageName <> \", not \" <> Shared.packageName" - , packageName: Nothing - } - } - void $ setupDir - { packageName: "package-b" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-two-package-b" - , dependencies: [ "console", "effect", "prelude", "case-two-package-shared" ] - , test: Just { moduleMain: "Subpackage.B.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.B.Main" - , imports: [ "import Subpackage.Shared.Lib as Shared" ] - , logStatement: "log packageName" - , packageName: Just "packageB" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.B.Test.Main" - , imports: - [ "import Subpackage.B.Main as Package" - , "import Subpackage.Shared.Lib as Shared" - ] - , logStatement: "log $ \"Test for \" <> Package.packageName <> \", not \" <> Shared.packageName" - , packageName: Nothing - } - } + shared <- setupPackageWithDeps { packageName: "package-shared", hasTest: false, deps: [] } + void $ setupPackageWithDeps { packageName: "package-a", hasTest: true, deps: [ shared ] } + void $ setupPackageWithDeps { packageName: "package-b", hasTest: true, deps: [ shared ] } spago [ "build" ] >>= shouldBeSuccess {- ```mermaid flowchart TD subgraph "Case 3" - A3 ---> effect3 - A3 ---> console3 A3 ---> prelude3 A3 ---> B3 A3 ---> C3 - B3 ---> effect3 - B3 ---> console3 B3 ---> prelude3 B3 ---> C3 C3 ---> prelude3 @@ -255,87 +172,16 @@ spec = Spec.describe "polyrepo" do -} Spec.it "Case 3 (dependencies: A&B -> C; A -> B) builds" \{ spago } -> do writeWorkspaceOnlySpagoYamlFile - void $ setupDir - { packageName: "package-c" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-three-package-c" - , dependencies: [ "prelude" ] - , test: Nothing - , build: Nothing - } - , srcMain: mkModuleContent - { modName: "Subpackage.C.Lib" - , imports: [] - , body: - [ "" - , "packageName :: String" - , "packageName = \"package\" <> \"C\"" - ] - } - , testMain: Nothing - } - void $ setupDir - { packageName: "package-b" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-three-package-b" - , dependencies: [ "console", "effect", "prelude", "case-three-package-c" ] - , test: Just { moduleMain: "Subpackage.B.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.B.Main" - , imports: [ "import Subpackage.C.Lib as C" ] - , logStatement: "log packageName" - , packageName: Just "packageB" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.B.Test.Main" - , imports: - [ "import Subpackage.B.Main as Package" - , "import Subpackage.C.Lib as C" - ] - , logStatement: "log $ Package.packageName <> \" is not \" <> C.packageName" - , packageName: Nothing - } - } - void $ setupDir - { packageName: "package-a" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-three-package-a" - , dependencies: [ "console", "effect", "prelude", "case-three-package-b", "case-three-package-c" ] - , test: Just { moduleMain: "Subpackage.A.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.A.Main" - , imports: - [ "import Subpackage.B.Main as B" - , "import Subpackage.C.Lib as C" - ] - , logStatement: "log $ packageName <> \" is not \" <> C.packageName <> \" or \" <> B.packageName" - , packageName: Just "packageA" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.A.Test.Main" - , imports: - [ "import Subpackage.A.Main as Package" - , "import Subpackage.C.Lib as C" - ] - , logStatement: "log $ \"Test for \" <> Package.packageName <> \", not \" <> C.packageName" - , packageName: Nothing - } - } + pkgC <- setupPackageWithDeps { packageName: "package-c", hasTest: false, deps: [] } + pkgB <- setupPackageWithDeps { packageName: "package-b", hasTest: true, deps: [ pkgC ] } + void $ setupPackageWithDeps { packageName: "package-a", hasTest: true, deps: [ pkgC, pkgB ] } spago [ "build" ] >>= shouldBeSuccess {- ```mermaid flowchart TD subgraph "Case 4 (duplicate module)" - A4 ---> effect4 - A4 ---> console4 A4 ---> prelude4 - B4 ---> effect4 - B4 ---> console4 B4 ---> prelude4 C4 ---> prelude4 end @@ -343,70 +189,29 @@ spec = Spec.describe "polyrepo" do -} Spec.it "Declaring 2+ modules with the same name across 2+ packages fails to build" \{ spago } -> do writeWorkspaceOnlySpagoYamlFile - void $ setupDir - { packageName: "package-c" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-four-package-c" - , dependencies: [ "prelude" ] - , test: Nothing - , build: Nothing - } - , srcMain: mkModuleContent - { modName: "Subpackage.C.Lib" - , imports: [] - , body: - [ "" - , "packageName :: String" - , "packageName = \"package\" <> \"C\"" - ] - } - , testMain: Nothing - } - void $ setupDir - { packageName: "package-b" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-four-package-b" - , dependencies: [ "console", "effect", "prelude" ] - , test: Just { moduleMain: "Subpackage.SameName.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.SameName.Main" - , imports: [] - , logStatement: "log packageName" - , packageName: Just "packageB" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.SameName.Test.Main" - , imports: [ "import Subpackage.SameName.Main as Package" ] - , logStatement: "log Package.packageName" - , packageName: Nothing - } - } - void $ setupDir - { packageName: "package-a" - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName "case-four-package-a" - , dependencies: [ "console", "effect", "prelude" ] - , test: Just { moduleMain: "Subpackage.SameName.Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } - , build: Nothing - } - , srcMain: mkMainModuleContent - { modName: "Subpackage.SameName.Main" - , imports: [] - , logStatement: "log packageName" - , packageName: Just "packageA" - } - , testMain: mkMainModuleContent - { modName: "Subpackage.SameName.Test.Main" - , imports: [ "import Subpackage.SameName.Main as Package" ] - , logStatement: "log Package.packageName" - , packageName: Nothing + let + sameModuleName = "SameModuleName" + setupPackage packageName samePkgName = do + void $ setupDir + { packageName: packageName + , spagoYaml: mkPackageOnlyConfig { packageName, srcDependencies: [ "prelude" ] } [] + , srcMain: mkModuleContent + { modName: if samePkgName then sameModuleName else mkSrcModuleName packageName + , imports: [] + , body: + [ "" + , "packageName :: String" + , "packageName = \"" <> packageName <> "\"" + ] + } + , testMain: Nothing } - } + setupPackage "package-a" true + setupPackage "package-b" true + setupPackage "package-c" false let hasExpectedModules stdErr = do - let exp = "Module Subpackage.SameName.Main has been defined multiple times" + let exp = "Module " <> sameModuleName <> " has been defined multiple times" unless (String.contains (Pattern exp) stdErr) do Assert.fail $ "STDERR did not contain text:\n" <> exp <> "\n\nStderr was:\n" <> stdErr @@ -416,8 +221,8 @@ spec = Spec.describe "polyrepo" do let setupPackageWithUnusedNameWarning packageName deps strict censorShadowedName includeTestCode = do let - mkMainFile testPart = mkModuleContent - { modName: testPart <> "Subpackage." <> packageToModuleName packageName + mkMainFile isSrc = mkModuleContent + { modName: (if isSrc then mkSrcModuleName else mkTestModuleName) packageName , imports: [] , body: [ "" @@ -428,32 +233,24 @@ spec = Spec.describe "polyrepo" do } void $ setupDir { packageName: packageName - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName packageName - , dependencies: [ "prelude" ] <> deps - , build: Just - { strict: pure strict - , censorProjectWarnings: - if censorShadowedName then - Just $ Config.CensorSpecificWarnings $ pure $ Config.ByCode "UnusedName" - else - Nothing - , pedanticPackages: Nothing - } - , test: Just - { moduleMain: "Test.Subpackage." <> packageToModuleName packageName - , strict: pure strict - , censorTestWarnings: - if censorShadowedName then - Just $ Config.CensorSpecificWarnings $ pure $ Config.ByCode "UnusedName" - else - Nothing - , pedanticPackages: Nothing - , dependencies: Nothing - } - } - , srcMain: mkMainFile "" - , testMain: if includeTestCode then mkMainFile "Test." else Nothing + , spagoYaml: do + let censorValue = Config.CensorSpecificWarnings $ pure $ Config.ByCode "UnusedName" + mkPackageOnlyConfig + { packageName, srcDependencies: [ "prelude" ] <> deps } + $ + [ if strict then configAddSrcStrict else identity + , if censorShadowedName then configAddSrcCensor censorValue else identity + ] + <> + ( if not includeTestCode then [] + else + [ configAddTestMain + , if strict then configAddTestStrict else identity + , if censorShadowedName then configAddTestCensor censorValue else identity + ] + ) + , srcMain: mkMainFile true + , testMain: if includeTestCode then mkMainFile false else Nothing } Spec.it "build succeeds when 'strict: true' because warnings were censored" \{ spago } -> do writeWorkspaceOnlySpagoYamlFile @@ -492,12 +289,7 @@ spec = Spec.describe "polyrepo" do let setupNonRootPackage packageName = void $ setupDir { packageName: packageName - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName packageName - , dependencies: [ "prelude" ] - , test: Nothing - , build: Nothing - } + , spagoYaml: mkPackageOnlyConfig { packageName, srcDependencies: [ "prelude" ] } [] , srcMain: mkModuleContent { modName: "Subpackage." <> packageToModuleName packageName , imports: [] @@ -538,9 +330,6 @@ spec = Spec.describe "polyrepo" do Spec.describe "pedantic packages" do let - mkSrcModuleName packageName = "Src." <> packageToModuleName packageName - mkTestModuleName packageName = "Test." <> packageToModuleName packageName - {- /-- tuples (unused dep by `src`) newtype (transitive dep) <-- control (direct dep) <--+ @@ -552,22 +341,13 @@ spec = Spec.describe "polyrepo" do - test imports `either` because it inherit's `src`'s dependencies implicitly -} setupPackage packageName { src, test } = void $ setupDir { packageName: packageName - , spagoYaml: Init.defaultConfig' $ PackageOnly - { name: mkPackageName packageName - , dependencies: [ "prelude", "control", "tuples" ] - , build: Just - { pedanticPackages: Just src - , censorProjectWarnings: Nothing - , strict: Nothing - } - , test: Just - { pedanticPackages: Just test - , censorTestWarnings: Nothing - , strict: Nothing - , moduleMain: mkTestModuleName packageName - , dependencies: Just $ Config.Dependencies $ Map.fromFoldable $ map (flip Tuple Nothing <<< mkPackageName) [ "prelude", "control", "either" ] - } - } + , spagoYaml: mkPackageOnlyConfig + { packageName, srcDependencies: [ "prelude", "control", "tuples" ] } + [ if src then configAddSrcPedantic else identity + , configAddTestMain + , configAddTestDependencies [ "prelude", "control", "either" ] + , if test then configAddTestPedantic else identity + ] , srcMain: mkModuleContent { modName: mkSrcModuleName packageName , imports: @@ -584,7 +364,7 @@ spec = Spec.describe "polyrepo" do { modName: mkTestModuleName packageName , imports: [ "import Control.Alt as Control" - , "import Data.Newtype as Newtype" + , "import Safe.Coerce as Coerce" ] , body: [ "" @@ -603,7 +383,11 @@ spec = Spec.describe "polyrepo" do [ toMsgPrefix isSrc <> " for package '" <> package <> "' declares unused dependencies - please remove them from the project config:" , " - " <> (if isSrc then "tuples" else "either") ] - mkTransitiveDepErr isSrc package = + mkTransitiveDepErr isSrc package = do + let + { pkg, mkModName, pkgModName } = + if isSrc then { pkg: "newtype", mkModName: mkSrcModuleName, pkgModName: "Data.Newtype" } + else { pkg: "safe-coerce", mkModName: mkTestModuleName, pkgModName: "Safe.Coerce" } Array.intercalate "\n" [ Array.fold [ toMsgPrefix isSrc @@ -611,9 +395,9 @@ spec = Spec.describe "polyrepo" do , package , "' import the following transitive dependencies - please add them to the project dependencies, or remove the imports:" ] - , " newtype" - , " from `" <> (if isSrc then mkSrcModuleName else mkTestModuleName) package <> "`, which imports:" - , " Data.Newtype" + , " " <> pkg + , " from `" <> mkModName package <> "`, which imports:" + , " " <> pkgModName ] Spec.it "when package config has 'pedantic_packages: true', build fails with expected errors" \{ spago } -> do writeWorkspaceOnlySpagoYamlFile diff --git a/test/Spago/Bundle.purs b/test/Spago/Bundle.purs index f586167a6..a3b389b1d 100644 --- a/test/Spago/Bundle.purs +++ b/test/Spago/Bundle.purs @@ -9,10 +9,15 @@ spec :: Spec Unit spec = Spec.around withTempDir do Spec.describe "bundle" do - Spec.it "bundles into an app" \{ spago, fixture } -> do + Spec.it "bundles into an app (browser)" \{ spago, fixture } -> do spago [ "init" ] >>= shouldBeSuccess - spago [ "bundle", "-v", "--bundle-type", "app", "--outfile", "bundle-app.js" ] >>= shouldBeSuccess - checkFixture "bundle-app.js" (fixture "bundle-app.js") + spago [ "bundle", "-v", "--bundle-type", "app", "--outfile", "bundle-app-browser.js" ] >>= shouldBeSuccess + checkFixture "bundle-app-browser.js" (fixture "bundle-app-browser.js") + + Spec.it "bundles into an app (node)" \{ spago, fixture } -> do + spago [ "init" ] >>= shouldBeSuccess + spago [ "bundle", "-v", "--bundle-type", "app", "--outfile", "bundle-app-node.js", "--platform", "node" ] >>= shouldBeSuccess + checkFixture "bundle-app-node.js" (fixture "bundle-app-node.js") Spec.it "bundles into a module" \{ spago, fixture } -> do spago [ "init" ] >>= shouldBeSuccess @@ -23,11 +28,17 @@ spec = Spec.around withTempDir do spago [ "bundle", "--bundle-type=module", "--outfile", "bundle-module.js" ] >>= shouldBeSuccess checkFixture "bundle-module.js" (fixture "bundle-module.js") - Spec.it "bundles an app with source map" \{ spago, fixture } -> do + Spec.it "bundles an app with source map (browser)" \{ spago, fixture } -> do + spago [ "init" ] >>= shouldBeSuccess + spago [ "bundle", "-v", "--outfile", "bundle-app-browser-map.js", "--source-maps", "--bundle-type", "app" ] >>= shouldBeSuccess + checkFixture "bundle-app-browser-map.js" (fixture "bundle-app-browser-map.js") + checkFixture "bundle-app-browser-map.js.map" (fixture "bundle-app-browser-map.js.map") + + Spec.it "bundles an app with source map (node)" \{ spago, fixture } -> do spago [ "init" ] >>= shouldBeSuccess - spago [ "bundle", "-v", "--outfile", "bundle-app-map.js", "--source-maps", "--bundle-type", "app" ] >>= shouldBeSuccess - checkFixture "bundle-app-map.js" (fixture "bundle-app-map.js") - checkFixture "bundle-app-map.js.map" (fixture "bundle-app-map.js.map") + spago [ "bundle", "-v", "--outfile", "bundle-app-node-map.js", "--source-maps", "--bundle-type", "app", "--platform", "node" ] >>= shouldBeSuccess + checkFixture "bundle-app-node-map.js" (fixture "bundle-app-node-map.js") + checkFixture "bundle-app-node-map.js.map" (fixture "bundle-app-node-map.js.map") Spec.it "bundles a module with source map" \{ spago, fixture } -> do spago [ "init" ] >>= shouldBeSuccess diff --git a/test/Spago/Graph.purs b/test/Spago/Graph.purs new file mode 100644 index 000000000..7e9749277 --- /dev/null +++ b/test/Spago/Graph.purs @@ -0,0 +1,34 @@ +module Test.Spago.Graph where + +import Test.Prelude + +import Test.Spec (Spec) +import Test.Spec as Spec + +spec :: Spec Unit +spec = Spec.around withTempDir do + Spec.describe "graph" do + + Spec.it "can output the module graph in JSON" \{ spago, fixture } -> do + spago [ "init", "--name", "my-project", "--package-set", "41.2.0" ] >>= shouldBeSuccess + spago [ "graph", "modules", "--json" ] >>= shouldBeSuccessOutput (fixture "graph-modules.json") + + Spec.it "can output the module graph for graphviz" \{ spago, fixture } -> do + spago [ "init", "--name", "my-project", "--package-set", "41.2.0" ] >>= shouldBeSuccess + spago [ "graph", "modules", "--dot" ] >>= shouldBeSuccessOutput (fixture "graph-modules.dot") + + Spec.it "can topologically sort modules" \{ spago, fixture } -> do + spago [ "init", "--name", "my-project", "--package-set", "41.2.0" ] >>= shouldBeSuccess + spago [ "graph", "modules", "--topo" ] >>= shouldBeSuccessOutput (fixture "graph-modules-topo.txt") + + Spec.it "can output the package graph in JSON" \{ spago, fixture } -> do + spago [ "init", "--name", "my-project", "--package-set", "41.2.0" ] >>= shouldBeSuccess + spago [ "graph", "packages", "--json" ] >>= shouldBeSuccessOutput (fixture "graph-packages.json") + + Spec.it "can output the package graph for graphviz" \{ spago, fixture } -> do + spago [ "init", "--name", "my-project", "--package-set", "41.2.0" ] >>= shouldBeSuccess + spago [ "graph", "packages", "--dot" ] >>= shouldBeSuccessOutput (fixture "graph-packages.dot") + + Spec.it "can topologically sort packages" \{ spago, fixture } -> do + spago [ "init", "--name", "my-project", "--package-set", "41.2.0" ] >>= shouldBeSuccess + spago [ "graph", "packages", "--topo" ] >>= shouldBeSuccessOutput (fixture "graph-packages-topo.txt") diff --git a/test/Spago/Install.purs b/test/Spago/Install.purs index fd3e1ce2c..529e86104 100644 --- a/test/Spago/Install.purs +++ b/test/Spago/Install.purs @@ -4,11 +4,11 @@ import Test.Prelude import Data.Array as Array import Data.Map as Map +import Effect.Now as Now import Node.FS.Aff as FSA import Node.Path as Path import Registry.Version as Version import Spago.Command.Init as Init -import Spago.Core.Config (Dependencies(..)) import Spago.Core.Config as Config import Spago.FS as FS import Spago.Log (LogVerbosity(..)) @@ -80,13 +80,13 @@ spec = Spec.around withTempDir do { git: "https://github.com/purescript/spago.git" , ref: "master" , subdir: Nothing - , dependencies: Just $ Dependencies $ Map.singleton (mkPackageName "b") Nothing + , dependencies: Just $ mkDependencies [ "b" ] } , Tuple (mkPackageName "b") $ Config.ExtraRemotePackage $ Config.RemoteGitPackage { git: "https://github.com/purescript/spago.git" , ref: "master" , subdir: Nothing - , dependencies: Just $ Dependencies $ Map.singleton (mkPackageName "a") Nothing + , dependencies: Just $ mkDependencies [ "a" ] } ] } @@ -124,7 +124,7 @@ spec = Spec.around withTempDir do { git: "https://github.com/spacchetti/purescript-metadata.git" , ref: "spago-test/branch-with-slash" , subdir: Nothing - , dependencies: Just $ Dependencies $ Map.singleton (mkPackageName "prelude") Nothing + , dependencies: Just $ mkDependencies [ "prelude" ] } ] } @@ -158,7 +158,7 @@ spec = Spec.around withTempDir do { git: "https://github.com/purescript/spago.git" , ref: "cbdbbf8f8771a7e43f04b18cdefffbcb0f03a990" , subdir: Nothing - , dependencies: Just $ Dependencies $ Map.singleton (mkPackageName "prelude") Nothing + , dependencies: Just $ mkDependencies [ "prelude" ] } ] } @@ -186,7 +186,7 @@ spec = Spec.around withTempDir do { git: "https://github.com/purescript/spago.git" , ref: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" , subdir: Nothing - , dependencies: Just $ Dependencies $ Map.singleton (mkPackageName "prelude") Nothing + , dependencies: Just $ mkDependencies [ "prelude" ] } ] } @@ -239,7 +239,8 @@ spec = Spec.around withTempDir do Spec.it "can build with a newer (but still compatible) compiler than the one in the package set" \{ spago } -> do spago [ "init", "--package-set", "10.0.0" ] >>= shouldBeSuccess - purs <- runSpago { logOptions: { color: false, verbosity: LogQuiet } } Purs.getPurs + startingTime <- liftEffect $ Now.now + purs <- runSpago { logOptions: { color: false, verbosity: LogQuiet, startingTime } } Purs.getPurs -- The package set 10.0.0 has purescript 0.15.4, so we check that we have a newer version case purs.version > mkVersion "0.15.4" of true -> pure unit @@ -266,12 +267,7 @@ writeConfigWithEither = do { git: "https://github.com/purescript/purescript-either.git" , ref: "af655a04ed2fd694b6688af39ee20d7907ad0763" , subdir: Nothing - , dependencies: Just $ Dependencies $ Map.fromFoldable - [ mkPackageName "control" /\ Nothing - , mkPackageName "invariant" /\ Nothing - , mkPackageName "maybe" /\ Nothing - , mkPackageName "prelude" /\ Nothing - ] + , dependencies: Just $ mkDependencies [ "control", "invariant", "maybe", "prelude" ] } ] } diff --git a/test/Spago/Upgrade.purs b/test/Spago/Upgrade.purs index 8431ce763..6765867fe 100644 --- a/test/Spago/Upgrade.purs +++ b/test/Spago/Upgrade.purs @@ -2,6 +2,7 @@ module Test.Spago.Upgrade where import Test.Prelude +import Effect.Now as Now import Spago.Core.Config (SetAddress(..)) import Spago.Core.Config as Core import Spago.Log (LogVerbosity(..)) @@ -19,7 +20,8 @@ spec = Spec.around withTempDir do -- we can't just check a fixture here, as there are new package set versions all the time. -- so we read the config file, and check that the package set version is more recent than the one we started with let initialVersion = mkVersion "20.0.1" - maybeConfig <- runSpago { logOptions: { color: false, verbosity: LogQuiet } } (Core.readConfig "spago.yaml") + startingTime <- liftEffect $ Now.now + maybeConfig <- runSpago { logOptions: { color: false, verbosity: LogQuiet, startingTime } } (Core.readConfig "spago.yaml") case maybeConfig of Right { yaml: { workspace: Just { package_set: Just (SetFromRegistry { registry }) } } } | registry > initialVersion -> pure unit Right { yaml: c } -> Assert.fail $ "Could not upgrade the package set, config: " <> printJson Core.configCodec c