Skip to content

Engine: Introduce SubtreeCompiler to compile part of a template - #2241

Merged
marcoroth merged 1 commit into
engine-dynamics-compilerfrom
engine-subtree-compiler
Aug 15, 2026
Merged

marcoroth merged 1 commit into
engine-dynamics-compilerfrom
engine-subtree-compiler

Conversation

@marcoroth

@marcoroth marcoroth commented Aug 15, 2026 •

Copy link
Copy Markdown
Owner

This pull request introduces Herb::Engine::SubtreeCompiler, which compiles a template into Ruby that renders one part of it instead of all of it.

Re-rendering a region means running the template again and keeping only the piece that changed. The piece cannot be compiled on its own, because what it renders depends on everything the template did before reaching it.

require "herb/engine/subtree_compiler"

source = Herb::Engine::SubtreeCompiler.new(template, node_path: [4]).src
view.instance_eval(source)
#=> "<ul><li>a-assigned</li><li>b-assigned</li></ul>"

Everything runs, only the target is kept

A local assigned earlier, a helper called for its side effect, the loop the target sits inside, all of it has to run for the target to render the same as it would have in a full render. So nothing is skipped.

<% total = track("assigned") %>

<div><%= track(@title) %></div>

<ul><li><%= total %></li></ul>

Asking for the <ul> returns <ul><li>assigned</li></ul>. The assignment above it ran, both tracked calls ran, and the <div> rendered into a buffer that is thrown away.

What changes is where the output goes. The engine writes every append through one buffer variable, so the target's output is collected by pointing that variable at the buffer being returned while the target renders and at a sink the rest of the time. Escaping, blocks, and control flow then need no special handling, because none of them know which buffer they are writing to. That is also why the escaping is right without any work, since the target is going back to the place it was cut from.

Pruning the work that only fed discarded output is a separate question, and answering it needs to know which expressions the target actually depends on. Running everything is the answer that is correct without that analysis.

Control flow around the target

A target inside a loop renders once per iteration, and a target inside a branch that did not run renders nothing.

<% @names.each do |name| %>
  <li><%= name %></li>
<% end %>

Asking for the <li> with two items returns <li>a</li><li>b</li>, because the loop it sits in is part of what has to run.

Addressing

The target is named by node_path, the same path SlotVisitor records for a slot in #1986 and Herb::ActionView::TemplateDependencies reports for a node, so a caller holding one from either has one that works here. An empty path is the whole document, which is tested for output identity against Herb::Engine itself. A path that leads nowhere raises TargetNotFound instead of compiling a template that silently renders nothing.

@github-actions github-actions Bot added ruby Ruby source for the gem and its libraries rbs RBS type signatures in sig/ engine Herb engine and Rails template compilation rubygem The herb RubyGem and its packaging labels Aug 15, 2026
@marcoroth marcoroth changed the title Engine: Introduce SubtreeCompiler to render one part of a template Engine: Introduce SubtreeCompiler to compile part of a template Aug 15, 2026
@nx-cloud

nx-cloud Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 931f40e

Command Status Duration Result
nx run-many -t test --all --parallel --exclude=... ✅ Succeeded 3m 23s View ↗
nx run-many -t build --all --exclude=herb-langu... ✅ Succeeded 2m 32s View ↗
nx build @herb-tools/tailwind-class-sorter ✅ Succeeded 1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-15 15:34:47 UTC

@marcoroth marcoroth added the reactivity Reactive ERB templates: diff and re-render only what changed label Aug 15, 2026
@marcoroth
marcoroth marked this pull request as ready for review August 15, 2026 15:34
@github-actions

github-actions Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

🌿 Interactive Playground and Documentation Preview

A preview deployment has been built for this pull request. Try out the changes live in the interactive playground:


🌱 Grown from commit 931f40e


✅ Preview deployment has been cleaned up.

@pkg-pr-new

pkg-pr-new Bot commented Aug 15, 2026

Copy link
Copy Markdown
npx https://pkg.pr.new/@herb-tools/formatter@2241
npx https://pkg.pr.new/@herb-tools/language-server@2241
npx https://pkg.pr.new/@herb-tools/linter@2241

commit: 931f40e

@marcoroth
marcoroth merged commit b404efb into main Aug 15, 2026
43 checks passed
@marcoroth
marcoroth deleted the engine-subtree-compiler branch August 15, 2026 15:37

This branch was successfully deployed

1 active deployment
herb-tools (Preview) — 931f40e4 Deployed Aug 15, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine Herb engine and Rails template compilation rbs RBS type signatures in sig/ reactivity Reactive ERB templates: diff and re-render only what changed ruby Ruby source for the gem and its libraries rubygem The herb RubyGem and its packaging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant