Plugin Development

Plugin Development

The best and easiest way to contribute to PkgTemplates is to write new plugins.

There are two types of plugins: GenericPlugins and CustomPlugins.

Generic Plugins

Generic plugins are plugins that add any number of patterns to the generated package's .gitignore, and have at most one associated file to generate.

Attributes

  • gitignore::Vector{AbstractString}: Array of patterns to be added to the .gitignore of generated packages that use this plugin.

  • src::Nullable{AbstractString}: Path to the file that will be copied into the generated package repository. If set to nothing, no file will be generated. When this defaults to an empty string, there should be a default file in defaults that will be copied. That file's name is usually the same as the plugin's name, except in all lowercase and with the .yml extension. If this is not the case, an interactive method needs to be implemented to call interactive(; file="file.ext").

  • dest::AbstractString: Path to the generated file, relative to the root of the generated package repository.

  • badges::Vector{Badge}: Array of Badges containing information used to create Markdown-formatted badges from the plugin. Entries will be run through substitute, so they may contain placeholder values.

  • view::Dict{String, Any}: Additional substitutions to make in both the plugin's badges and its associated file. See substitute for details.

Example

@auto_hash_equals struct MyPlugin <: GenericPlugin
    gitignore::Vector{AbstractString}
    src::Nullable{AbstractString}
    dest::AbstractString
    badges::Vector{Badge}
    view::Dict{String, Any}

    function MyPlugin(; config_file::Union{AbstractString, Void}="")
        if config_file != nothing
            if isempty(config_file)
                config_file = joinpath(DEFAULTS_DIR, "my-plugin.toml")
            elseif !isfile(config_file)
                throw(ArgumentError(
                    "File $(abspath(config_file)) does not exist"
                ))
            end
        end
        new(
            ["*.mgp"],
            config_file,
            ".myplugin.yml",
            [
                Badge(
                    "My Plugin",
                    "https://myplugin.com/badge-{{YEAR}}.png",
                    "https://myplugin.com/{{USER}}/{{PKGNAME}}.jl",
                ),
            ],
            Dict{String, Any}("YEAR" => Dates.year(Dates.today())),
        )
    end
end

interactive(plugin_type::Type{MyPlugin}) = interactive(plugin_type; file="my-plugin.toml")

The above plugin ignores files ending with .mgp, copies defaults/my-plugin.toml by default, and creates a badge that links to the project on its own site, using the default substitutions with one addition: {{YEAR}} => Dates.year(Dates.today()). Since the default config template file doesn't follow the generic naming convention, we added another interactive method to correct the assumed filename.

source

Custom Plugins

Custom plugins are plugins whose behaviour does not follow the GenericPlugin pattern. They can implement gen_plugin, badges, and interactive in any way they choose.

Attributes

  • gitignore::Vector{AbstractString}: Array of patterns to be added to the .gitignore of generated packages that use this plugin.

Example

@auto_hash_equals struct MyPlugin <: CustomPlugin
    gitignore::Vector{AbstractString}
    lucky::Bool

    MyPlugin() = new([], rand() > 0.8)

    function gen_plugin(
        plugin::MyPlugin,
        template::Template,
        dir::AbstractString,
        pkg_name::AbstractString
    )
        if plugin.lucky
            text = substitute(
                "You got lucky with {{PKGNAME}}, {{USER}}!",
                template,
            )
            gen_file(joinpath(dir, pkg_name, ".myplugin.yml"), text)
        else
            println("Maybe next time.")
        end
    end

    function badges(
        plugin::MyPlugin,
        user::AbstractString,
        pkg_name::AbstractString,
    )
        if plugin.lucky
            return [
                format(Badge(
                    "You got lucky!",
                    "https://myplugin.com/badge.png",
                    "https://myplugin.com/$user/$pkg_name.jl",
                )),
            ]
        else
            return String[]
        end
    end
end

interactive(plugin_type::Type{MyPlugin}) = MyPlugin()

This plugin doesn't do much, but it demonstrates how gen_plugin, badges and interactive can be implemented using substitute, gen_file, Badge, and format.

Defining Template Files

Often, the contents of the config file that your plugin generates depends on variables like the package name, the user's username, etc. Template files (which are stored in defaults) can use here's syntax to define replacements.

Note: Due to a bug in Mustache, conditionals can insert undesired newlines (more detail here).

source

CustomPlugin Required Methods

gen_plugin

gen_plugin(
    plugin::Plugin,
    template::Template,
    dir::AbstractString,
    pkg_name::AbstractString
) -> Vector{String}

Generate any files associated with a plugin.

Arguments

  • plugin::Plugin: Plugin whose files are being generated.

  • template::Template: Template configuration.

  • dir::AbstractString: The directory in which the files will be generated. Note that this will be joined to pkg_name.

  • pkg_name::AbstractString: Name of the package.

Returns an array of generated file/directory names.

source
interactive(
    plugin_type::Type{P <: Plugin};
    file::Union{AbstractString, Void}="",
) -> Plugin

Interactively create a plugin of type plugin_type, where file is the plugin type's default config template with a non-standard name (for MyPlugin, this is anything but "myplugin.yml").

source

Note: interactive is not strictly required, however without it, your custom plugin will not be available when creating templates with interactive_template.

badges

PkgTemplates.badgesFunction.
badges(plugin::Plugin, user::AbstractString, pkg_name::AbstractString) -> Vector{String}

Generate Markdown badges for the plugin.

Arguments

  • plugin::Plugin: Plugin whose badges we are generating.

  • user::AbstractString: Username of the package creator.

  • pkg_name::AbstractString: Name of the package.

Returns an array of Markdown badges.

source

Helper Types/Functions

gen_file

PkgTemplates.gen_fileFunction.
gen_file(file_path::AbstractString, text::AbstractString) -> Int

Create a new file containing some given text. Always ends the file with a newline.

Arguments

  • file::AbstractString: Path to the file to be created.

  • text::AbstractString: Text to write to the file.

Returns the number of bytes written to the file.

source

substitute

substitute(template::AbstractString, view::Dict{String, Any}) -> String

Replace placeholders in template with values in view via Mustache. template is not modified.

For information on how to structure template, see "Defining Template Files" section in Custom Plugins.

Note: Conditionals in template without a corresponding key in view won't error, but will simply be evaluated as false.

source
substitute(
    template::AbstractString,
    pkg_template::Template;
    view::Dict{String, Any}=Dict{String, Any}(),
) -> String

Replace placeholders in template, using some default replacements based on the pkg_template and additional ones in view. template is not modified.

source

Badge

Badge(hover::AbstractString, image::AbstractString, link::AbstractString) -> Badge

A Badge contains the data necessary to generate a Markdown badge.

Arguments

  • hover::AbstractString: Text to appear when the mouse is hovered over the badge.

  • image::AbstractString: URL to the image to display.

  • link::AbstractString: URL to go to upon clicking the badge.

source

format

PkgTemplates.formatFunction.
format(b::Badge)

Return badge's data formatted as a Markdown string.

source

version_floor

version_floor(v::VersionNumber=VERSION) -> String

Format the given Julia version.

Keyword arguments

  • v::VersionNumber=VERSION: Version to floor.

Returns "major.minor" for the most recent release version relative to v. For prereleases with v.minor == v.patch == 0, returns "major.minor-".

source