Add plugin creation guide
This commit is contained in:
parent
188bfb9390
commit
a073eb4492
184
CONTRIBUTING.md
Normal file
184
CONTRIBUTING.md
Normal 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 [
|
||||
"[](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:
|
||||
|
||||
```
|
||||
[](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.
|
Loading…
Reference in New Issue
Block a user