BarefootJS

 view release on metacpan or  search on metacpan

lib/BarefootJS/DevReload.pm  view on Meta::CPAN


=head1 NAME

BarefootJS::DevReload - Framework-agnostic dev-only browser auto-reload for BarefootJS apps

=head1 SYNOPSIS

    # Plain PSGI / Plack (e.g. the Text::Xslate backend)
    use BarefootJS::DevReload;

    # Mount the SSE endpoint (dev only):
    my $reload = BarefootJS::DevReload->to_app(dist_dir => 'dist');
    # ... route '/_bf/reload' => $reload ...

    # And emit the browser snippet before </body> in your layout:
    BarefootJS::DevReload->snippet('/_bf/reload');

=head1 DESCRIPTION

Companion to C<barefoot build --watch> in C<@barefootjs/cli>. The CLI drops
C<< <dist>/.dev/build-id >> after every successful rebuild that changed output;
a browser snippet subscribes to an SSE endpoint that emits C<< event: reload >>
when that file changes, so an editor save triggers an automatic reload.

This module holds the engine-agnostic pieces — the browser snippet, the
build-id reader, and a ready-made PSGI streaming app for the SSE endpoint — so
both L<Mojolicious::Plugin::BarefootJS::DevReload> (Mojo streaming) and plain
PSGI/Plack hosts (the Text::Xslate backend) share one implementation.

=cut

# Sentinel path contract with @barefootjs/cli (DEV_SENTINEL_SUBDIR /
# DEV_SENTINEL_FILENAME in packages/cli/src/lib/build.ts). Duplicated so this
# package avoids a runtime dep on the CLI — keep in sync with the CLI.
my $DEV_SUBDIR    = '.dev';
my $BUILD_ID_FILE = 'build-id';

lib/BarefootJS/DevReload.pm  view on Meta::CPAN

    close $fh;
    $content //= '';
    $content =~ s/^\s+|\s+$//g;
    return $content;
}

# The browser snippet: a small IIFE — EventSource subscriber + scrollY
# preservation across reloads. Idempotent across duplicate mounts (the
# window.__bfDevReload guard). Returns a plain HTML string; callers mark it raw
# for their template engine.
sub snippet ($class, $endpoint) {
    my $ep = _js_str($endpoint);
    my $sk = _js_str($SCROLL_STORAGE_KEY);
    return qq{<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;try{var s=sessionStorage.getItem($sk);if(s){sessionStorage.removeItem($sk);var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};if(docu...
}

# A ready-made PSGI app for the SSE endpoint. Streams `event: reload` whenever
# <dist>/.dev/build-id changes, with `: hb` heartbeats in between.
#
# Implemented with the PSGI streaming interface and a blocking poll loop, so it
# holds one worker per open connection for the connection's lifetime — run it
# under a prefork PSGI server (Starman / Starlet) in dev, which is the natural
# choice for an app that also streams (e.g. an AI-chat SSE route). DevReload is
# automatically a no-op unless you mount it, and you should only mount it in
# development.
sub to_app ($class, %opts) {
    my $dist_dir      = $opts{dist_dir} // 'dist';

t/dev_reload.t  view on Meta::CPAN

use Test2::V0;
use File::Temp qw(tempdir);

use BarefootJS::DevReload;

# --- browser snippet ---------------------------------------------------------
my $snip = BarefootJS::DevReload->snippet('/_bf/reload');
like $snip, qr/new EventSource\("\/_bf\/reload"\)/, 'snippet wires EventSource to the endpoint';
like $snip, qr/window\.__bfDevReload/, 'snippet is idempotent across duplicate mounts';
like $snip, qr/location\.reload\(\)/, 'snippet reloads on the `reload` event';

# --- build-id sentinel -------------------------------------------------------
my $dir  = tempdir(CLEANUP => 1);
my $path = BarefootJS::DevReload->build_id_path($dir);
like $path, qr/\.dev.+build-id$/, 'build_id_path points at <dist>/.dev/build-id';
is(BarefootJS::DevReload->read_build_id($path), '', 'missing sentinel reads as empty');

BarefootJS::DevReload->ensure_dev_dir($dir);



( run in 0.365 second using v1.01-cache-2.11-cpan-524268b4103 )