BarefootJS
view release on metacpan or search on metacpan
lib/BarefootJS.pm view on Meta::CPAN
for my $path (@{$self->_scripts}) {
push @tags, qq{<script type="module" src="$path"></script>};
}
return join("\n", @tags);
}
# ---------------------------------------------------------------------------
# Streaming SSR (Out-of-Order)
# ---------------------------------------------------------------------------
sub streaming_bootstrap ($self) {
return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async="'+id+'"]');var t=document.querySelector('template[bf-async-resolve="'+id+'"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('b...
}
sub async_boundary ($self, $id, $fallback_html) {
# The fallback comes in via Mojo `begin %>...<% end` capture (see
# MojoAdapter::renderAsync), which produces a CODE ref returning a
# Mojo::ByteStream. Materialize it through the backend so the rendered
# HTML embeds in the placeholder rather than the CODE ref's
# stringification.
$fallback_html = $self->backend->materialize($fallback_html);
lib/BarefootJS/DevReload.pm view on Meta::CPAN
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
# 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';
my $build_id_path = $class->build_id_path($dist_dir);
$class->ensure_dev_dir($dist_dir);
return sub ($env) {
return [500, ['Content-Type' => 'text/plain'], ['DevReload needs a psgi.streaming server']]
unless $env->{'psgi.streaming'};
my $last_event_id = $env->{HTTP_LAST_EVENT_ID} // '';
$last_event_id =~ s/^\s+|\s+$//g;
return sub ($responder) {
my $writer = $responder->([
200,
[
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache, no-transform',
t/dev_reload.t view on Meta::CPAN
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);
open my $fh, '>', $path or die $!;
print $fh "abc123\n";
close $fh;
is(BarefootJS::DevReload->read_build_id($path), 'abc123', 'sentinel is read and trimmed');
# --- PSGI streaming app ------------------------------------------------------
# Drive the streaming coderef with a fake responder/writer; break the otherwise
# infinite poll loop by throwing from write() after the initial events.
{
package FakeWriter;
sub new { bless { n => 0, lines => [] }, shift }
sub write { my ($s, $d) = @_; push @{ $s->{lines} }, $d; die "stop\n" if ++$s->{n} >= 2 }
sub close { }
}
my $app = BarefootJS::DevReload->to_app(dist_dir => $dir);
is $app->({ 'psgi.streaming' => 0 })->[0], 500, 'requires a psgi.streaming server';
my $stream = $app->({ 'psgi.streaming' => 1, HTTP_LAST_EVENT_ID => '' });
is ref $stream, 'CODE', 'streaming response is a delayed coderef';
my $writer = FakeWriter->new;
my ($status, $headers);
$stream->(sub { ($status, $headers) = @{ $_[0] }[0, 1]; return $writer });
is $status, 200, 'streams 200';
my %h = @$headers;
is $h{'Content-Type'}, 'text/event-stream', 'SSE content-type';
my $out = join '', @{ $writer->{lines} };
like $out, qr/retry: 1000/, 'sets the SSE retry hint';
like $out, qr/event: hello/, 'emits hello with the current build-id at connect';
# A stale Last-Event-ID means a rebuild was missed â reload immediately.
my $stream2 = $app->({ 'psgi.streaming' => 1, HTTP_LAST_EVENT_ID => 'STALE' });
my $w2 = FakeWriter->new;
$stream2->(sub { return $w2 });
like join('', @{ $w2->{lines} }), qr/event: reload/, 'stale Last-Event-ID triggers reload';
done_testing;
( run in 1.494 second using v1.01-cache-2.11-cpan-140bd7fdf52 )