@evancz I propose that a new “metadata” command be added to the Elm CLI. When invoked, it will:
- look for an
elm.json
file in current working directory
- compute the project’s exact dependencies, including direct, indirect, and test dependencies
- print a human-readable list of those dependencies and versions to stdout
The human-readable output would be similar to npm ls
. For example:
$ elm metadata
Project Dependencies
└─┬ elm/core@1.0.2
├ elm/json@1.0.0
| â”” elm/core@1.0.2 (deduped)
├ elm/html@1.0.0
| ├ elm/virtual-dom@1.0.2
| ├ elm/json@1.0.0 (deduped)
| â”” elm/core@1.0.2 (deduped)
â”” elm/time@1.0.0
â”” elm/core@1.0.2 (deduped)
Additional Test Dependencies
└── elm-explorations/test@1.0.0
â”” elm/random@1.0.0
Aside: one complication is that everything depends on elm/core
, so if we show the true graph there would be a lot of noise. We could filter out elm/core
from the human-readable output, but that would be a lie.
Machine-readable Output
A --report=json
option will cause it to instead output the summary in JSON format. This option would be intended for Elm language plugins and other tools, and backwards compatibility would, ideally, be maintained.
The bare minimum output would look something like this:
{
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/json": "1.1.2",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
But I think we should aim a little higher in order to:
- provide some wiggle room for backwards-compatible changes
- include enough information so that the caller does not need to read any
elm.json
files
- make implicit things explicit (locations of packages on disk)
So I propose the following:
{
"project": {
"type": "package", // either "application" or "package"
"source-directories": [ "src" ], // "application" projects only
"name": "klazuka/elm-foo", // "package" projects only
"version": "1.0.1", // "package" projects only
"path": "~/dev/elm-foo",
"elm-version": "0.19.0",
"direct-dependencies": [ "elm/core", "elm/html" ],
"direct-test-dependencies": [ "elm-explorations/test" ]
},
"packages": [
{
"name": "elm/core",
"version": "1.0.2",
"path": "~/.elm/0.19.0/package/elm/core/1.0.2",
"exposed-modules": [ "Basics", "List", "String" /* , ... */ ],
"dependencies": [],
"test-dependencies": []
},
{
"name": "elm/html",
"version": "1.0.0",
"path": "~/.elm/0.19.0/package/elm/html/1.0.0",
"exposed-modules": [ "Html", "Html.Attributes", "Html.Events" ],
"dependencies": [ "elm/core", "elm/json", "elm/virtual-dom" ],
"test-dependencies": []
},
{
"name": "elm/json",
"version": "1.0.0",
"path": "~/.elm/0.19.0/package/elm/json/1.0.0",
"exposed-modules": [ "Json.Decode", "Json.Encode" ],
"dependencies": [ "elm/core" ],
"test-dependencies": []
},
{
"name": "elm/virtual-dom",
"version": "1.0.2",
"path": "~/.elm/0.19.0/package/elm/virtual-dom/1.0.2",
"exposed-modules": [ "VirtualDom" ],
"dependencies": [ "elm/core", "elm/json" ],
"test-dependencies": []
},
{
"name": "elm-explorations/test",
"version": "1.0.0",
"path": "~/.elm/0.19.0/package/elm-explorations/test/1.0.2",
"exposed-modules": [ "Test", "Test.Runner", "Expect" /* , ... */ ],
"dependencies": ["elm/core", "elm/random"],
"test-dependencies": []
},
{
"name": "elm/random",
"version": "1.0.0",
"path": "~/.elm/0.19.0/package/elm/random/1.0.0",
"exposed-modules": [ "Random" ],
"dependencies": ["elm/core", "elm/time"],
"test-dependencies": []
}
]
}
The idea is that since the Elm binary will be crawling the dependency graph and doing a bunch of file I/O, we may as well collect all of the information from the elm.json
files and include it in the output. This way the caller (i.e. a language plugin) can make a single call to elm metadata
and obtain all of the information it needs to locate modules relative to a given source file. It also decouples Elm tools from both the elm.json
format and the directory structure inside the global Elm package cache.
Aside: When presenting the human-readable dependency graph, there is no need to show the test-dependencies of your project’s dependencies (because no one cares). However, in the JSON output, the test-dependencies of each package must be included so that things like go-to-declaration can work. For example, I might open in my editor the tests for one of my project’s dependencies. That test will import some modules, and we need to know which packages provide those modules.
Bonus Information
Additional fields could be added to the package object.
- sha256 hash
- this was suggested by @jerith in order to verify package integrity
- I’m not sure where this comes from (maybe it’s the Git tag sha?)
- package source-directories
- Elm 0.19 implicitly assumes that there is a single source-directory for a package and it is called “src”
- we could include a field like
"source-directories": [ "src" ]
to make this explicit
- if we were to include it, then the
project
section should also include source-directories
for package projects
- this is probably a bad idea, but I wanted to put it out there
Complications
It’s possible that an elm.json
file has been modified without actually running elm install
. In which case, a stated dependency may not actually exist on disk. Similarly, if the user installed some Elm packages using a custom ELM_HOME
environment variable, but now runs elm metadata
without that environment variable set, the Elm CLI will be unable to locate the installed dependencies. The latter situation may be particularly likely in the case where an Elm language plugin or other tool is running elm metadata
from a non-shell environment and thus ELM_HOME
may not be set.
In such cases the Elm CLI should print a message to stderr and exit with an error code.
Schema Compatibility
Seeing as how Elm is not yet a 1.0 product, it’s probably sufficient to assume that any changes to the output will be purely additive without breaking backward compatibility. JSON is good at this. Once Elm begins to make compatibility guarantees, this should be revisited so that (1) the caller can specify the schema version that it can handle and (2) the output includes a field describing the schema version that it was written with.
Naming
Calling the CLI command “metadata” is based on a similar command in Rust’s cargo
package manager. It seems like a good idea to choose a suitably broad name so that if other tooling needs arise, it can be shoehorned into this command. Alternative names to consider: “project”, “structure”, “info”, or “plan” (following Cabal)…
But seeing as how the above proposal is primarily about dependencies, it may be better to consider more concrete names such as “dependencies” or “build-plan”.