Grove
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.
Using Grove Safely
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).
Major features:
- 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
)
Roadmap:
- Elm 0.19 support (once released)
Install
Make sure you have the following:
- Elm version 0.18.x
- npm version 5.3.0
NPM 5.x.x issues
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.
Installing grove
cd ~
git clone https://github.com/panosoft/elm-grove
cd elm-grove
sudo npm link
Updating grove
cd ~/elm-grove
git pull
sudo npm link
Configure Grove For Safety
Global/Local Safety
You 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
.
SafeMode On
grove config --safe=on
This 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.
SafeMode Off
grove config --safe=off
This 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.
SafeMode None
grove config --safe=none
This will produce no messages regarding package statuses.
Remove SafeMode Configuration
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).
Configuration Files
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.
Simple Example 1
Assuming you're starting a new project that needs the following:
elm-lang/core
grove init
grove install
The 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
.
Simple Example 2
Assuming you're starting a new project that needs the following:
elm-lang/core
elm-lang/html
panosoft/elm-utils
group/repo
(from Gitlab atgitlab.private.com
)
grove init
grove install elm-lang/html panosoft/elm-utils [email protected]:group/repo
The 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 Problems Grove Solves
Native packages are forbidden
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.
Grove
supports packages that have native code.
NPM isn't supported
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)
Grove
updates your program'spackage.json
with packages that have native code and then runsnpm install
andnpm uninstall
automatically. It also removes the need for semantic equivalence.
Developing dependent packages simultaneously is not supported
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.
Grove
can automatically create links to local repositories.
Difficult to check for outdated dependencies
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).
Package sources are limited
Installing from locations other than Github is not possible with the standard package manager.
Grove
will accept fully qualified package names allowing it support any Git server.
Usage
General Usage
The general command-line format is:
grove <command> [options]
Get help
This command will print the command-line help.
grove help
Check program's version
This command will print the version of Grove
and the version of Elm that's supported.
grove version
Basic Usage
Initialize a package
grove init
elm-package.json
This 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/core
is the one and only dependency, to add more usegrove install
Elm Version
- the current version supported to the next version
package.json
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 name
promptVersion
- set to 0.0.0License
- set based on theLicense
prompt
Install packages
elm-lang/html
Installing grove install elm-lang/html
Installing multiple packages
grove install elm-node/core panosoft/elm_parent_child_update
myGroup/repo
from Gitlab at gitlab.mydomain.com
Installing grove install [email protected]:myGroup/repo
Details
This 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.com
orgithub.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>.git
Uninstalling packages
Uninstalling packages removes the packages from Elm Packages and then it performs an Install minus the Npm install step. Then Npm Uninstall is performed.
elm-community/list-extra
Uninstalling grove uninstall elm-community/list-extra
Uninstalling multiple packages
grove uninstall panosoft/elm-postgres elm-community/result-extra
myGroup/repo
from Gitlab at gitlab.mydomain.com
Uninstalling grove uninstall myGroup/repo
Details
This 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.com
orgithub.com
[.git]
- optional (may be required by some Git servers)
While the other formats are supported, it's easiest to just use the <repo>
.
Advanced Usage
Creating documentation
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.
Configuring Grove for AUTOMATIC documentation creation
During a bump
command, you can configure Grove to automatically generate documentation.
grove config --local --docs=on
Typically, 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=
.
Using Grove to MANUALLY generate documentation
This gives you a chance to debug your documentation before you release your package.
grove docs
Installing a local package
grove install --link [email protected]:myGroup/elm-thing
When 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.
grove-links.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
Releasing a package
There are 3 types of releases:
- Normal -
HEAD
is based on the most recent release - Rebased -
HEAD
is based on a release that is not the most recent of it's major version - Legacy -
HEAD
is based on an oldermajor
release number
For details on release scenarios see Understanding Release Scenarios.
Pre-release
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:
Release
Releasing a package is a 2-step process.
- Bump version using
grove bump
command - Pushing repo with
git push && git push --tags
(Without thegit push --tags
command, the latest version of the package will not be recognized byGrove
.)
Bump version (Step 1)
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
,minor
andpatch
are 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 bump
This 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.json
are the latest versions (override with--allow-old-dependencies
) - no uncommitted changes (override with
--allow-uncommitted
)
Push repository (Step 2)
Next, you must MANUALLY push the repo AND tags via:
git push && git push --tags
Without the git push --tags
command, the latest version of the package will not be recognized by Grove
.
Version Determination
There are 2 types of Packages:
- Library, e.g.
panosoft/elm-utils
- Application, e.g.
panosoft/elm-grove
Libraries
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
Applications do NOT have public interfaces, so you must provide the version type via bump
options --major
, --minor
, or --patch
.
False positives
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.
Understanding Release Scenarios
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 HEAD
s of your repo:
Normal Release (Typical case)
A Normal
release occurs when HEAD
is based on the most recent release, in this example, that is 3.0.1
.
Legacy Release (Support multiple major versions)
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.
Rebased Release (Rare case to revert back to an old codebase)
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)
HEAD
is based off of an older release that shares the samemajor
version as the latest release, in this example, that could only be3.0.0
. - (Rebased Legacy)
HEAD
is based off an older release of aLegacy
release, e.g.2.0.0
,1.2.0
and1.0.1
would be Rebased Legacy releases. Note that2.1.0
and1.2.0
would NOT be aRebased
release since they are the latest releases of thosemajor
versions.
Additional Usage
Dry runs
Dry run Install
grove install --dry-run panosoft/elm-cmd-retry panosoft/elm_parent_child_update
This runs the install process and stops right before installation.
Dry run Bump
grove bump --dry-run
This is EXTREMELY useful before releasing to perform all of the checks that bump normally does without actually preparing the package for a release.
Controlling NPM
Silencing NPM's Output
grove install --npm-silent panosoft/elm-cmd-retry panosoft/elm_parent_child_update
The --npm-silent
option will NOT display any output during the NPM install.
NPM Production Install
grove install --npm-production panosoft/elm-cmd-retry panosoft/elm_parent_child_update
The --npm-production
option passes -production
flag to NPM during its install operation.
NPM Production Uninstall
grove uninstall --npm-production panosoft/elm-websocket-server
The --npm-production
option passes -production
flag to NPM during its uninstall operation.
elm-package.json
Changes to Support for non-Github servers
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.
repository
key
Caveat with the 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.
package.json
needed?
When is There are 2 instances where package.json
is needed.
- Any Elm Package (including Apps) that has Native code that uses
require
to 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
.
Code Rewriting
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
.
Conflicts in local packages
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.
No rewrite
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.
Known issues
- 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_rsa
and.ssh/id_rsa.pub
)