crates/minijinja-utils/README.md
Static analysis utilities for MiniJinja templates.
This crate analyzes MiniJinja templates to extract all template dependencies by parsing the template syntax and walking the AST. It identifies include, import, from, and extends statements and determines whether template loading can be statically resolved.
Template names that can be determined at parse time:
{% include 'header.html' %}
{% extends 'base.html' %}
{% import 'macros.html' as m %}
{% include ['first.html', 'second.html'] %}
{% include 'a.html' if condition else 'b.html' %}
{% include 'optional.html' if condition %}
Template names that depend on runtime values:
{% include template_var %}
{% include get_template() %}
pub fn collect_all_template_paths(
env: &Environment<'_>,
template_name: &str,
) -> Result<HashSet<PathBuf>, AnalysisError>
Recursively collects all template paths starting from a root template. Returns an error if any dynamic loads are found or parsing fails.
AnalysisErrorpub enum AnalysisError {
ParseError(minijinja::Error),
DynamicLoadsFound(Vec<DynamicLoadLocation>),
}
ParseError: Template has invalid MiniJinja syntaxDynamicLoadsFound: One or more templates contain dynamic loads that cannot be statically resolvedDynamicLoadLocationProvides detailed location information for dynamic loads:
template_name: Which template contains the dynamic loadline, column: 1-indexed position in the templatespan: Byte offsets of the expressionsource_quote: Extracted source showing the problemreason: Why it's dynamic (e.g., "variable", "conditional without else")load_kind: Type of statement (include, import, etc.)LoadKindpub enum LoadKind {
Include { ignore_missing: bool },
Import,
FromImport,
Extends,
}
Identifies which type of template loading statement was encountered.
use minijinja::Environment;
use minijinja_utils::collect_all_template_paths;
let mut env = Environment::new();
env.add_template("main.html", "{% include 'header.html' %}Content").unwrap();
env.add_template("header.html", "Header").unwrap();
// Collect all template dependencies
let paths = collect_all_template_paths(&env, "main.html").unwrap();
assert_eq!(paths.len(), 2); // main.html and header.html
use minijinja::Environment;
use minijinja_utils::{collect_all_template_paths, AnalysisError};
let mut env = Environment::new();
env.add_template("dynamic.html", "{% include template_var %}").unwrap();
match collect_all_template_paths(&env, "dynamic.html") {
Err(AnalysisError::DynamicLoadsFound(locations)) => {
for loc in locations {
eprintln!("{}:{}:{}: {} - {}",
loc.template_name, loc.line, loc.column,
loc.load_kind, loc.reason);
}
}
Err(AnalysisError::ParseError(e)) => {
eprintln!("Parse error: {}", e);
}
Ok(paths) => {
println!("Found {} templates", paths.len());
}
}
{% include 'template.html' %}{% import 'macros.html' as m %}{% from 'macros.html' import button %}{% extends 'base.html' %}{% include ['a.html', 'b.html'] %}{% include 'a.html' if x else 'b.html' %}unstable_machinery feature which may change between versionsWhen a template uses a conditional include without an else clause, the static template name is still extracted:
{% include 'optional.html' if show_feature %}
This will:
'optional.html' as a dependency'optional.html' exists at analysis timeThis behavior enables static validation of all template paths while supporting conditional loading patterns. If the template name is dynamically computed (e.g., from a variable or function call), it will still be considered a dynamic load and produce an error.
This crate uses MiniJinja's unstable_machinery feature to access the AST. This API is marked unstable and may change between MiniJinja versions.