6.3 KiB
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
:
@auto_hash_equals struct MyPlugin <: Plugin end
The @auto_hash_equals
macro means we don't have to implement ==
or hash
ourselves (ref).
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:
@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
need 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:
@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:
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.
- 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
andmyplugin/bar
, we'd only need to return["myplugin/"]
. If nothing is created, then we return an empty array.
- This array should contain all root-level files or directories that were
created. If we created
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:
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.
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
.
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's rules. Some
replacements are defined by PkgTemplates
:
{{PKGNAME}}
is replaced bypkg_name
.{{VERSION}}
is replaced by$major.$minor
corresponding totemplate.julia_version
.
Some conditional replacements are also defined:
{{DOCUMENTER}}Documenter{{/DOCUMENTER}}
- "Documenter" only appears in the rendered text if the template contains
a
Documenter
subtype.
- "Documenter" only appears in the rendered text if the template contains
a
{{CODECOV}}CodeCov{{/CODECOV}}
- "CodeCov" only appears in the rendered text if the template contains
the
CodeCov
plugin.
- "CodeCov" only appears in the rendered text if the template contains
the
{{#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
:
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). 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.