Installs any Elm package from any Git server including both Elm and NPM dependencies
WARNING: This is NOT the Official Elm Package Manager. Grove can install official and non-official packages. If you use Grove to install non-official packages, realize that those packages offer NO GUARANTEES regarding RUNTIME ERRORS.
You can, however, benefit from all of the advanced Grove features AND all the runtime safety of the Official Elm Packages by configuring Grove to operate in safe mode (see Configure Grove for Safety).
- Written in Elm
- Enforce Semantic Version
- Supports Official, Native and Effects Manager packages
- Install from Github, Gitlab, private Git servers, etc.
- Install local packages during development (via symbolic links)
- Manage Elm and NPM dependencies (works without NPM as well)
- Uninstall packages
- Bump package version with validations (with git check in)
- Create documentation markdown from code comments (module & function)
- Initialize package (create
elm-package.json)
- Elm 0.19 support (once released)
Make sure you have the following:
- Elm version 0.18.x
- npm version 5.3.0
Due to npm 5.x.x bugs, installing AND updating grove globally will have to be done unconventionally, for now.
Also, if you encounter access denied or permission issues using npm you may want to consult Fixing npm permissions.
cd ~
git clone https://github.com/panosoft/elm-grove
cd elm-grove
sudo npm linkcd ~/elm-grove
git pull
sudo npm linkYou can configure Grove either Globally or Locally. There is a command line option, --local that can be used on the grove config command to configure local safety only.
Local configuration overrides Global. So you can set Global as --safe=on and then set a particular repository's Local as --local --safe=none.
grove config --safe=onThis will DISALLOW Non-official Elm Packages from being installed.
NOTE: This check is NOT performed when you link to local packages since it is assumed that these packages are part of your codebase.
grove config --safe=offThis will ALLOW Non-official Elm Packages from being installed, but will display a WARNING to remind you of the risks.
NOTE: This check is NOT performed when you link to local packages since it is assumed that these packages are part of your codebase.
grove config --safe=noneThis will produce no messages regarding package statuses.
grove config --safe=This will REMOVE the safe mode option from Grove's configuration file, e.g. when you no longer want the local override (the above command would need the --local option in that particular case).
Grove's global configuration file, grove-config.json, is in the user's home directory. Local configuration files are at the root of the repository.
Assuming you're starting a new project that needs the following:
elm-lang/core
grove init
grove installThe grove init adds elm-lang/core in elm-package.json. The grove install installs all packages in elm-packages.json, which is elm-lang/core.
Assuming you're starting a new project that needs the following:
elm-lang/coreelm-lang/htmlpanosoft/elm-utilsgroup/repo(from Gitlab atgitlab.private.com)
grove init
grove install elm-lang/html panosoft/elm-utils git@gitlab.private.com:group/repoThe grove init adds elm-lang/core in elm-package.json. The grove install adds the specified of the packages to elm-package.json and installs all packages.
The standard package manager that comes with Elm is very limited. It will not accept packages with Native code in them. This rules out any server side code, Effects Managers, Elm running in Electron or on mobile devices.
Grovesupports packages that have native code.
When written for node servers, Elm packages that have NPM dependencies must be manually added to the main program's package.json. You also must manually check to make sure that the top-level NPM packages are semantically equivalent versions. (see Code Rewriting)
Groveupdates your program'spackage.jsonwith packages that have native code and then runsnpm installandnpm uninstallautomatically. It also removes the need for semantic equivalence.
When you're working on multiple repositories at once and there are interdependencies (e.g. Repo1 depends on Repo2 and Repo3) and all of these repositories are being changed in unison, it becomes very difficult to test since there is no way to reference the local repositories.
One solution is to use NoRedInks's elm_self_publish but that becomes problematic when Repo2 depends on Repo3 directly but Repo1 does not. Since elm_self_publish updates Repo1's Elm package JSON for each repository that it copies, Repo1's Elm dependencies now includes Repo3 even though it should NOT since its an indirect dependency.
Another solutions are to manually copy these files or create symbolic links to the local repositories but these are time consuming, error prone and tedious.
Grovecan automatically create links to local repositories.
It's too easy to release packages that depend on older versions. The only way to check is by manually going to Github and checking for a newer version.
- This check is done during a version bump in
Grove(see Releasing a package).
Installing from locations other than Github is not possible with the standard package manager.
Grovewill accept fully qualified package names allowing it support any Git server.
The general command-line format is:
grove <command> [options]This command will print the command-line help.
grove helpThis command will print the version of Grove and the version of Elm that's supported.
grove versiongrove initThis command will build a bare-bones elm-package.json file. In order to do this, it will prompt you for the following:
Summary of package- a general description of the packageRepository name- the name of the repository in the format:group/name. Github naming conventions are adhered to.License- type of license for the packageSource directory- relative directory where the package code is stored
When prompted, the string in the brackets, [], is the default value if one is supported.
The following are the values of items that are NOT prompted for and therefore are constants:
Version- set to 0.0.0Exposed Modules- is an empty ListNative Modules- the flag is NOT includedDependencies-elm-lang/coreis the one and only dependency, to add more usegrove installElm Version- the current version supported to the next version
The init command will also prompt you to create a minimal package.json. If you respond with Yes, then the following keys will be created:
Name- set based on theRepository namepromptVersion- set to 0.0.0License- set based on theLicenseprompt
grove install elm-lang/htmlgrove install elm-node/core panosoft/elm_parent_child_updategrove install git@gitlab.mydomain.com:myGroup/repoThis command installs <package> which can have the following formats:
<repo>
git@<hostname>:<repo>[.git]
http[s]://<hostname>/<repo>[.git]where:
<repo>- the repository name in the formgroup/name, e.g.panosoft/elm-grove<hostname>- the name of the Git server, e.g.gitlab.mydomain.comorgithub.com[.git]- optional (may be required by some Git servers)
When ONLY <repo> is specified then Github is assumed and the following format is used:
https://github.com/<repo>.gitUninstalling packages removes the packages from Elm Packages and then it performs an Install minus the Npm install step. Then Npm Uninstall is performed.
grove uninstall elm-community/list-extragrove uninstall panosoft/elm-postgres elm-community/result-extragrove uninstall myGroup/repoThis command uninstalls <package> which can have the following the formats:
<repo>
git@<hostname>:<repo>[.git]
http[s]://<hostname>/<repo>[.git]where:
<repo>- the repository name in the formgroup/name, e.g.panosoft/elm-grove<hostname>- the name of the Git server, e.g.gitlab.mydomain.comorgithub.com[.git]- optional (may be required by some Git servers)
While the other formats are supported, it's easiest to just use the <repo>.
Both Module and Function Comments, which are just markdown, will be used to create documentation in a directory called elm-docs. Grove uses the panosoft/elm-docs package to generate documentation.
Please see panosoft/elm-docs for documentation on how to comment your code for documentation generation.
During a bump command, you can configure Grove to automatically generate documentation.
grove config --local --docs=onTypically, documentation configuration will be locally configured, but it can also be set globally by omitting the --local option.
When --docs=on, then the bump command will generate documentation.
You can disable documentation generation by using docs=off or remove it completely from the configuration by using docs=.
This gives you a chance to debug your documentation before you release your package.
grove docsgrove install --link git@gitlab.mydomain.com:myGroup/elm-thingWhen the link option is specified, grove will consult the grove-links.json (in the current directory) to determine which repos are to be installed with symlinks to local directories.
When running the command in the above example, elm-thing will NOT be linked if it is NOT in grove-links.json. Instead it will be installed from gitlab.mydomain.com.
It's also important to fully qualify the repository so that dependency-sources are properly updated in the Elm Package Json.
It is STRONGLY advised that grove-links.json is in your global .gitignore (or at least the local package's .gitignore) to ensure it never gets checked in.
Here the keys are package names and the values are paths to the local package that will be linked to. Paths can contain Environment Variables in the form {<env-variable-name>}.
Here's an example grove-links.json:
{
"myGroup/elm-thing": "{ELMDEV}/myGroup/elm-thing",
"anotherGroup/elm-other-thing": "{ELMDEV}/anotherGroup/elm-other-thing"
}where
{ELMDEV}- the value of the Environment VariableELMDEV
There are 3 types of releases:
- Normal -
HEADis based on the most recent release - Rebased -
HEADis based on a release that is not the most recent of it's major version - Legacy -
HEADis based on an oldermajorrelease number
For details on release scenarios see Understanding Release Scenarios.
The bump command performs many validation steps prior to optionally generating documentation and bumping the version.
It is HIGHLY RECOMMENDED that you use the --dry-run option to validate and display the differences between HEAD and the latest version HEAD is based on.
This gives you a chance to see if you unintentionally made breaking changes.
Here is an example of what the output looks like:
Releasing a package is a 2-step process.
- Bump version using
grove bumpcommand - Pushing repo with
git push && git push --tags(Without thegit push --tagscommand, the latest version of the package will not be recognized byGrove.)
Package versions are controlled by git tags, e.g. a tag 1.0.2 is a valid version tag whereas tag 1.0.2a, test and 1.2 are not.
Versions are of the following format:
<major>.<minor>.<patch>
where:
major,minorandpatchare numbers
Grove automatically determines the version number based on public interface changes following the semver rules:
- major - breaking change in public interface, NOT backward compatible
- minor - additional functionality added to public interface
- patch - no public interface changes
grove bumpThis will bump the version in both Elm and NPM package Json files (elm-package.json and package.json) keeping them in lock-step and then check in the Elm and NPM package Json files into git and tag that commit with the bumped version.
For details on how the version number is determined see Version Determination and Understanding Release Scenarios.
Numerous validations are performed prior to doing the bump:
- the latest version tag in the repo is the same version in both Elm and NPM package JSON Files
- the latest version tag in Elm package JSON matches the latest release your code is based on
- no links installed in
elm-stuff, i.e. this package, which is about to be released, MUST NOT be using any non-released packages - all packages in
elm-package.jsonare the latest versions (override with--allow-old-dependencies) - no uncommitted changes (override with
--allow-uncommitted)
Next, you must MANUALLY push the repo AND tags via:
git push && git push --tagsWithout the git push --tags command, the latest version of the package will not be recognized by Grove.
There are 2 types of Packages:
- Library, e.g.
panosoft/elm-utils - Application, e.g.
panosoft/elm-grove
Libraries expose modules via elm-package.json. Grove determines the version based on changes to the Public Interface of the package, i.e. the exposed functions of the exposed modules. The logic follows semver rules:
- Changes to an existing exposed function = Major
- Deletion of a previously exposed function = Major
- Addition of a new exposed function = Minor
- Otherwise = Patch
Applications do NOT have public interfaces, so you must provide the version type via bump options --major, --minor, or --patch.
Grove uses the documentation feature that is built-in to the Elm compiler to determine changes to the public interface. It compares HEAD with the most recent release that HEAD is based on.
There are times where this can produce false positives, i.e. Grove will think something has changed when it effectively has not, e.g.:
Version 3.0.1 code:
rename : String -> Task Error ()
rename filename =
HEAD code (based on 3.0.1):
type alias Filename =
String
rename : Filename -> Task Error ()
rename filename =
Here we have aliased types that cause a difference in signatures even though they are semantically equivalent.
This is a limitation in the Elm compiler since it doesn't provide additional information regarding the fully reduced to a non-aliased type.
At the moment, Grove doesn't try to resolve this. The hope is that, someday, Elm will resolve this issue when generating documentation. As far as I know, the standard Elm package manager also suffers from such a deficiency.
In order to understand Release Scenarios, let's assume the following git history where the smaller circles are releases and the larger circles all the possible HEADs of your repo:
A Normal release occurs when HEAD is based on the most recent release, in this example, that is 3.0.1.
If you need to support an older version of your package, e.g. to support an older version of Elm, then you are going to be making a Legacy Release.
A Legacy release is where the HEAD is based off of an older major release, e.g. 2.1.0 or 1.2.0.
Legacy releases are RESTRICTED to minor and patch. If your code makes breaking changes, then Grove will exit with an error.
If for some reason, you decide to base your next release on code from an older release, it is considered a Rebased release in Grove.
There are 2 types of rebased releases:
- (Rebased)
HEADis based off of an older release that shares the samemajorversion as the latest release, in this example, that could only be3.0.0. - (Rebased Legacy)
HEADis based off an older release of aLegacyrelease, e.g.2.0.0,1.2.0and1.0.1would be Rebased Legacy releases. Note that2.1.0and1.2.0would NOT be aRebasedrelease since they are the latest releases of thosemajorversions.
grove install --dry-run panosoft/elm-cmd-retry panosoft/elm_parent_child_updateThis runs the install process and stops right before installation.
grove bump --dry-runThis is EXTREMELY useful before releasing to perform all of the checks that bump normally does without actually preparing the package for a release.
grove install --npm-silent panosoft/elm-cmd-retry panosoft/elm_parent_child_updateThe --npm-silent option will NOT display any output during the NPM install.
grove install --npm-production panosoft/elm-cmd-retry panosoft/elm_parent_child_updateThe --npm-production option passes -production flag to NPM during its install operation.
grove uninstall --npm-production panosoft/elm-websocket-serverThe --npm-production option passes -production flag to NPM during its uninstall operation.
In order to work within the confines of the Elm compiler while still supporting multiple sources, Grove stores the source locations of packages in elm-package.json in a key called dependency-sources. This makes migration from elm-github-install easier.
Unfortunately, the Elm compiler dictates that the repository key MUST contain github.com even for repositories that are stored on elsewhere. It is important that your username or group, and repo names are correct but, for now, we have to pretend that the repository is on Github.
There are 2 instances where package.json is needed.
- Any Elm Package (including Apps) that has Native code that uses
requireto load an external Node library, i.e. not core modules, e.g.fs. - An Application that depends on an Elm Package that meets criteria #1 above.
Elm Packages that depend on Elm Packages that meet criteria #1 but are not Elm Apps do NOT need a package.json.
Normally, when you write Javascript code in Node, require statements will look for a node_modules directory under the directory where the module, which is doing the require, resides. If nothing is found, it will look to that module's parent directory for a node_modules directory. This continues all the way up the chain until the library is found or the root directory is encountered.
NPM version 2 used to create a tree of node_modules but since NPM version 3, all libraries are placed at the root level. The only exception is when two libraries require different versions of the same library. At that point, NPM v3 falls back to NPM v2's behavior and places the conflicting library underneath the module, which depends on it, in its own node_modules.
Since Elm is compiled, there is no sense of dynamic loading of modules. So all dependencies must resolve without conflicts, i.e. if Elm Package X uses Elm Package Z version 3 and Elm Package Y uses Elm Package Z version 4, then there's a conflict and there's no way to build you program without first resolving the conflict. This is why Grove checks for this during an install command.
To support NPM during an install command, Grove adds all dependent Elm packages that have a package.json to your program's package.json as an NPM dependency. Then Grove runs an npm install which follows the aforementioned behavior.
Things become problematic because the Elm compiler hoists the Native Code into the final Javascript at the root directory. But NPM installed the dependent packages's Javascript libraries, not at the root, but far below that. So when the Native Code from those dependent packages executes a require function, Node will start looking for that library at the root.
This isn't a problem if there are no conflicts among all NPM libraries. But it only takes one library conflict to cause problems.
So to solve this, Grove rewrites any requires that are loading a conflicting library.
Imagine the following example:
Your Elm Program (root)
|
+--- node_modules
|
+--- @gitUser
| |
| +--- elm-pack-a (Elm Package A)
| |
| +--- elm-pack-b (Elm Package B)
| |
| +--- node_modules
| |
| +--- libx (Library X v2)
|
+--- libx (Library X v3)
Native code in Elm Package A and Elm Package B will be hoisted to Your Elm Program directory by the compiler. So the require statements in those packages will load Javascript libraries from Your Elm Program/node_modules which is fine for Elm Package A since version 3 of Library X happens to be there.
But that behavior is a real problem for Elm Package B since it needs version 2 of Library X which NPM put under Elm Package B/node_modules.
To remedy this, Grove targets all require statements in all Javascript files of Elm Package B that load Library X and rewrites the require statement.
For example, the following code in Elm Package B:
const libx = require('libx');gets rewritten to:
const libx = require('./node_modules/@gitUser/elm-pack-b/node_modules/libx');Now version 2 of Library X will be loaded for Elm Package B.
Since local packages are linked to actual source code, Code Rewriting cannot be performed otherwise Grove would be modifying original source code. Grove will exit with an error if this is the case.
If you want to resolve the require problem yourself through some post process, e.g. via Webpack, or some other process, then you can disable this default behavior by specifying the --no-rewrite option on the install or uninstall command line.
I suspect Webpack will suffer from the same problems as Node, but this option was added for maximum flexibility.
- Not tested on Windows - even though the code tries to be file path agnostic, I suspect that there are places that have been missed.
- Windows SSH not tested (expects
os.homedir()to contain.ssh/id_rsaand.ssh/id_rsa.pub)

