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/"/&quot;/gr;
        push @parts, qq{bf-h="$h"};
    }
    if (defined $mount && length $mount) {
        my $m = $mount =~ s/"/&quot;/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/&/&amp;/g;
    $k =~ s/"/&quot;/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 )