Skip to content
LaravelBlog

livewire/livewire: Preserve islands after nested Blade directives

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Categories: fix (id 3), upgrade notes (id 11)

Livewire 4's islands compiler had a narrow but production-fatal edge case: when an @island block appeared after ordinary Blade directives whose expressions shared a nested-call prefix — say, two @if directives where the second extends the first — the island silently lost its generated token during compilation, and every request to that page failed with a 500. PR #10626 fixes this by temporarily setting aside non-island directives during Livewire's island-only compilation pass and restoring them byte-for-byte afterward. If you're upgrading from v3.8.10, note that islands are a v4-only feature, so this entire bug class is new territory for you — and with this fix, the failing pattern compiles correctly with zero changes to your templates.

What broke

The report (#10624) involved a view as plain as this:

@if (auth()->check())
    first
@endif

@if (auth()->check() && request()->isMethod('get'))
    second
@endif

@island
    <div>Count: {{ $count }}</div>
@endisland

Nothing exotic — supported syntax on every line. Yet the page deterministically returned a 500 with:

IslandCompiler::getCachedPathFromToken(): Argument #1 ($token)
must be of type string, null given

The island's token was dropped while the compiled view was built, so rendering failed on every request, not intermittently.

Why it happened

Livewire's island pipeline begins with an island-only pass over the template, running Blade's statement compiler in a restricted mode: only island-related directives are processed. Before this fix, every other directive was returned unchanged.

That pass-through turned out to be the problem. Laravel's BladeCompiler::compileStatements() repairs an initially unbalanced regex match by searching again from the start of the template. Because the earlier @if (auth()->check()) was still sitting in the source, Blade could reconstruct the later, longer directive from that earlier text, advance its forward-only replacement offset past the island, and silently drop the tokenized replacement. Livewire had already fixed this same position-vs-value lookup class in a separate compiler path (#9921); this was the same disease in a different organ.

The fix: set aside, then restore

Instead of replacing Blade's compiler with a dedicated island parser (a much larger project) or asking you to restructure templates, the PR builds a small compatibility bridge:

  1. During the island-only pass, every non-island directive is replaced with a placeholder like [LIVEWIRE_DIRECTIVE:0] and stored.
  2. Islands are processed with their generated tokens, now immune to Blade's offset repair because no matching prefix remains in the source.
  3. Before islands are extracted, placeholders are restored byte-for-byte via a single strtr, so Blade's real compilation pass sees the original directives exactly as authored.

The placeholder prefix is collision-safe: if your view genuinely contains text like [LIVEWIRE_DIRECTIVE:0], the compiler extends its prefix with underscores until it no longer collides, and your literal content still renders untouched. Custom directives (a registered @islandGreeting, for example) still compile normally, and escaped directives such as @@if are preserved until Blade's own pass renders them as literal @if.

Conceptually:

@if (auth()->check())            →  [LIVEWIRE_DIRECTIVE:0]     (set aside)
@island … @endisland             →  island + generated token   (processed)
[LIVEWIRE_DIRECTIVE:0]           →  @if (auth()->check())      (restored)

What this means for your upgrade

  • No action required. No public API changed and no documentation changed — this restores behavior that was already documented.
  • Workarounds can go. If you hit #10624 and hoisted nested calls into variables or component properties to dodge the 500, you can return to the natural, direct syntax shown above.
  • Clear compiled views once. If a broken view was already cached in production, run php artisan view:clear after deploying so the fixed compiler rebuilds it cleanly.
  • Coverage is solid. New regression tests cover the reported page plus marker-like authored content, custom directives, and escaped directives. The full unit suite (1,408 tests / 4,017 assertions) and the islands browser suite (35 tests / 192 assertions) pass.

Known limits, honestly stated

One adjacent path is not covered here. Because the island pre-pass calls compileStatements() before Blade stores raw/verbatim blocks, raw content containing an unbalanced ( or a literal @island can still produce the same missing-token symptom through a different route. That is deliberately tracked as a separate, targeted fix rather than folded into this one. Longer term, the real cure is upstream: Laravel's position lookup should be repaired so any source-preserving compiler client is safe, and Livewire can remove this bridge once every supported Laravel line includes that fix.

Takeaways

  • Islands don't exist in v3, so if you're migrating from v3.8.10, adopt them on a build that includes this fix.
  • The trigger is subtle: directives before an island sharing a nested-call prefix, like stacked @if expressions.
  • The failure is loud (a deterministic 500 with a null-token error), but the fix is invisible: no template changes, no API changes, and no placeholders ever leak into output.
  • Raw/verbatim content with unbalanced parentheses or a literal @island remains a known, separate edge case for now.

Sources

More from this release

Related Articles