BarefootJS
view release on metacpan or search on metacpan
lib/BarefootJS.pm view on Meta::CPAN
package BarefootJS;
our $VERSION = "0.18.5";
use strict;
use warnings;
use utf8;
use feature 'signatures';
no warnings 'experimental::signatures';
use POSIX ();
use Scalar::Util qw(looks_like_number weaken);
use BarefootJS::Evaluator ();
# NOTE: This runtime is template-engine-agnostic AND framework-agnostic by
# design, so it can ship as a standalone CPAN distribution. It depends only on
# core Perl (subroutine signatures + the hand-rolled minimal accessor base
# below â no Mojo::Base, no Class::Tiny). Every operation that depends on *how*
# a template is rendered â JSON marshalling, raw-string marking, JSX-children
# materialisation, and named-template rendering â is delegated to a pluggable
# `backend` (see BarefootJS::Backend::Mojo for the reference Mojolicious
# implementation), which is the only component that pulls in the Mojo
# distribution, and only when it is actually used.
# ---------------------------------------------------------------------------
# Minimal accessor base (no Mojo::Base / Class::Tiny dependency)
# ---------------------------------------------------------------------------
#
# Generates read/write accessors with optional lazy defaults so the runtime
# stays free of any non-core OO base. Semantics mirror the Mojo::Base `has`
# this class used to inherit: a getter returns the stored value (building it
# from the default on first access if unset); a setter stores the value and
# returns $self for chaining. A default is either a plain scalar or a coderef
# invoked as `$default->($self)` (for per-instance refs like `[]` / `{}` and
# the lazily-required Mojo backend).
my %ATTR_DEFAULT = (
_scripts => sub { [] },
_script_seen => sub { {} },
_child_renderers => sub { {} },
_is_child => 0,
# Lazily fall back to the Mojo reference backend so a bare-blessed
# instance (the pure-function unit tests) and the historical
# `BarefootJS->new($c, ...)` callers keep working unchanged. A non-Mojo
# host injects its own backend via `BarefootJS->new($c, { backend => $b })`
# and never triggers this require â keeping the core load Mojo-free.
backend => sub {
require BarefootJS::Backend::Mojo;
return BarefootJS::Backend::Mojo->new;
},
);
# c â Mojolicious controller (kept for back-compat accessors)
# config â plugin / instance config
# backend â the template-engine seam (#engine-abstraction)
# _scope_id â addressable scope id
# _bf_parent / _bf_mount â slot identity when this scope is slot-attached
# _props â props serialised into bf-p / the scope comment
# _data_key â keyed-loop-item key, emitted as data-key on the scope root
for my $attr (qw(
c config backend
_scripts _script_seen _scope_id _is_child _bf_parent _bf_mount _props
_data_key _child_renderers
)) {
no strict 'refs';
*{"BarefootJS::$attr"} = sub {
my $self = shift;
if (@_) { $self->{$attr} = shift; return $self; }
if (!exists $self->{$attr} && exists $ATTR_DEFAULT{$attr}) {
my $d = $ATTR_DEFAULT{$attr};
$self->{$attr} = ref($d) eq 'CODE' ? $d->($self) : $d;
}
return $self->{$attr};
};
}
sub new ($class, $c, $config = {}) {
# Build (or accept an injected) rendering backend. The default Mojo
# backend wraps the controller and honours an optional `json_encoder`
# override so a host can swap in a faster XS JSON implementation
# without subclassing. A caller targeting another template engine
# passes its own backend via `$config->{backend}`.
my $backend = $config->{backend};
unless ($backend) {
require BarefootJS::Backend::Mojo;
$backend = BarefootJS::Backend::Mojo->new(
c => $c,
($config->{json_encoder}
? (json_encoder => $config->{json_encoder})
: ()),
);
}
my $self = bless {
c => $c,
config => $config,
backend => $backend,
}, $class;
# Hold the controller weakly. Mojolicious stashes this bf instance under
# `$c->stash->{'bf.instance'}`, so a strong bf -> controller back-reference
# closes a per-request cycle ($c -> stash -> bf -> $c) that Perl's
# refcount GC cannot reclaim, leaking one controller + bf + child-renderer
# closures per request. The controller owns (outlives) the per-request bf,
# so the weak ref stays valid for the whole render. Callers that need the
# controller to outlive the bf instance independently must keep their own
# strong reference (the normal Mojo request scope already does).
weaken($self->{c}) if defined $c;
return $self;
}
# search_params($query = '')
#
# Build a request-scoped reader for the reactive searchParams() environment
# signal (router v0.5, #1922) from a raw query string. Callable as a class or
# instance method â the invocant is unused.
#
# The `require` lives here so consumers (the Mojo plugin, the Xslate host, the
# test harness, generated render scripts) reach BarefootJS::SearchParams through
# the BarefootJS object they already hold, never `use`-ing it directly â the
# same lazy-load seam the Mojo backend uses above. The compiled template reads
# the returned object via `$searchParams->get('key')`.
sub search_params ($invocant, $query = '') {
require BarefootJS::SearchParams;
return BarefootJS::SearchParams->new($query);
}
# ---------------------------------------------------------------------------
# Scope & Props
# ---------------------------------------------------------------------------
sub scope_attr ($self) {
# bf-s is the addressable scope id only (#1249).
return $self->_scope_id // '';
}
# Emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.
# See spec/compiler.md "Slot identity".
sub hydration_attrs ($self) {
my @parts;
my $host = $self->_bf_parent;
my $mount = $self->_bf_mount;
if (defined $host && length $host) {
my $h = $host =~ s/"/"/gr;
push @parts, qq{bf-h="$h"};
}
if (defined $mount && length $mount) {
my $m = $mount =~ s/"/"/gr;
push @parts, qq{bf-m="$m"};
}
unless ($self->_is_child) {
push @parts, q{bf-r=""};
}
return join(' ', @parts);
}
# Emits ` data-key="<key>"` for a keyed loop item, else ''. The client
# runtime uses data-key for list reconciliation; SSR must match the Hono
# reference, which stamps it on each loop item's scope root. The value is set
# on the child instance by the child renderer (`register_child_renderer` /
# `register_components_from_manifest`) from the JSX `key` prop â a reserved
# prop, never a real template variable.
sub data_key_attr ($self) {
my $k = $self->_data_key;
return '' unless defined $k;
$k =~ s/&/&/g;
$k =~ s/"/"/g;
return qq{ data-key="$k"};
lib/BarefootJS.pm view on Meta::CPAN
for my $entry_name (keys %$manifest) {
# `__barefoot__` is the runtime entry, not a component.
next if $entry_name eq '__barefoot__';
# Only UI registry components (path shape `ui/<name>/index`)
# become child renderers; top-level page components are the
# render target rather than a child.
next unless $entry_name =~ m{^ui/([^/]+)/index$};
my $slot_key = $1;
my $entry = $manifest->{$entry_name};
# Directory-name registration â the pre-`components` convention,
# kept so manifests from older builds (no `components` map) still
# resolve single-component modules like `button`.
$self->_register_manifest_child(
$slot_key, $entry->{markedTemplate},
$signal_inits->{$slot_key}, $entry->{ssrDefaults},
);
# Per-component registrations (#2132), keyed by the snake_cased
# component name the compiled templates actually call.
my $components = $entry->{components};
next unless ref($components) eq 'HASH';
for my $component_name (sort keys %$components) {
my $component = $components->{$component_name};
next unless ref($component) eq 'HASH';
my $component_key = _snake_case($component_name);
$self->_register_manifest_child(
$component_key, $component->{markedTemplate},
$signal_inits->{$component_key}, $component->{ssrDefaults},
);
}
}
}
# PascalCase â snake_case, mirroring the Mojo adapter's `toTemplateName`
# (prefix every uppercase letter with `_`, lowercase the whole string,
# strip the leading `_`): `ToastProvider` â `toast_provider`.
sub _snake_case ($name) {
my $s = $name;
$s =~ s/([A-Z])/_$1/g;
$s = CORE::lc $s;
$s =~ s/^_//;
return $s;
}
# Register one manifest-driven child renderer: `$slot_key` becomes the
# `render_child` name, `$marked` locates the template, and `$signal_init`
# / `$manifest_defaults` seed the child's template vars (see
# `register_components_from_manifest` for the contract).
sub _register_manifest_child ($self, $slot_key, $marked, $signal_init, $manifest_defaults) {
$marked //= '';
return unless $marked;
my $parent_scope = $self->_scope_id;
# Weaken the parent capture so the child-renderer closures stored on
# `$self->_child_renderers` don't keep `$self` alive (the direct
# closure <-> parent cycle). The controller is reached through `$parent`
# at call time rather than captured strongly here, so the closures hold
# no strong reference to `$c` either â see the controller-cycle note in
# `new`. `$parent` is always live whenever a closure runs (the closure is
# stored on `$parent`, so `$parent` outlives every invocation).
weaken(my $parent = $self);
# `templates/ui/button/index.html.ep` â `ui/button/index`
my $template_name = $marked;
$template_name =~ s{^templates/}{};
$template_name =~ s{\.html\.ep$}{};
$self->register_child_renderer($slot_key, sub {
# `$caller` is the instance whose template invoked
# `render_child` (#1897) â for a nested render that is a child
# instance, and the grandchild's scope/slot identity must chain
# off ITS scope id (`root_s0_s0`), not the registrant's.
my ($props, $caller) = @_;
my $host = $caller // $parent;
my $host_scope = $host->_scope_id // $parent_scope;
# Child shares the parent's backend so nested renders go
# through the same engine + controller (and inherit any
# injected json_encoder). The controller is fetched via the weak
# `$parent` at call time â never captured strongly â so the
# closure adds no edge to the per-request reference cycle.
my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });
my $slot_id = delete $props->{_bf_slot};
# JSX `key` (a reserved prop) â data-key on the child's scope root
# for keyed-loop reconciliation (see `data_key_attr`).
my $data_key = delete $props->{key};
$child_bf->_data_key($data_key) if defined $data_key;
$child_bf->_scope_id(
$slot_id ? $host_scope . '_' . $slot_id
: $template_name . '_' . substr(rand() =~ s/^0\.//r, 0, 6)
);
$child_bf->_is_child(1);
# (#1249) Slot identity: host scope + slot id. Emitted as
# bf-h / bf-m attributes by hydration_attrs.
if ($slot_id) {
$child_bf->_bf_parent($host_scope);
$child_bf->_bf_mount($slot_id);
}
# Share the root registry so the child's own template can
# render further imported components (#1897).
$child_bf->_child_renderers($parent->_child_renderers);
$child_bf->_scripts($parent->_scripts);
$child_bf->_script_seen($parent->_script_seen);
my %extra;
if ($signal_init) {
%extra = $signal_init->($props);
} elsif ($manifest_defaults) {
%extra = _derive_stash_from_defaults($manifest_defaults, $props);
}
# Render the child template with $child_bf bound as the active
# instance for the nested render. The backend owns the
# engine-specific binding + restore (stash juggle for Mojo).
my $html = $parent->backend->render_named(
$template_name, $child_bf, { %$props, %extra },
);
chomp $html;
return $html;
});
}
( run in 1.174 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )