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