Skip to content

Plugin asset extension point

aadrian edited this page Sep 25, 2026 · 1 revision

Page assets for plugins

Current options

  • javaScripts (explorer plugin #66): matches a regex against getRequestURI, which includes the context path. The pattern cannot distinguish repository pages from other x/y URLs (/root/signin vs. /signin, /gist/:user/:id, /:user/_edit, ...). One slot, at the end of <body>; the plugin writes the <link>/<script> markup and the URLs itself.
  • repositoryHeaders (explorer plugin #73): rendered from menu.scala.html, so only on pages with the repository sidebar. The plugin still writes the markup and URLs; the CSS ends up in the body, the script mid-page. Repository pages only.

Proposal: assets declared by the plugin, rendered by GitBucket

The plugin declares which assets it needs, per page. GitBucket, not the plugin, produces the tags:

  • builds the URL (context.path/baseUrl + /plugin-assets/ + path), optionally with a version or digest,
  • puts CSS in <head> (after gitbucket.css) and JS before </body>, with defer,
  • de-duplicates an asset requested more than once.

Bundling, minification and hashing stay in the plugin's own build (npm/Vite/webpack). Only the inclusion is handled by GitBucket; resolving a hashed file name through a build manifest could be added to the URL step later.

sealed trait AssetKind
object AssetKind { case object Css extends AssetKind; case object Js extends AssetKind }

// path is relative to /plugin-assets/, e.g. "explorer/bundle.js"
case class PluginAsset(path: String, kind: AssetKind, defer: Boolean = true)

// Plugin
def pageAssets(registry: PluginRegistry, context: ServletContext, settings: SystemSettings)
  : Seq[(Option[RepositoryInfo], Context) => Seq[PluginAsset]] = Nil
File Change
plugin/Plugin.scala val + def pair, registered in initialize
plugin/PluginRegistry.scala queue, addPageAssets, getPageAssets
main.scala.html CSS loop in <head> after gitbucket.css; JS loop before </body>
  • main already takes repository: Option[RepositoryInfo]. Of the templates that render the repository sidebar, 33 pass Some(repository) to html.main and 5 do not (issues/labels/list, issues/milestones/{edit,list,milestone}, issues/priorities/list).
  • Additive, empty default. A plugin using it requires a GitBucket version that has it; older versions ignore it.

Reference: Grails asset-pipeline

asset-pipeline separates build-time processing (bundling, minification, digests) from render-time tags.

  • A Grails plugin ships assets under grails-app/assets/, in the same URL space as the application's; they are packaged into the plugin jar and found on the classpath (META-INF/assets). An application file with the same path overrides the plugin's (organization).
  • The application includes assets by logical name (//= require in a manifest, or <asset:stylesheet> / <asset:javascript> in a view or layout). Inclusion is decided by the application, not by the plugin.
  • <asset:script> blocks and asset-defer="true" are collected while rendering and emitted at <asset:deferredScripts/> (taglibs).
  • Difference: in GitBucket the plugin adds its own assets to pages, so the declaration is on the plugin side and the layout only places it.

Independent one-liner: getJavaScript(context.currentPath) instead of getRequestURI removes the context path from all existing regexes.

Open: page kind beyond Option[RepositoryInfo]; ordering between plugins; deprecating javaScripts.