Add plugin creation guide

This commit is contained in:
Chris de Graaf 2017-08-16 00:34:37 -05:00
parent 188bfb9390
commit a073eb4492

184
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,184 @@
# Contributing to PkgTemplates
The best way to contribute to `PkgTemplates` is by adding new plugins.
Plugins are pretty simple. They're defined as subtypes to `Plugin`, in their
own file inside `src/plugins`. Let's create one, called `MyPlugin`, in
`src/plugins/myplugin.jl`:
```julia
@auto_hash_equals struct MyPlugin <: Plugin end
```
The `@auto_hash_equals` macro means we don't have to implement `==` or `hash`
ourselves ([ref](https://github.com/andrewcooke/AutoHashEquals.jl)).
All plugins need at least one attribute: `gitignore_files`. This is a
`Vector{AbstractString}`, of which each entry will be inserted in the
`.gitignore` of generated packages that use this plugin.
Maybe the service that `MyPlugin` is associated with creates a directory
called `secrets`, containing top secret data. In that case, `gitignore_files`
should contain that string:
```julia
@auto_hash_equals struct MyPlugin <: Plugin
gitignore_files::Vector{AbstractString}
function MyPlugin()
new(["/secrets"])
end
end
```
You can also add patterns like `*.key`, etc. to this array. Note that Windows
Git also recognizes `/` as a path separator in `.gitignore`, so there's no
pneed for `joinpath`.
Suppose that `MyPlugin` also has a configuration file at the root of the repo.
We're going to put a default `myplugin.yml` in `defaults`, but we also want
to let users supply their own, or choose to not use one at all:
```julia
@auto_hash_equals struct MyPlugin <: Plugin
gitignore_files::Vector{AbstractString}
config_file::Union{AbstractString, Void}
function MyPlugin(; config_file::Union{AbstractString, Void}="")
if config_file != nothing
if isempty(config_file)
config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
end
if !isfile(config_file)
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
end
end
new(["/secrets"], config_file)
end
end
```
Now to actually create this configuration file at package generation time,
we need a `gen_plugin` method. This method looks like this:
```julia
function gen_plugin(plugin::MyPlugin, template::Template, pkg_name::AbstractString)
if plugin.config_file == nothing
return String[]
end
text = substitute(readstring(plugin.config_file), pkg_name, template)
gen_file(joinpath(template.temp_dir, pkg_name, ".myplugin.yml"))
return [".myplugin.yml"]
end
```
There are a few things to note here:
* We use the `substitute` function on the config file's text.
* More on that [later](#template-substitution).
* We use the `gen_file` function to create the file.
* It takes two arguments: the path to the file to be generated,
and the text to be written.
* We place our file in `template.temp_dir`.
* `template.temp_dir` is where all file generation takes place, files are
only moved to their final location at the end of package generation
to avoid leftovers in the case of an error.
* We return an array containing at most the name of our generated file.
* This array should contain all root-level files or directories that were
created. If we created `myplugin/foo` and `myplugin/bar`, we'd only need
to return `["myplugin/"]`. If nothing is created, then we return an
empty array.
We've got the essentials now, but perhaps `MyPlugin` has a web interface
that we want to access from the repo's homepage. We'll do this by adding a
badge to the README:
```julia
function badges(_::MyPlugin, user::AbstractString, pkg_name::AbstractString)
return [
"[![MyPlugin](https://myplugin.com/badges/$user/$pkg_name.jl)](https://myplugin.com/$user/$pkg_name.jl)"
]
end
```
This method should return an array of Markdown-formatted strings that display
badges and link to somewhere relevant. Note that a plugin can have any number
of badges. The Markdown syntax is as follows:
```
[![Hover Text](https://badge-image.url)](https://link.url)
```
Badges for just about everything can be found at
[Shields.io](https://shields.io/).
We're not done yet though, we need to add the plugin type to the list of
badge-enabled plugins. We want `MyPlugin`'s badge to be displayed on the far
right side, so we're going to add `MyPlugin` to the end of `BADGE_ORDER` in
`src/PkgTemplates.jl`.
```julia
const BADGE_ORDER = [GitHubPages, TravisCI, AppVeyor, CodeCov, MyPlugin]
```
And we're done! We've just created a nifty new plugin.
***
### Template Substitution
Since plugin configuration files are often specific to the package they belong
to, we might want to replace some placeholder values in our plugin's config
file. We can do this by following
[Mustache.jl](https://github.com/jverzani/Mustache.jl)'s rules. Some
replacements are defined by `PkgTemplates`:
* `{{PKGNAME}}` is replaced by `pkg_name`.
* `{{VERSION}}` is replaced by `$major.$minor` corresponding to
`template.julia_version`.
Some conditional replacements are also defined:
* `{{DOCUMENTER}}Documenter{{/DOCUMENTER}}`
* "Documenter" only appears in the rendered text if the template contains
a [`Documenter`](src/plugins/documenter.jl) subtype.
* `{{CODECOV}}CodeCov{{/CODECOV}}`
* "CodeCov" only appears in the rendered text if the template contains
the [`CodeCov`](src/plugins/codecov.jl) plugin.
* `{{#AFTER}}After{{/AFTER}}`
* "After" only appears in the rendered text if something needs to happen
**after** CI testing occurs. As of right now, this is true when either of
the above two conditions are true.
We can also specify our own replacements by passing a dictionary to
`substitute`:
```julia
view = Dict("KEY" => "VAL", "HEADS" => 2rand() > 1)
text = """
{{KEY}}
{{PKGNAME}}
{{#HEADS}}Heads{{/HEADS}}
"""
substituted = substitute(text, "MyPkg", template; view=view)
```
This will return `"VAL\nMyPkg\nHeads\n"` if `2rand() > 1` was true,
`"VAL\nMyPkg\n\n"` otherwise.
Note the double newline in the second outcome; `Mustache` has a bug with
conditionals that inserts extra newlines (more detail
[here](https://github.com/jverzani/Mustache.jl/issues/47)). We can get around
this by writing ugly template files, like so:
```
{{KEY}}
{{PKGNAME}}{{#HEADS}}
Heads{{/HEADS}}
```
The resulting string will end with a single newline regardless of the value
of `view["HEADS"]`
Also note that conditionals without a corresponding key in `view` won't error,
but will simply be evaluated as false.