@barefootjs/cli 0.9.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,6 +101,12 @@ The generated `.html.ep` templates call the runtime through the `bf` helper
101
101
  npm install @barefootjs/xslate
102
102
  ```
103
103
 
104
+ Scaffold a runnable starter (a plain Plack/PSGI app served by Starman):
105
+
106
+ ```
107
+ npm create barefootjs@latest -- --adapter xslate
108
+ ```
109
+
104
110
  ```typescript
105
111
  import { createConfig } from '@barefootjs/xslate/build'
106
112
 
package/dist/index.js CHANGED
@@ -23352,7 +23352,7 @@ main {
23352
23352
  });
23353
23353
 
23354
23354
  // src/lib/adapters/runtimes.generated.ts
23355
- var bfGoSource, streamingGoSource, bfdevGoSource, barefootPmSource, barefootBackendMojoPmSource, barefootPluginPmSource, barefootDevReloadPmSource;
23355
+ var bfGoSource, streamingGoSource, bfdevGoSource;
23356
23356
  var init_runtimes_generated = __esm({
23357
23357
  "src/lib/adapters/runtimes.generated.ts"() {
23358
23358
  "use strict";
@@ -23564,120 +23564,6 @@ func StreamingFuncMap() template.FuncMap {
23564
23564
  }
23565
23565
  `;
23566
23566
  bfdevGoSource = '// Package bfdev provides a dev-only browser auto-reload handler.\n//\n// It watches `<distDir>/.dev/build-id` (produced by `bf build --watch`\n// in the @barefootjs/cli package) and streams SSE `event: reload` whenever\n// the sentinel changes. Combined with the inline client snippet returned by\n// Snippet, editing a .tsx component triggers a browser reload automatically.\n//\n// The handler is framework-agnostic (net/http.Handler). Echo users can mount\n// it via echo.WrapHandler; other routers use it as-is.\n//\n// Example (Echo):\n//\n// if bfdev.IsDevDefault() {\n// e.GET("/_bf/reload", echo.WrapHandler(bfdev.NewReloadHandler(bfdev.Config{\n// DistDir: "./dist",\n// })))\n// }\n//\n// Example (net/http):\n//\n// http.Handle("/_bf/reload", bfdev.NewReloadHandler(bfdev.Config{DistDir: "./dist"}))\npackage bfdev\n\nimport (\n "fmt"\n "html/template"\n "net/http"\n "os"\n "path/filepath"\n "strings"\n "time"\n)\n\n// Sentinel path contract with `@barefootjs/cli`\n// (`packages/cli/src/lib/build.ts`, DEV_SENTINEL_SUBDIR / DEV_SENTINEL_FILENAME).\n// Duplicated here so the Go runtime avoids a dependency on the CLI. If the\n// CLI changes these values, update this package in the same PR.\nconst (\n devSubdir = ".dev"\n buildIDFile = "build-id"\n scrollStorageKey = "__bf_devreload_scroll"\n\n // heartbeatInterval keeps the SSE stream under the framework\'s idle\n // timeout (Bun.serve defaults to 10s; Go/Echo defaults are more forgiving\n // but middleware-level timeouts exist in the wild). 5s leaves comfortable\n // headroom.\n heartbeatInterval = 5 * time.Second\n\n // pollInterval is how often the handler checks `.dev/build-id`. Uses\n // polling instead of fsnotify to keep the runtime dependency-free \u2014 dev\n // latency of ~500ms is imperceptible next to the browser\'s reload time.\n pollInterval = 500 * time.Millisecond\n)\n\n// Config configures a dev reload handler or snippet.\ntype Config struct {\n // DistDir is the directory that `bf build` writes output into\n // (contains `.dev/build-id`). Required for the handler; ignored by\n // Snippet.\n DistDir string\n\n // Endpoint is the public SSE URL the client will connect to. Used only by\n // Snippet to populate the EventSource URL. Defaults to "/_bf/reload" when\n // empty.\n Endpoint string\n\n // Disabled, when true, makes NewReloadHandler return a 404 handler and\n // Snippet return an empty fragment. Intended for production builds.\n Disabled bool\n}\n\n// IsDevDefault reports whether the process is running in a development\n// environment using the common Go convention of APP_ENV=development.\n// Callers can use this to populate Config.Disabled:\n//\n// cfg := bfdev.Config{DistDir: "./dist", Disabled: !bfdev.IsDevDefault()}\nfunc IsDevDefault() bool {\n return os.Getenv("APP_ENV") == "development"\n}\n\n// NewReloadHandler returns an http.Handler that streams Server-Sent Events\n// and emits `event: reload` whenever `<DistDir>/.dev/build-id` changes. When\n// cfg.Disabled is true, the handler responds 404 and never opens a stream.\nfunc NewReloadHandler(cfg Config) http.Handler {\n if cfg.Disabled {\n return http.HandlerFunc(http.NotFound)\n }\n devDir := filepath.Join(cfg.DistDir, devSubdir)\n buildIDPath := filepath.Join(devDir, buildIDFile)\n // Ensure the directory exists so the first read does not race with the\n // initial build. Ignore the error: subsequent reads simply return "".\n _ = os.MkdirAll(devDir, 0o755)\n\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n flusher, ok := w.(http.Flusher)\n if !ok {\n http.Error(w, "streaming unsupported", http.StatusInternalServerError)\n return\n }\n h := w.Header()\n h.Set("Content-Type", "text/event-stream")\n h.Set("Cache-Control", "no-cache, no-transform")\n h.Set("Connection", "keep-alive")\n h.Set("X-Accel-Buffering", "no")\n\n send := func(chunk string) bool {\n if _, err := fmt.Fprint(w, chunk); err != nil {\n return false\n }\n flusher.Flush()\n return true\n }\n\n if !send("retry: 1000\\n\\n") {\n return\n }\n\n lastEventID := strings.TrimSpace(r.Header.Get("Last-Event-ID"))\n initialID := readBuildID(buildIDPath)\n lastSent := ""\n if initialID != "" {\n lastSent = initialID\n // When the client reconnects with a stale Last-Event-ID, a build\n // happened during its disconnected window \u2014 fire `reload`\n // immediately so the missed rebuild does not silently stay\n // unpainted until the next change.\n event := "hello"\n if lastEventID != "" && lastEventID != initialID {\n event = "reload"\n }\n if !send(fmt.Sprintf("event: %s\\nid: %s\\ndata: %s\\n\\n", event, initialID, initialID)) {\n return\n }\n }\n\n ctx := r.Context()\n hbTicker := time.NewTicker(heartbeatInterval)\n defer hbTicker.Stop()\n pollTicker := time.NewTicker(pollInterval)\n defer pollTicker.Stop()\n\n for {\n select {\n case <-ctx.Done():\n return\n case <-hbTicker.C:\n if !send(": hb\\n\\n") {\n return\n }\n case <-pollTicker.C:\n id := readBuildID(buildIDPath)\n if id == "" || id == lastSent {\n continue\n }\n lastSent = id\n if !send(fmt.Sprintf("event: reload\\nid: %s\\ndata: %s\\n\\n", id, id)) {\n return\n }\n }\n }\n })\n}\n\nfunc readBuildID(path string) string {\n b, err := os.ReadFile(path)\n if err != nil {\n return ""\n }\n return strings.TrimSpace(string(b))\n}\n\n// Snippet returns an inline <script> that subscribes to the SSE endpoint,\n// reloads on `reload`, and preserves window.scrollY across reloads via\n// sessionStorage. Returns an empty fragment when cfg.Disabled is true.\n//\n// Place it before </body>, typically just after the rendered scripts.\nfunc Snippet(cfg Config) template.HTML {\n if cfg.Disabled {\n return ""\n }\n endpoint := cfg.Endpoint\n if endpoint == "" {\n endpoint = "/_bf/reload"\n }\n // Small IIFE: EventSource subscriber + scrollY preservation. Idempotent\n // across duplicate mounts (guarded by window.__bfDevReload).\n js := fmt.Sprintf(\n `(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;`+\n `try{var s=sessionStorage.getItem(%q);if(s){sessionStorage.removeItem(%q);`+\n `var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};`+\n `if(document.readyState===\'loading\'){addEventListener(\'DOMContentLoaded\',restore,{once:true})}else{restore()}}}}catch(e){}`+\n `var es=new EventSource(%q);`+\n `es.addEventListener(\'reload\',function(){try{sessionStorage.setItem(%q,String(window.scrollY))}catch(e){}location.reload()});`+\n `es.addEventListener(\'error\',function(){})})();`,\n scrollStorageKey, scrollStorageKey, endpoint, scrollStorageKey,\n )\n // Safe: `js` is assembled from package-internal literals plus `endpoint`\n // escaped by %q (Go-syntax quoting == valid JS string literal for the\n // ASCII endpoint paths this accepts).\n return template.HTML("<script>" + js + "</script>") //nolint:gosec\n}\n';
23567
- barefootPmSource = "package BarefootJS;\nour $VERSION = \"0.9.5\";\nuse strict;\nuse warnings;\nuse utf8;\nuse feature 'signatures';\nno warnings 'experimental::signatures';\n\nuse POSIX ();\nuse Scalar::Util qw(looks_like_number weaken);\n\n# NOTE: This runtime is template-engine-agnostic AND framework-agnostic by\n# design, so it can ship as a standalone CPAN distribution. It depends only on\n# core Perl (subroutine signatures + the hand-rolled minimal accessor base\n# below \u2014 no Mojo::Base, no Class::Tiny). Every operation that depends on *how*\n# a template is rendered \u2014 JSON marshalling, raw-string marking, JSX-children\n# materialisation, and named-template rendering \u2014 is delegated to a pluggable\n# `backend` (see BarefootJS::Backend::Mojo for the reference Mojolicious\n# implementation), which is the only component that pulls in the Mojo\n# distribution, and only when it is actually used.\n\n# ---------------------------------------------------------------------------\n# Minimal accessor base (no Mojo::Base / Class::Tiny dependency)\n# ---------------------------------------------------------------------------\n#\n# Generates read/write accessors with optional lazy defaults so the runtime\n# stays free of any non-core OO base. Semantics mirror the Mojo::Base `has`\n# this class used to inherit: a getter returns the stored value (building it\n# from the default on first access if unset); a setter stores the value and\n# returns $self for chaining. A default is either a plain scalar or a coderef\n# invoked as `$default->($self)` (for per-instance refs like `[]` / `{}` and\n# the lazily-required Mojo backend).\nmy %ATTR_DEFAULT = (\n _scripts => sub { [] },\n _script_seen => sub { {} },\n _child_renderers => sub { {} },\n _is_child => 0,\n # Lazily fall back to the Mojo reference backend so a bare-blessed\n # instance (the pure-function unit tests) and the historical\n # `BarefootJS->new($c, ...)` callers keep working unchanged. A non-Mojo\n # host injects its own backend via `BarefootJS->new($c, { backend => $b })`\n # and never triggers this require \u2014 keeping the core load Mojo-free.\n backend => sub {\n require BarefootJS::Backend::Mojo;\n return BarefootJS::Backend::Mojo->new;\n },\n);\n\n# c \u2014 Mojolicious controller (kept for back-compat accessors)\n# config \u2014 plugin / instance config\n# backend \u2014 the template-engine seam (#engine-abstraction)\n# _scope_id \u2014 addressable scope id\n# _bf_parent / _bf_mount \u2014 slot identity when this scope is slot-attached\n# _props \u2014 props serialised into bf-p / the scope comment\n# _data_key \u2014 keyed-loop-item key, emitted as data-key on the scope root\nfor my $attr (qw(\n c config backend\n _scripts _script_seen _scope_id _is_child _bf_parent _bf_mount _props\n _data_key _child_renderers\n)) {\n no strict 'refs';\n *{\"BarefootJS::$attr\"} = sub {\n my $self = shift;\n if (@_) { $self->{$attr} = shift; return $self; }\n if (!exists $self->{$attr} && exists $ATTR_DEFAULT{$attr}) {\n my $d = $ATTR_DEFAULT{$attr};\n $self->{$attr} = ref($d) eq 'CODE' ? $d->($self) : $d;\n }\n return $self->{$attr};\n };\n}\n\nsub new ($class, $c, $config = {}) {\n # Build (or accept an injected) rendering backend. The default Mojo\n # backend wraps the controller and honours an optional `json_encoder`\n # override so a host can swap in a faster XS JSON implementation\n # without subclassing. A caller targeting another template engine\n # passes its own backend via `$config->{backend}`.\n my $backend = $config->{backend};\n unless ($backend) {\n require BarefootJS::Backend::Mojo;\n $backend = BarefootJS::Backend::Mojo->new(\n c => $c,\n ($config->{json_encoder}\n ? (json_encoder => $config->{json_encoder})\n : ()),\n );\n }\n my $self = bless {\n c => $c,\n config => $config,\n backend => $backend,\n }, $class;\n # Hold the controller weakly. Mojolicious stashes this bf instance under\n # `$c->stash->{'bf.instance'}`, so a strong bf -> controller back-reference\n # closes a per-request cycle ($c -> stash -> bf -> $c) that Perl's\n # refcount GC cannot reclaim, leaking one controller + bf + child-renderer\n # closures per request. The controller owns (outlives) the per-request bf,\n # so the weak ref stays valid for the whole render. Callers that need the\n # controller to outlive the bf instance independently must keep their own\n # strong reference (the normal Mojo request scope already does).\n weaken($self->{c}) if defined $c;\n return $self;\n}\n\n# ---------------------------------------------------------------------------\n# Scope & Props\n# ---------------------------------------------------------------------------\n\nsub scope_attr ($self) {\n # bf-s is the addressable scope id only (#1249).\n return $self->_scope_id // '';\n}\n\n# Emits `bf-h=\"<host>\" bf-m=\"<slot>\" bf-r=\"\"` conditionally.\n# See spec/compiler.md \"Slot identity\".\nsub hydration_attrs ($self) {\n my @parts;\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n my $h = $host =~ s/\"/&quot;/gr;\n push @parts, qq{bf-h=\"$h\"};\n }\n if (defined $mount && length $mount) {\n my $m = $mount =~ s/\"/&quot;/gr;\n push @parts, qq{bf-m=\"$m\"};\n }\n unless ($self->_is_child) {\n push @parts, q{bf-r=\"\"};\n }\n return join(' ', @parts);\n}\n\n# Emits ` data-key=\"<key>\"` for a keyed loop item, else ''. The client\n# runtime uses data-key for list reconciliation; SSR must match the Hono\n# reference, which stamps it on each loop item's scope root. The value is set\n# on the child instance by the child renderer (`register_child_renderer` /\n# `register_components_from_manifest`) from the JSX `key` prop \u2014 a reserved\n# prop, never a real template variable.\nsub data_key_attr ($self) {\n my $k = $self->_data_key;\n return '' unless defined $k;\n $k =~ s/&/&amp;/g;\n $k =~ s/\"/&quot;/g;\n return qq{ data-key=\"$k\"};\n}\n\nsub props_attr ($self) {\n my $props = $self->_props;\n return '' unless $props && %$props;\n # encode_json returns a character string (not bytes) for safe embedding\n # in templates (the Mojo backend uses Mojo::JSON::to_json).\n my $json = $self->backend->encode_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Context (SSR mirror of the client `provideContext` / `useContext`)\n# ---------------------------------------------------------------------------\n#\n# A `<Ctx.Provider value>` seeds a value that descendant `useContext(Ctx)`\n# consumers read during the same render. Dynamic scoping mirrors the client:\n# the provider pushes the value before rendering its children and pops it\n# after, and `use_context` reads the innermost active value (or the\n# `createContext` default when none is active).\n#\n# The value stacks live in a package-level store rather than per-instance or\n# on `$c->stash`: a parent template and the child templates it renders via\n# `render_child` are separate bf instances that don't reliably share a\n# controller (the Xslate backend runs with `c => undef`) nor a backend (the\n# Mojo path lazily builds one per instance). SSR rendering is synchronous \u2014\n# nothing awaits between a provider's push and its matching pop \u2014 and the\n# push/pop are perfectly balanced, so the per-name stack always unwinds to\n# empty at the end of each provider subtree, keeping concurrent root renders\n# isolated. provide/revoke return '' so they drop cleanly into an inline\n# `<: \u2026 :>` (Kolon) or `% \u2026 ;` (EP) emit.\n\nmy %CONTEXT_STACKS;\n\nsub provide_context ($self, $name, $value) {\n push @{ $CONTEXT_STACKS{$name} //= [] }, $value;\n return '';\n}\n\nsub revoke_context ($self, $name) {\n pop @{ $CONTEXT_STACKS{$name} } if $CONTEXT_STACKS{$name} && @{ $CONTEXT_STACKS{$name} };\n return '';\n}\n\nsub use_context ($self, $name, $default = undef) {\n my $stack = $CONTEXT_STACKS{$name};\n return $default unless $stack && @$stack;\n return $stack->[-1];\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . $self->backend->encode_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n# (`_child_renderers` accessor is generated by the minimal accessor base above.)\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, @args) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # Accept both the Mojo list form \u2014 `bf->render_child($name, k => v, ...)`\n # \u2014 and the single-hashref form \u2014 `$bf.render_child($name, { k => v })`.\n # Template languages whose method calls can't splat a hash into positional\n # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.\n my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;\n # JSX children come in via the engine's children-capture mechanism\n # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a\n # Mojo::ByteStream). Materialize it through the backend before handing\n # the props to the child renderer so the child template sees\n # `$children` as already-rendered HTML. Guard on `exists` so a\n # childless invocation (`bf->render_child('counter')`) doesn't gain a\n # spurious `children => undef` key \u2014 preserving the historical \"only\n # touch children when present\" behaviour.\n $props{children} = $self->backend->materialize($props{children})\n if exists $props{children};\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n # Weaken the parent capture so the child-renderer closures stored on\n # `$self->_child_renderers` don't keep `$self` alive (the direct\n # closure <-> parent cycle). The controller is reached through `$parent`\n # at call time rather than captured strongly here, so the closures hold\n # no strong reference to `$c` either \u2014 see the controller-cycle note in\n # `new`. `$parent` is always live whenever a closure runs (the closure is\n # stored on `$parent`, so `$parent` outlives every invocation).\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n # Child shares the parent's backend so nested renders go\n # through the same engine + controller (and inherit any\n # injected json_encoder). The controller is fetched via the weak\n # `$parent` at call time \u2014 never captured strongly \u2014 so the\n # closure adds no edge to the per-request reference cycle.\n my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });\n my $slot_id = delete $props->{_bf_slot};\n # JSX `key` (a reserved prop) \u2192 data-key on the child's scope root\n # for keyed-loop reconciliation (see `data_key_attr`).\n my $data_key = delete $props->{key};\n $child_bf->_data_key($data_key) if defined $data_key;\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n # Render the child template with $child_bf bound as the active\n # instance for the nested render. The backend owns the\n # engine-specific binding + restore (stash juggle for Mojo).\n my $html = $parent->backend->render_named(\n $template_name, $child_bf, { %$props, %extra },\n );\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n 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('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it through the backend so the rendered\n # HTML embeds in the placeholder rather than the CODE ref's\n # stringification.\n $fallback_html = $self->backend->materialize($fallback_html);\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return $self->backend->encode_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.filter(fn)` / `.every(fn)` / `.some(fn)`. The Xslate adapter\n# lowers a JS arrow predicate to a Kolon lambda (`-> $x { ... }`), which is\n# callable from Perl as a code ref, and emits `$bf.filter($arr, <lambda>)`.\n# `filter` returns a new arrayref; `every` / `some` return 1/0. Non-array /\n# empty receivers follow JS (`filter` \u2192 [], `every` \u2192 true, `some` \u2192 false).\n# (The Mojo adapter lowers these shapes inline and never reaches these methods.)\nsub filter ($self, $recv, $pred) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ grep { $pred->($_) } @$recv ];\n}\n\nsub every ($self, $recv, $pred) {\n return 1 unless ref($recv) eq 'ARRAY';\n for my $item (@$recv) { return 0 unless $pred->($item) }\n return 1;\n}\n\nsub some ($self, $recv, $pred) {\n return 0 unless ref($recv) eq 'ARRAY';\n for my $item (@$recv) { return 1 if $pred->($item) }\n return 0;\n}\n\n# `Array.prototype.find(fn)` / `.findIndex(fn)` / `.findLast(fn)` /\n# `.findLastIndex(fn)` \u2014 same Kolon-lambda predicate mechanism as filter. The\n# camelCase JS names lower to these snake_case methods (like index_of /\n# last_index_of). `find` / `find_last` return the matching element (or undef \u2192\n# JS `undefined`); the index forms return the 0-based position (or -1).\nsub find ($self, $recv, $pred) {\n return undef unless ref($recv) eq 'ARRAY';\n for my $item (@$recv) { return $item if $pred->($item) }\n return undef;\n}\n\nsub find_index ($self, $recv, $pred) {\n return -1 unless ref($recv) eq 'ARRAY';\n for my $i (0 .. $#$recv) { return $i if $pred->($recv->[$i]) }\n return -1;\n}\n\nsub find_last ($self, $recv, $pred) {\n return undef unless ref($recv) eq 'ARRAY';\n for my $i (reverse 0 .. $#$recv) { return $recv->[$i] if $pred->($recv->[$i]) }\n return undef;\n}\n\nsub find_last_index ($self, $recv, $pred) {\n return -1 unless ref($recv) eq 'ARRAY';\n for my $i (reverse 0 .. $#$recv) { return $i if $pred->($recv->[$i]) }\n return -1;\n}\n\n# `String.prototype.toLowerCase()` / `.toUpperCase()`. Kolon has a builtin\n# `.join` array method (so the adapter uses that directly) but no builtin\n# `lc` / `uc`, so these live on the runtime object. `CORE::` avoids recursing\n# into these methods.\nsub lc ($self, $s) { return defined $s ? CORE::lc($s) : '' }\nsub uc ($self, $s) { return defined $s ? CORE::uc($s) : '' }\n\n# `Array.prototype.join(sep)` with JS semantics: separator defaults to \",\",\n# and undefined / null elements render as empty (`[1,,2].join(\",\")` \u2192 \"1,,2\").\n# Kolon has a builtin `.join`, but routing through the runtime keeps the\n# JS-compat element handling in one place. `CORE::join` avoids recursing.\nsub join ($self, $recv, $sep = undef) {\n return '' unless ref($recv) eq 'ARRAY';\n $sep //= ',';\n return CORE::join($sep, map { defined $_ ? $_ : '' } @$recv);\n}\n\n# `.length` \u2014 JS works on BOTH arrays (element count) and strings (character\n# count); Kolon's builtin `.size()` is array-only and faults on a string. So\n# dispatch on ref type here. `CORE::length` avoids recursing into this method.\nsub length ($self, $recv) {\n return scalar @$recv if ref($recv) eq 'ARRAY';\n return 0 if ref($recv);\n return CORE::length($recv // '');\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\n# - start < 0 \u2192 length + start (e.g. -1 = last index)\n# - end < 0 \u2192 length + end\n# - start < 0 after clamp \u2192 0\n# - end > length \u2192 length\n# - start >= end \u2192 empty\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `Array.prototype.flat(depth?)` (#1448 Tier C) \u2014 flatten nested ARRAY\n# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel\n# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept\n# as-is (JS only flattens nested arrays). Non-ARRAY receiver \u2192 [].\nsub flat ($self, $recv, $depth = 1) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n if ($depth != 0 && ref($el) eq 'ARRAY') {\n my $next = $depth > 0 ? $depth - 1 : $depth;\n push @out, @{ $self->flat($el, $next) };\n }\n else {\n push @out, $el;\n }\n }\n return \\@out;\n}\n\n# `Array.prototype.flatMap(fn)` value-returning field projection\n# (#1448 Tier C) \u2014 map each element through a self / field projection,\n# then flatten one level. `field` reads a HASH-ref key (the raw JS prop\n# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is\n# (flatMap = map + flat(1)). Non-ARRAY receiver \u2192 [].\nsub flat_map ($self, $recv, $key_kind, $key) {\n return [] unless ref($recv) eq 'ARRAY';\n my @projected;\n for my $el (@$recv) {\n if ($key_kind eq 'field') {\n # JS `i => i.field` on a non-object yields `undefined`, not the\n # element itself \u2014 push `undef` so a scalar element doesn't leak\n # into the output (matches Go's `getFieldValue` returning nil).\n push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @projected, $el;\n }\n }\n return $self->flat(\\@projected, 1);\n}\n\n# `Array.prototype.flatMap(i => [i.a, i.b])` \u2014 array-literal tuple\n# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref\n# (['self', ''] or ['field', 'a']). For each element, every leaf's value\n# is appended in order. flat(1) removes only the literal wrapper, so an\n# array-valued leaf is appended verbatim (no spread) \u2014 i.e. just append\n# each leaf. A non-HASH element under a `field` leaf yields undef (JS\n# `i.field` on a non-object). Non-ARRAY receiver \u2192 [].\nsub flat_map_tuple ($self, $recv, @specs) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n for my $spec (@specs) {\n my ($kind, $key) = @$spec;\n if ($kind eq 'field') {\n push @out, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @out, $el;\n }\n }\n }\n return \\@out;\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `String.prototype.split(sep)` (#1448 Tier B) \u2014 string \u2192 ARRAY ref.\n#\n# Two JS-parity wrinkles drive the helper (a bare `split` emit would\n# diverge from both JS and Go):\n#\n# * Perl's `split` treats its first argument as a *regex*, so a\n# separator like '.' or '|' would match far too much. We\n# `quotemeta` it to force literal-string matching, mirroring JS's\n# string-separator semantics (the regex-separator form stays\n# refused upstream \u2014 see the parser arm).\n# * Perl's `split` drops trailing empty fields by default; JS keeps\n# them (`\"a,\".split(\",\")` is `[\"a\", \"\"]`). Passing the `-1` limit\n# preserves them, matching JS and Go's `strings.Split`.\n#\n# An empty separator splits into individual characters (JS + Go agree).\n# Undef receiver renders as the single-element `['']` \u2014 the same\n# \"missing prop \u2192 empty string\" convention `bf->trim` uses.\n\nsub split ($self, $recv, $sep = undef, $limit = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n\n my @parts;\n if (!defined $sep) {\n # No separator \u2192 the whole string in a single-element array\n # (matches JS `\"x\".split()` / `.split(undefined)`).\n @parts = ($s);\n }\n elsif (\"$sep\" eq '') {\n # Empty separator \u2192 individual characters. No `-1` limit here:\n # on an empty pattern Perl's `split` with `-1` appends a spurious\n # trailing empty field (\"abc\" \u2192 'a','b','c',''), which JS/Go don't.\n @parts = split //, $s;\n }\n elsif ($s eq '') {\n # Empty input with a non-empty separator: JS `\"\".split(\",\")` is\n # `[\"\"]` and Go's `strings.Split(\"\", \",\")` is `[\"\"]`, but Perl's\n # `split /,/, ''` returns the empty list \u2014 special-case for parity.\n @parts = ('');\n }\n else {\n # `quotemeta` forces literal-string matching (JS string-separator\n # semantics); the `-1` keeps trailing empty fields (JS keeps them,\n # Perl's bare `split` drops them).\n my $q = quotemeta(\"$sep\");\n @parts = split /$q/, $s, -1;\n }\n\n # Optional `limit` caps the number of pieces (JS `split(sep, limit)`).\n # 0 \u2192 empty; a negative limit keeps all (JS ToUint32 wrap makes it\n # effectively unbounded) \u2014 both match Go's `bf_split`.\n if (defined $limit) {\n my $n = int($limit);\n if ($n == 0) { @parts = () }\n elsif ($n > 0 && $n < scalar @parts) { @parts = @parts[0 .. $n - 1] }\n }\n\n return [@parts];\n}\n\n# `String.prototype.startsWith(prefix, position?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). `substr`-anchored literal comparison mirrors\n# Go's `strings.HasPrefix`. An empty prefix is always true (JS parity);\n# undef / non-string receivers coerce to the empty string first. The\n# optional `position` re-anchors the test (clamped to `[0, length]`),\n# matching JS `\"abc\".startsWith(\"b\", 1)`.\n\nsub starts_with ($self, $recv, $prefix, $position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $p = defined $prefix ? \"$prefix\" : '';\n if (defined $position) {\n my $n = int($position);\n $n = 0 if $n < 0;\n $n = CORE::length($s) if $n > CORE::length($s);\n $s = substr($s, $n);\n }\n return substr($s, 0, CORE::length $p) eq $p ? 1 : 0;\n}\n\n# `String.prototype.endsWith(suffix, endPosition?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). Mirrors Go's `strings.HasSuffix`. An empty\n# suffix is always true (JS parity); a suffix longer than the string is\n# false. `substr($s, -length $x)` would mis-read the whole string when\n# `length $x == 0`, so that case short-circuits. The optional\n# `endPosition` treats the string as if it were only that many chars\n# long (clamped to `[0, length]`), matching JS `\"abc\".endsWith(\"b\", 2)`.\n\nsub ends_with ($self, $recv, $suffix, $end_position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $x = defined $suffix ? \"$suffix\" : '';\n if (defined $end_position) {\n my $e = int($end_position);\n $e = 0 if $e < 0;\n $e = CORE::length($s) if $e > CORE::length($s);\n $s = substr($s, 0, $e);\n }\n return 1 if $x eq '';\n return 0 if CORE::length($s) < CORE::length($x);\n return substr($s, -CORE::length $x) eq $x ? 1 : 0;\n}\n\n# `String.prototype.replace(pattern, replacement)` \u2014 string-pattern\n# form only (#1448 Tier B), replacing the FIRST occurrence (JS string-\n# pattern semantics). Spliced via index/substr rather than `s///` so\n# BOTH the pattern and the replacement are literal: no Perl regex\n# metacharacters in the pattern and no `$1` / `$&` interpolation in the\n# replacement. Go's `bf_replace` (strings.Replace, n=1) treats the\n# replacement literally too, so the two adapters stay byte-equal \u2014 this\n# diverges from JS only for replacement strings containing `$`-patterns\n# (rare in template position). An empty pattern inserts the replacement\n# at the front (`\"abc\".replace(\"\", \"X\")` \u2192 \"Xabc\"), matching JS + Go.\n\nsub replace ($self, $recv, $pattern, $replacement) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $o = defined $pattern ? \"$pattern\" : '';\n my $n = defined $replacement ? \"$replacement\" : '';\n return $n . $s if $o eq '';\n my $i = index($s, $o);\n return $s if $i < 0;\n return substr($s, 0, $i) . $n . substr($s, $i + CORE::length($o));\n}\n\n# `String.prototype.repeat(n)` \u2014 the receiver concatenated n times\n# (#1448 Tier B), via Perl's `x` operator. JS throws RangeError for a\n# negative count, but SSR templates degrade to the empty string rather\n# than dying mid-render, so a count <= 0 returns \"\" (Go's `bf_repeat`\n# applies the same clamp). The count is truncated toward zero\n# (`int`), matching JS's ToIntegerOrInfinity on `\"a\".repeat(3.7)`.\n\nsub repeat ($self, $recv, $count) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $n = defined $count ? int($count) : 0;\n return $n <= 0 ? '' : $s x $n;\n}\n\n# `String.prototype.padStart` / `padEnd` (#1448 Tier B) \u2014 pad the\n# receiver to `$target` characters with `$pad` (default a single space)\n# repeated and truncated to fill, prepended or appended. Length is\n# measured in characters (Perl `length`), matching Go's rune-based\n# `bf_pad_*` \u2014 diverges from JS's UTF-16-unit length only for\n# astral-plane input. An empty pad, or a receiver already >= `$target`,\n# returns the receiver unchanged (JS parity). The `$target` is\n# truncated toward zero (JS ToLength on the first arg).\n\nsub _pad ($s, $target, $pad, $at_start) {\n $pad = ' ' unless defined $pad;\n $pad = \"$pad\";\n return $s if $pad eq '';\n my $len = CORE::length $s;\n my $t = int($target // 0);\n return $s if $len >= $t;\n my $need = $t - $len;\n # Repeat enough copies to cover $need, then trim to exactly $need.\n my $fill = substr($pad x (int($need / CORE::length($pad)) + 1), 0, $need);\n return $at_start ? $fill . $s : $s . $fill;\n}\n\nsub pad_start ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 1);\n}\n\nsub pad_end ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 0);\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref. The compiler emits a `keys` list of per-key hashes\n# in priority order; each hash carries:\n#\n# key_kind => 'self' | 'field'\n# key => '' when key_kind eq 'self'; field name verbatim\n# from the comparator AST (e.g. 'price', 'createdAt')\n# when key_kind eq 'field' \u2014 no case normalisation\n# applied. Perl hash lookups are case-sensitive so\n# the key here must match the actual hash key the\n# user populated.\n# compare_type => 'numeric' | 'string' | 'auto'\n# direction => 'asc' | 'desc'\n#\n# Accepted comparator catalogue (gated upstream at parse time \u2014\n# anything outside refuses with BF101 before reaching this helper):\n#\n# (a,b) => a.f - b.f \u2192 field, numeric\n# (a,b) => a - b \u2192 self, numeric\n# (a,b) => a[.f].localeCompare(b[.f]) \u2192 field|self, string\n# (a,b) => a.f > b.f ? 1 : -1 \u2192 field|self, auto\n# any of the above ||-chained \u2192 multi-key tie-breaks\n# (and reversed-operand variants for `desc`).\n#\n# `auto` (relational-ternary lowering) compares numerically when both\n# keys `looks_like_number`, else lexically \u2014 Go's `bf_sort` applies the\n# same rule so the two template adapters stay byte-equal.\n#\n# A future `nulls => 'first' | 'last'` knob can land per key without\n# churn \u2014 the opts hash is the right place to grow.\n\nsub sort ($self, $recv, $opts = {}) {\n return [] unless ref($recv) eq 'ARRAY';\n\n # Normalise the per-key specs (priority order, length >= 1).\n my @spec = map {\n {\n key_kind => $_->{key_kind} // 'self',\n key => $_->{key} // '',\n compare_type => $_->{compare_type} // 'numeric',\n direction => $_->{direction} // 'asc',\n }\n } @{ $opts->{keys} // [] };\n return [ @$recv ] unless @spec;\n\n # Schwartzian transform: project each item to all its sort keys\n # once, then compare projected keys. Cheaper than re-resolving the\n # field accessors inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my @ks = map {\n $_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;\n } @spec;\n [ \\@ks, $item ];\n } @$recv;\n\n my $cmp = sub {\n for my $i (0 .. $#spec) {\n my $sp = $spec[$i];\n my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});\n next if $c == 0; # tie on this key \u2014 try the next\n return $sp->{direction} eq 'desc' ? -$c : $c;\n }\n return 0;\n };\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\n}\n\n# Compare two projected keys, ascending orientation (-1 / 0 / 1); the\n# caller negates for 'desc'. 'auto' compares numerically when both\n# keys look like numbers, else lexically (matches Go's `bf_sort`).\n# undef coalesces to '' / 0 so the order stays total without warnings.\nsub _compare_sort_key ($av, $bv, $compare_type) {\n if ($compare_type eq 'string') {\n return ($av // '') cmp ($bv // '');\n }\n if ($compare_type eq 'auto') {\n if (looks_like_number($av // '') && looks_like_number($bv // '')) {\n return ($av // 0) <=> ($bv // 0);\n }\n return ($av // '') cmp ($bv // '');\n }\n return ($av // 0) <=> ($bv // 0); # numeric\n}\n\n# Fold an array into a scalar via the arithmetic-fold catalogue\n# (#1448 Tier C). Mirrors Go's `bf_reduce` and JS `reduce(fn, init)` /\n# `reduceRight(fn, init)` for the shapes `(acc, x) => acc <op> x` /\n# `(acc, x) => acc <op> x.field`:\n#\n# bf->reduce($recv, {\n# op => '+' | '*',\n# key_kind => 'self' | 'field',\n# key => '<field>', # when key_kind eq 'field'\n# type => 'numeric' | 'string',\n# init => <seed>, # number, or string for concat\n# direction => 'left' | 'right', # 'right' = reduceRight (default 'left')\n# })\n#\n# Numeric folds accumulate with `+` / `*` (non-numeric keys coalesce to\n# 0); string folds concatenate via `bf->string` (undef \u2192 ''). The init\n# seeds the accumulator, so an empty array returns it unchanged \u2014 exactly\n# like JS. `direction => 'right'` folds right-to-left (reduceRight); only\n# observable for string concat, since numeric sum / product commute.\n# Float stringification can diverge from Go's for inexact binary\n# fractions (e.g. 0.1 + 0.2); integer sums \u2014 the common case \u2014 agree.\nsub reduce ($self, $recv, $opts = {}) {\n my $op = $opts->{op} // '+';\n my $key_kind = $opts->{key_kind} // 'self';\n my $key = $opts->{key} // '';\n my $type = $opts->{type} // 'numeric';\n my $direction = $opts->{direction} // 'left';\n\n my @items = ref($recv) eq 'ARRAY' ? @$recv : ();\n # reduceRight folds right-to-left; reversing the snapshot keeps the\n # single forward loop below. Only observable for string concat \u2014\n # numeric sum / product commute. Qualify as CORE::reverse \u2014 this\n # package defines `sub reverse` (the `.reverse()` helper), so a bare\n # `reverse` is ambiguous under `use warnings`.\n @items = CORE::reverse(@items) if $direction eq 'right';\n my $project = sub ($item) {\n $key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;\n };\n\n if ($type eq 'string') {\n my $acc = $opts->{init} // '';\n $acc .= $self->string($project->($_)) for @items;\n return $acc;\n }\n\n my $acc = $opts->{init} // 0;\n for my $item (@items) {\n my $n = $project->($item);\n # Guard `defined` before `looks_like_number` so a missing field\n # (undef) folds as 0 without an \"uninitialized value\" warning\n # under `use warnings` \u2014 matching the `$av // ''` style `sort` uses.\n $n = 0 unless defined $n && looks_like_number($n);\n $op eq '*' ? ($acc *= $n) : ($acc += $n);\n }\n return $acc;\n}\n\n# ---------------------------------------------------------------------------\n# JSX intrinsic-element spread (#1407)\n# ---------------------------------------------------------------------------\n#\n# Mirrors the JS `spreadAttrs` runtime\n# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's\n# `bf.SpreadAttrs` so SSR output stays byte-equal across the three\n# adapters. Generated Mojo templates invoke this as\n# `<%== bf->spread_attrs($bag) %>`.\n#\n# Skip rules: nil/false values, event handlers (`on[A-Z]\u2026` shape\n# matching JS `key[2] === key[2].toUpperCase()` \u2014 true for any\n# character whose uppercase is itself, including digits and\n# underscore), `children`. `ref` is intentionally NOT filtered,\n# matching the JS reference.\n#\n# Key remap: className \u2192 class, htmlFor \u2192 for; SVG camelCase\n# attrs preserved (case-sensitive XML spec); other camelCase keys\n# lowered to kebab-case with a leading `-` for an initial\n# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`).\n#\n# `style` is routed through `_style_to_css` so object literals\n# serialise to a real CSS string instead of Perl's default\n# `HASH(0x...)` form.\n#\n# Output is deterministic: keys are sorted alphabetically before\n# emission, matching the Go adapter's `sort.Strings(keys)` policy\n# and Mojo::JSON's marshal order.\n#\n# The return value is a Mojo::ByteStream so the calling template's\n# `<%==` raw-emit skips re-escaping (the helper has already\n# HTML-escaped each value).\n\nmy %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw(\n allowReorder attributeName attributeType autoReverse\n baseFrequency baseProfile calcMode clipPathUnits\n contentScriptType contentStyleType diffuseConstant edgeMode\n externalResourcesRequired filterRes filterUnits glyphRef\n gradientTransform gradientUnits kernelMatrix kernelUnitLength\n keyPoints keySplines keyTimes lengthAdjust limitingConeAngle\n markerHeight markerUnits markerWidth maskContentUnits\n maskUnits numOctaves pathLength patternContentUnits\n patternTransform patternUnits pointsAtX pointsAtY pointsAtZ\n preserveAlpha preserveAspectRatio primitiveUnits refX refY\n repeatCount repeatDur requiredExtensions requiredFeatures\n specularConstant specularExponent spreadMethod startOffset\n stdDeviation stitchTiles surfaceScale systemLanguage\n tableValues targetX targetY textLength viewBox viewTarget\n xChannelSelector yChannelSelector zoomAndPan\n);\n\nsub _to_attr_name ($key) {\n return 'class' if $key eq 'className';\n return 'for' if $key eq 'htmlFor';\n return $key if $SVG_CAMEL_CASE_ATTRS{$key};\n # camelCase \u2192 kebab-case, with a leading `-` for an initial\n # uppercase letter (JS-reference parity, even though that case\n # produces an HTML-invalid attribute name \u2014 same documented\n # behaviour as the Go adapter's `toAttrName`).\n my $out = $key;\n $out =~ s/([A-Z])/-\\L$1/g;\n return $out;\n}\n\nsub _html_escape ($value) {\n # HTML attribute-value escape for SSR string emission. The\n # spread bag's values reach the browser as part of a generated\n # `key=\"...\"` substring inside the rendered HTML, so the\n # escape set has to cover everything that could break either\n # the surrounding double-quoted attribute or the enclosing\n # tag: `&`, `<`, `>`, `\"`, and `'`. Matches Go's\n # `template.HTMLEscapeString` semantics byte-for-byte (using\n # `&#34;` / `&#39;` for quotes rather than the named entities)\n # so the SSR output is identical across the Go and Mojo\n # adapters (#1407, #1413 review). The CSR-side\n # `applyRestAttrs` calls `el.setAttribute(name, String(value))`\n # \u2014 which does its own DOM-level escaping in the browser \u2014\n # so JS doesn't need an explicit escape pass; Perl/Go emit a\n # string, so we do.\n my $s = defined $value ? \"$value\" : '';\n $s =~ s/&/&amp;/g;\n $s =~ s/</&lt;/g;\n $s =~ s/>/&gt;/g;\n $s =~ s/\"/&#34;/g;\n $s =~ s/'/&#39;/g;\n return $s;\n}\n\nsub _style_to_css ($value) {\n return undef unless defined $value;\n # Non-hashref values pass through stringified \u2014 matches the JS\n # `typeof value !== 'object'` branch in `styleToCss`.\n if (ref($value) ne 'HASH') {\n my $s = \"$value\";\n return CORE::length $s ? $s : undef;\n }\n my @parts;\n for my $key (sort keys %$value) {\n my $v = $value->{$key};\n next unless defined $v;\n my $prop = $key;\n $prop =~ s/([A-Z])/-\\L$1/g;\n push @parts, \"$prop:$v\";\n }\n return @parts ? CORE::join(';', @parts) : undef;\n}\n\nsub spread_attrs ($self, $bag) {\n return '' unless defined $bag && ref($bag) eq 'HASH';\n my @parts;\n for my $key (sort keys %$bag) {\n # Event handlers: skip when key starts `on` and the third\n # character is its own uppercase form (uppercase letter,\n # digit, underscore, \u2026). Mirrors the JS predicate.\n if (CORE::length($key) > 2 && substr($key, 0, 2) eq 'on') {\n my $c = substr($key, 2, 1);\n next if CORE::uc($c) eq $c;\n }\n next if $key eq 'children';\n my $val = $bag->{$key};\n # null / undef \u2192 drop.\n next unless defined $val;\n # Boolean values arrive as Mojo::JSON sentinel objects\n # (`Mojo::JSON::true` / `false`) \u2014 both from JSON-deserialised\n # props and from the test harness's `toPerlLiteral`\n # (which emits the sentinels rather than plain 0/1 to avoid\n # conflating booleans with numeric attribute values like\n # `tabindex=\"0\"`). The contract is: callers MUST use the\n # sentinels for boolean values; plain Perl scalars 0/1\n # render as numeric attribute values, matching how JS\n # `spreadAttrs` treats a `0`/`1` JS number.\n if (ref($val) eq 'JSON::PP::Boolean' || ref($val) eq 'Mojo::JSON::_Bool') {\n next unless $val;\n push @parts, _to_attr_name($key);\n next;\n }\n # `style` routes through `_style_to_css` so object literals\n # serialise to a real CSS string.\n if ($key eq 'style') {\n my $css = _style_to_css($val);\n next unless defined $css && CORE::length $css;\n push @parts, qq{style=\"} . _html_escape($css) . qq{\"};\n next;\n }\n my $name = _to_attr_name($key);\n push @parts, $name . qq{=\"} . _html_escape($val) . qq{\"};\n }\n return '' unless @parts;\n # Mark the result raw so the calling template's `<%==` raw-emit\n # doesn't re-escape the already-escaped values (the Mojo backend\n # returns a Mojo::ByteStream).\n return $self->backend->mark_raw(CORE::join(' ', @parts));\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nBarefootJS - Engine- and framework-agnostic server runtime for BarefootJS marked templates\n\n=head1 SYNOPSIS\n\n use BarefootJS;\n\n # A host injects a rendering backend (see BarefootJS::Backend::Xslate or\n # Mojolicious::Plugin::BarefootJS for shipping backends).\n my $bf = BarefootJS->new($context, { backend => $backend });\n\n # The compiled marked template calls the runtime as a `bf` object:\n # <: $bf.scope_attr() :> <: $bf.json($data) :> <: $bf.spread_attrs($h) :>\n\n=head1 DESCRIPTION\n\nBarefootJS compiles JSX/TSX into a marked template plus client JS. This module\nis the server-side runtime the marked templates call into at render time. It is\ndeliberately template-engine- and web-framework-agnostic: every operation that\ndepends on I<how> a template is rendered \u2014 JSON marshalling, raw-string marking,\nJSX-children materialisation, and named-template rendering \u2014 is delegated to a\npluggable C<backend>.\n\nThat design lets the one runtime drive any backend. Shipping backends:\n\n=over 4\n\n=item * L<BarefootJS::Backend::Xslate> \u2014 Text::Xslate (Kolon); runs under any PSGI/Plack app.\n\n=item * L<BarefootJS::Backend::Mojo> \u2014 Mojolicious (via L<Mojolicious::Plugin::BarefootJS>).\n\n=back\n\nThe core itself pulls in only core Perl modules (C<POSIX>, C<Scalar::Util>);\nno template engine or web framework is loaded unless a backend that needs one\nis used.\n\n=head1 SEE ALSO\n\nL<BarefootJS::Backend::Xslate>, L<Mojolicious::Plugin::BarefootJS>,\nL<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
23568
- barefootBackendMojoPmSource = "package BarefootJS::Backend::Mojo;\nour $VERSION = \"0.9.5\";\nuse Mojo::Base -base, -signatures;\n\nuse Mojo::ByteStream qw(b);\nuse Mojo::JSON qw(to_json);\nuse Scalar::Util qw(weaken);\n\n# ---------------------------------------------------------------------------\n# Reference rendering backend (Mojolicious / Mojo::Template).\n# ---------------------------------------------------------------------------\n#\n# BarefootJS.pm holds all the template-engine-agnostic logic (the JS-compat\n# value helpers, array/string methods, hydration markers). Everything that is\n# specific to *how a template is rendered* \u2014 JSON marshalling, raw-string\n# marking, JSX-children materialisation, and named-template rendering \u2014 lives\n# behind this backend object so the same runtime can drive a different Perl\n# template engine (Text::Xslate, Template Toolkit, \u2026) without rewriting the\n# helper surface.\n#\n# A backend MUST implement:\n# - encode_json($data) -> string\n# - mark_raw($str) -> value the engine emits without escaping\n# - materialize($value) -> string (resolve a captured-children ref)\n# - render_named($name, $bf, \\%vars) -> string\n#\n# This Mojo implementation is the reference. To target another engine, write a\n# sibling backend (BarefootJS::Backend::Xslate, \u2026) implementing the same four\n# methods and pass it via `BarefootJS->new($c, { backend => $b })`.\n\n# The Mojolicious controller. Optional: the value-marshalling helpers\n# (`encode_json` / `mark_raw` / `materialize`) work without it; only\n# `render_named` reaches into the controller's renderer + stash.\nhas 'c';\n\n# Pluggable JSON encoder (#engine-abstraction). Defaults to\n# `Mojo::JSON::to_json`, which returns a *character* string (not bytes)\n# suitable for embedding in HTML output via `<%==` / Mojo::ByteStream.\n#\n# Override with any `sub ($data) { ... }` to swap in a faster XS encoder \u2014\n# e.g. `json_encoder => sub { Cpanel::JSON::XS->new->canonical->encode($_[0]) }`.\n# The pure-Perl JSON::PP fallback Mojo::JSON uses can be a hot spot for large\n# props payloads; the seam lets a host pick its own implementation without\n# touching the runtime.\nhas 'json_encoder' => sub { \\&to_json };\n\n# Hold the controller weakly for the same reason BarefootJS does: the\n# controller owns the bf instance (which owns this backend) via its stash,\n# so a strong back-reference would close a per-request cycle the refcount GC\n# can't reclaim. `render_named` only touches `$self->c` mid-render, while the\n# controller is still alive on the request stack.\nsub new ($class, %args) {\n my $self = $class->SUPER::new(%args);\n weaken($self->{c}) if $self->{c};\n return $self;\n}\n\nsub encode_json ($self, $data) {\n return $self->json_encoder->($data);\n}\n\n# Mark a string as already-safe so the template engine emits it verbatim\n# (no re-escaping). In Mojo this is a Mojo::ByteStream, which the calling\n# template's `<%==` raw-emit passes through unescaped.\nsub mark_raw ($self, $str) {\n return b($str);\n}\n\n# JSX children / async fallbacks arrive via Mojo's `begin %>...<% end`\n# capture, which produces a CODE ref returning a Mojo::ByteStream. Resolve\n# it to a string before embedding. Plain (already-rendered) strings pass\n# through unchanged.\nsub materialize ($self, $value) {\n return ref($value) eq 'CODE' ? $value->() : $value;\n}\n\n# Render a named template with `$child_bf` bound as the active runtime\n# instance for that render. The Mojo `bf` helper resolves the current\n# instance off `$c->stash->{'bf.instance'}`; swap it for the duration of\n# the nested render and restore it afterwards so sibling renders are\n# unaffected.\nsub render_named ($self, $template_name, $child_bf, $vars) {\n my $c = $self->c;\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(template => $template_name, %$vars);\n $c->stash->{'bf.instance'} = $prev;\n return $html;\n}\n\n1;\n";
23569
- barefootPluginPmSource = "package Mojolicious::Plugin::BarefootJS;\nour $VERSION = \"0.9.5\";\nuse Mojo::Base 'Mojolicious::Plugin', -signatures;\n\nuse Mojo::File qw(path);\nuse Mojo::JSON qw(decode_json);\n\nuse BarefootJS;\n\n# Plugin entry point. Wires up:\n#\n# 1. The `bf` controller helper. Lazily instantiates one\n# BarefootJS object per request and stashes it under\n# `bf.instance`.\n#\n# 2. A `before_render` hook that, when the rendered template name\n# matches a top-level component in the build manifest, fills the\n# heavy boilerplate the user previously hand-rolled in `app.pl`:\n# generates the scope id, registers every UI-registry child\n# renderer from the manifest, and seeds the stash with each\n# template variable's static default (issue #1416).\n#\n# Configuration (all optional):\n# - manifest_path: absolute path to the `bf build`-emitted\n# `manifest.json`. Defaults to `<app->home>/dist/templates/manifest.json`.\n# Pass `undef` to disable manifest-driven auto-init entirely; the\n# bf helper is still installed and callers can drive everything\n# manually as before.\nsub register ($self, $app, $config = {}) {\n $app->helper(bf => sub ($c) {\n $c->stash->{'bf.instance'} //= BarefootJS->new($c, $config);\n });\n\n my $manifest = _load_manifest($app, $config);\n return unless $manifest;\n\n # Cache the set of UI-registry slot keys so we can answer\n # \"is this template name a child or a top-level page?\" with a\n # single hash lookup at render time. Top-level entries are\n # everything that isn't `__barefoot__` and doesn't match\n # `ui/<name>/index` \u2014 the same partition `register_components_from_manifest`\n # applies internally.\n my %is_child_entry;\n for my $entry_name (keys %$manifest) {\n next if $entry_name eq '__barefoot__';\n next unless $entry_name =~ m{^ui/[^/]+/index$};\n $is_child_entry{$entry_name} = 1;\n }\n\n $app->hook(before_render => sub ($c, $args) {\n my $template = $args->{template};\n return unless defined $template && length $template;\n my $entry = $manifest->{$template};\n return unless $entry;\n return if $is_child_entry{$template};\n # Idempotency guard for nested renders. A controller might\n # call `render_to_string` inside an action and then `render`\n # \u2014 without this we'd re-init `bf` on the second pass and\n # wipe the script registrations the first pass collected.\n return if $c->stash->{'bf.auto_init_done'};\n\n # Escape hatch for callers that wire `bf` up by hand (the\n # existing `render_component` helper in the showcase app does\n # this). If `_scope_id` is already set we treat the request as\n # \"manually managed\" and leave it alone \u2014 same outcome as\n # before the plugin gained auto-init.\n my $bf = $c->bf;\n if (defined $bf->_scope_id && length $bf->_scope_id) {\n $c->stash->{'bf.auto_init_done'} = 1;\n return;\n }\n $c->stash->{'bf.auto_init_done'} = 1;\n\n $bf->_scope_id($template . '_' . substr(rand() =~ s/^0\\.//r, 0, 6));\n $bf->register_components_from_manifest($manifest);\n\n # Seed each ssrDefault into the stash unless the caller has\n # already supplied a value for that key \u2014 callers always win.\n my $defaults = $entry->{ssrDefaults};\n if (ref($defaults) eq 'HASH') {\n for my $name (keys %$defaults) {\n next if exists $c->stash->{$name};\n my $d = $defaults->{$name};\n my $value = ref($d) eq 'HASH' ? $d->{value} : $d;\n $c->stash->{$name} = $value;\n }\n }\n });\n}\n\nsub _load_manifest ($app, $config) {\n return undef if exists $config->{manifest_path} && !defined $config->{manifest_path};\n my $manifest_path = $config->{manifest_path}\n // $app->home->child('dist/templates/manifest.json');\n my $file = path($manifest_path);\n return undef unless -r $file;\n my $manifest = eval { decode_json($file->slurp) };\n if ($@ || ref($manifest) ne 'HASH') {\n $app->log->warn(\"BarefootJS: cannot parse manifest at $file: $@\") if $@;\n return undef;\n }\n return $manifest;\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nMojolicious::Plugin::BarefootJS - Mojolicious integration for BarefootJS\n\n=head1 SYNOPSIS\n\n # Mojolicious application\n $self->plugin('BarefootJS');\n\n # In a controller / template, the `bf` helper exposes a per-request\n # BarefootJS runtime backed by BarefootJS::Backend::Mojo.\n\n=head1 DESCRIPTION\n\nWires the L<BarefootJS> server runtime into L<Mojolicious>. It registers a\nC<bf> controller helper that lazily instantiates one BarefootJS object per\nrequest (rendering via L<BarefootJS::Backend::Mojo>), and supports rendering\ncompiled marked templates as Mojolicious templates.\n\nFor non-Mojolicious / PSGI hosts, see L<BarefootJS::Backend::Xslate>, which\ndrives the same runtime with Text::Xslate and no web framework.\n\n=head1 METHODS\n\nL<Mojolicious::Plugin::BarefootJS> inherits all methods from\nL<Mojolicious::Plugin> and implements the following new one.\n\n=head2 register\n\n $plugin->register(Mojolicious->new, \\%conf);\n\nRegisters the plugin (the C<bf> helper and supporting hooks) in a Mojolicious\napplication.\n\n=head1 SEE ALSO\n\nL<BarefootJS>, L<BarefootJS::Backend::Mojo>, L<BarefootJS::Backend::Xslate>,\nL<Mojolicious>, L<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
23570
- barefootDevReloadPmSource = `package Mojolicious::Plugin::BarefootJS::DevReload;
23571
- our $VERSION = "0.9.5";
23572
- use Mojo::Base 'Mojolicious::Plugin', -signatures;
23573
-
23574
- =head1 NAME
23575
-
23576
- Mojolicious::Plugin::BarefootJS::DevReload - Dev-only browser auto-reload for BarefootJS apps
23577
-
23578
- =head1 SYNOPSIS
23579
-
23580
- # In your Mojolicious::Lite app (development mode)
23581
- plugin 'BarefootJS::DevReload';
23582
-
23583
- # Then in your layout template, before </body>:
23584
- %== bf_dev_snippet
23585
-
23586
- =head1 DESCRIPTION
23587
-
23588
- Companion to C<barefoot build --watch> in C<@barefootjs/cli>. The CLI drops
23589
- C<< <dist>/.dev/build-id >> after every successful rebuild that changed
23590
- output; this plugin watches that file and streams SSE C<< event: reload >>
23591
- to subscribed browsers so an editor save triggers an automatic reload.
23592
-
23593
- Disabled automatically when C<< $app->mode eq 'production' >> (set via
23594
- C<MOJO_MODE=production>). Pass C<< enabled => 0 >> to disable explicitly or
23595
- C<< enabled => 1 >> to force-enable.
23596
-
23597
- =cut
23598
-
23599
- use Mojo::ByteStream qw(b);
23600
- use Mojo::IOLoop;
23601
- use File::Spec;
23602
- use BarefootJS::DevReload ();
23603
-
23604
- # Engine-agnostic snippet, build-id reading, and timing constants are shared
23605
- # with the PSGI/Plack path in BarefootJS::DevReload \u2014 one source of truth.
23606
- my $HEARTBEAT_S = $BarefootJS::DevReload::HEARTBEAT_S;
23607
- my $POLL_S = $BarefootJS::DevReload::POLL_S;
23608
-
23609
- sub register ($self, $app, $config = {}) {
23610
- my $dist_dir = $config->{dist_dir} // 'dist';
23611
- my $endpoint = $config->{endpoint} // '/_bf/reload';
23612
- my $enabled = exists $config->{enabled}
23613
- ? $config->{enabled}
23614
- : ($app->mode ne 'production');
23615
-
23616
- # Snippet helper is always registered so templates don't have to branch
23617
- # on mode \u2014 it simply returns an empty ByteStream when disabled.
23618
- $app->helper(bf_dev_snippet => sub ($c) {
23619
- return b('') unless $enabled;
23620
- return b(BarefootJS::DevReload->snippet($endpoint));
23621
- });
23622
-
23623
- return unless $enabled;
23624
-
23625
- # Resolve dist_dir relative to the Mojolicious home when not already
23626
- # absolute, so both \`dist_dir => 'dist'\` (the common case) and
23627
- # \`dist_dir => '/abs/path'\` (tests) work.
23628
- my $dist_abs = File::Spec->file_name_is_absolute($dist_dir)
23629
- ? $dist_dir
23630
- : $app->home->child($dist_dir)->to_string;
23631
- BarefootJS::DevReload->ensure_dev_dir($dist_abs);
23632
- my $build_id_path = BarefootJS::DevReload->build_id_path($dist_abs);
23633
-
23634
- $app->routes->get($endpoint => sub ($c) {
23635
- my $last_event_id = $c->req->headers->header('Last-Event-ID') // '';
23636
- $last_event_id =~ s/^\\s+|\\s+$//g;
23637
-
23638
- $c->res->headers->content_type('text/event-stream');
23639
- $c->res->headers->cache_control('no-cache, no-transform');
23640
- $c->res->headers->connection('keep-alive');
23641
- $c->res->headers->header('X-Accel-Buffering' => 'no');
23642
-
23643
- $c->write("retry: 1000\\n\\n");
23644
-
23645
- my $initial_id = BarefootJS::DevReload->read_build_id($build_id_path);
23646
- my $last_sent = '';
23647
- if (length $initial_id) {
23648
- $last_sent = $initial_id;
23649
- # When the client reconnects with a stale Last-Event-ID, a build
23650
- # happened during its disconnected window \u2014 fire \`reload\`
23651
- # immediately so the missed rebuild does not silently stay
23652
- # unpainted until the next change.
23653
- my $event = (length $last_event_id && $last_event_id ne $initial_id)
23654
- ? 'reload' : 'hello';
23655
- $c->write("event: $event\\nid: $initial_id\\ndata: $initial_id\\n\\n");
23656
- }
23657
-
23658
- my ($hb_id, $poll_id);
23659
- $c->on(finish => sub {
23660
- Mojo::IOLoop->remove($hb_id) if $hb_id;
23661
- Mojo::IOLoop->remove($poll_id) if $poll_id;
23662
- });
23663
-
23664
- $hb_id = Mojo::IOLoop->recurring($HEARTBEAT_S => sub {
23665
- $c->write(": hb\\n\\n");
23666
- });
23667
- $poll_id = Mojo::IOLoop->recurring($POLL_S => sub {
23668
- my $id = BarefootJS::DevReload->read_build_id($build_id_path);
23669
- return unless length $id;
23670
- return if $id eq $last_sent;
23671
- $last_sent = $id;
23672
- $c->write("event: reload\\nid: $id\\ndata: $id\\n\\n");
23673
- });
23674
- });
23675
-
23676
- return;
23677
- }
23678
-
23679
- 1;
23680
- `;
23681
23567
  }
23682
23568
  });
23683
23569
 
@@ -25396,7 +25282,6 @@ var init_mojo = __esm({
25396
25282
  "src/lib/adapters/mojo.ts"() {
25397
25283
  "use strict";
25398
25284
  init_shared2();
25399
- init_runtimes_generated();
25400
25285
  MOJO_BAREFOOT_CONFIG_TS = `import { createConfig } from '@barefootjs/mojolicious/build'
25401
25286
 
25402
25287
  export default createConfig({
@@ -25415,10 +25300,9 @@ export default createConfig({
25415
25300
  `;
25416
25301
  MOJO_APP_PL = `#!/usr/bin/env perl
25417
25302
  use Mojolicious::Lite -signatures;
25418
- use lib 'lib';
25419
25303
 
25420
- # Load the BarefootJS plugin (vendored under ./lib so the app runs
25421
- # without a CPAN release of the plugin yet).
25304
+ # Load the BarefootJS plugin (installed from CPAN \u2014 see cpanfile).
25305
+ # Provides the \`bf\` helper + manifest-driven child rendering.
25422
25306
  plugin 'BarefootJS';
25423
25307
 
25424
25308
  # Dev-only browser auto-reload over SSE. The plugin polls
@@ -25487,7 +25371,10 @@ __DATA__
25487
25371
  </html>
25488
25372
  `;
25489
25373
  MOJO_CPANFILE = `# Required Perl deps. Install with: cpanm --installdeps .
25490
- requires 'Mojolicious', '>= 9.34';
25374
+ requires 'perl', '5.020';
25375
+ requires 'BarefootJS', '0.9.6';
25376
+ requires 'Mojolicious::Plugin::BarefootJS', '0.9.6';
25377
+ requires 'Mojolicious', '9.0';
25491
25378
  `;
25492
25379
  MOJO_TSCONFIG = `{
25493
25380
  "compilerOptions": {
@@ -25508,7 +25395,7 @@ requires 'Mojolicious', '>= 9.34';
25508
25395
  }
25509
25396
  },
25510
25397
  "include": ["**/*.ts", "**/*.tsx"],
25511
- "exclude": ["node_modules", "dist", "lib"]
25398
+ "exclude": ["node_modules", "dist"]
25512
25399
  }
25513
25400
  `;
25514
25401
  MOJO_GITIGNORE = buildGitignore([
@@ -25527,10 +25414,6 @@ requires 'Mojolicious', '>= 9.34';
25527
25414
  files: {
25528
25415
  "app.pl": MOJO_APP_PL,
25529
25416
  "cpanfile": MOJO_CPANFILE,
25530
- "lib/BarefootJS.pm": barefootPmSource,
25531
- "lib/BarefootJS/Backend/Mojo.pm": barefootBackendMojoPmSource,
25532
- "lib/Mojolicious/Plugin/BarefootJS.pm": barefootPluginPmSource,
25533
- "lib/Mojolicious/Plugin/BarefootJS/DevReload.pm": barefootDevReloadPmSource,
25534
25417
  "barefoot.config.ts": MOJO_BAREFOOT_CONFIG_TS,
25535
25418
  "tsconfig.json": MOJO_TSCONFIG,
25536
25419
  "uno.config.ts": unoConfigTs([
@@ -25722,6 +25605,344 @@ replace github.com/barefootjs/runtime/bf => ./bf-runtime
25722
25605
  }
25723
25606
  });
25724
25607
 
25608
+ // src/lib/adapters/xslate.ts
25609
+ import { execSync as execSync4 } from "node:child_process";
25610
+ function perlPrereqs2() {
25611
+ const warnings = [];
25612
+ try {
25613
+ execSync4("perl --version", { stdio: "ignore" });
25614
+ } catch {
25615
+ warnings.push("Perl not found on PATH. Install Perl 5.20+ before starting the dev server.");
25616
+ }
25617
+ try {
25618
+ execSync4("perl -MText::Xslate -e1", { stdio: "ignore" });
25619
+ } catch {
25620
+ warnings.push(
25621
+ "Text::Xslate not installed. Run `cpanm --installdeps .` before starting the dev server."
25622
+ );
25623
+ }
25624
+ try {
25625
+ execSync4("perl -MPlack -e1", { stdio: "ignore" });
25626
+ } catch {
25627
+ warnings.push(
25628
+ "Plack not installed (provides `plackup`). Run `cpanm --installdeps .` before starting the dev server."
25629
+ );
25630
+ }
25631
+ try {
25632
+ execSync4("perl -MStarman -e1", { stdio: "ignore" });
25633
+ } catch {
25634
+ warnings.push(
25635
+ "Starman not installed (the dev server). Run `cpanm --installdeps .` before starting the dev server."
25636
+ );
25637
+ }
25638
+ return warnings;
25639
+ }
25640
+ var XSLATE_BAREFOOT_CONFIG_TS, XSLATE_APP_PSGI, XSLATE_CPANFILE, XSLATE_TSCONFIG, XSLATE_COUNTER_TSX, XSLATE_COUNTER_TEST_TSX, XSLATE_GITIGNORE, XSLATE_PORT, XSLATE_ADAPTER;
25641
+ var init_xslate = __esm({
25642
+ "src/lib/adapters/xslate.ts"() {
25643
+ "use strict";
25644
+ init_shared2();
25645
+ XSLATE_BAREFOOT_CONFIG_TS = `import { createConfig } from '@barefootjs/xslate/build'
25646
+
25647
+ export default createConfig({
25648
+ paths: {
25649
+ components: 'components/ui',
25650
+ tokens: 'tokens',
25651
+ meta: 'meta',
25652
+ },
25653
+ components: ['components'],
25654
+ outDir: 'dist',
25655
+ adapterOptions: {
25656
+ clientJsBasePath: '/static/components/',
25657
+ barefootJsPath: '/static/components/barefoot.js',
25658
+ },
25659
+ })
25660
+ `;
25661
+ XSLATE_APP_PSGI = `#!/usr/bin/env perl
25662
+ use strict;
25663
+ use warnings;
25664
+ use utf8;
25665
+ use feature 'signatures';
25666
+ no warnings 'experimental::signatures';
25667
+
25668
+ # All three modules ship on CPAN (see cpanfile). BarefootJS::Backend::Xslate
25669
+ # pulls in BarefootJS (the engine-agnostic core + dev-reload runtime) and
25670
+ # Text::Xslate.
25671
+ use Plack::Builder;
25672
+ use Plack::Request;
25673
+ use Plack::App::File;
25674
+ use Encode ();
25675
+ use JSON::PP ();
25676
+
25677
+ use BarefootJS;
25678
+ use BarefootJS::Backend::Xslate;
25679
+ use BarefootJS::DevReload;
25680
+
25681
+ my $DEV = ($ENV{PLACK_ENV} // 'development') ne 'production';
25682
+
25683
+ # Canonical JSON keeps SSR output deterministic (matching the runtime's
25684
+ # sorted-key policy) and round-trips utf8 cleanly.
25685
+ my $J = JSON::PP->new->canonical->allow_nonref->utf8;
25686
+
25687
+ # One Text::Xslate backend renders every component from dist/templates.
25688
+ # In dev the template cache is disabled so \`bf build --watch\` edits render
25689
+ # on the next request without restarting the server.
25690
+ my $backend = BarefootJS::Backend::Xslate->new(
25691
+ path => ['dist/templates'],
25692
+ json_encoder => sub ($data) { $J->encode($data) },
25693
+ xslate_options => { cache => $DEV ? 0 : 1 },
25694
+ );
25695
+
25696
+ sub rand_suffix () { return substr(sprintf('%f', rand()) =~ s/^0\\.//r, 0, 6) }
25697
+
25698
+ # Render a top-level component template and wrap it in the page layout.
25699
+ sub render_component ($component, %stash) {
25700
+ my $bf = BarefootJS->new(undef, { backend => $backend });
25701
+ $bf->_scope_id($component . '_' . rand_suffix());
25702
+ my $body = $backend->render_named($component, $bf, \\%stash);
25703
+ return layout(body => $body, scripts => $bf->scripts);
25704
+ }
25705
+
25706
+ sub layout (%a) {
25707
+ # Dev-only SSE reload subscriber. Self-suppressed in production (the
25708
+ # \`/_bf/reload\` mount below is gated too).
25709
+ my $dev_snippet = $DEV ? BarefootJS::DevReload->snippet('/_bf/reload') : '';
25710
+ return <<"HTML";
25711
+ <!DOCTYPE html>
25712
+ <html lang="en">
25713
+ <head>
25714
+ <meta charset="utf-8">
25715
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
25716
+ <title>BarefootJS app</title>
25717
+ <!-- Link all three sheets so the browser fetches them in parallel.
25718
+ tokens first so its CSS variables exist before any rule uses them. -->
25719
+ <link rel="stylesheet" href="/static/tokens.css">
25720
+ <link rel="stylesheet" href="/static/styles.css">
25721
+ <link rel="stylesheet" href="/static/uno.css">
25722
+ </head>
25723
+ <body>
25724
+ <main>$a{body}</main>
25725
+ $a{scripts}
25726
+ $dev_snippet
25727
+ </body>
25728
+ </html>
25729
+ HTML
25730
+ }
25731
+
25732
+ my $app = sub ($env) {
25733
+ my $req = Plack::Request->new($env);
25734
+ my $path = $req->path_info;
25735
+ if ($req->method eq 'GET' && ($path eq '/' || $path eq '')) {
25736
+ my $html = Encode::encode_utf8(render_component('Counter'));
25737
+ return [200, ['Content-Type' => 'text/html; charset=utf-8'], [$html]];
25738
+ }
25739
+ return [404, ['Content-Type' => 'text/plain'], ['Not Found']];
25740
+ };
25741
+
25742
+ # Mount table. Plack::App::URLMap matches the longest path prefix, so the
25743
+ # nested /static/components mount wins over /static for client bundles.
25744
+ # - /static/components/* -> dist/client/* (clientJsBasePath)
25745
+ # - /static/* -> public/* (handwritten stylesheets)
25746
+ builder {
25747
+ enable 'Plack::Middleware::ContentLength';
25748
+
25749
+ mount '/static/components' => Plack::App::File->new(root => 'dist/client')->to_app;
25750
+ mount '/static' => Plack::App::File->new(root => 'public')->to_app;
25751
+
25752
+ if ($DEV) {
25753
+ mount '/_bf/reload' => BarefootJS::DevReload->to_app(dist_dir => 'dist');
25754
+ }
25755
+
25756
+ mount '/' => $app;
25757
+ };
25758
+ `;
25759
+ XSLATE_CPANFILE = `# Required Perl deps. Install with: cpanm --installdeps .
25760
+ requires 'perl', '5.020';
25761
+ requires 'BarefootJS', '0.9.6';
25762
+ requires 'BarefootJS::Backend::Xslate', '0.9.6';
25763
+ requires 'Text::Xslate', '3.4.0';
25764
+ requires 'Plack';
25765
+ requires 'Starman';
25766
+ `;
25767
+ XSLATE_TSCONFIG = `{
25768
+ "compilerOptions": {
25769
+ "target": "ESNext",
25770
+ "module": "ESNext",
25771
+ "moduleResolution": "bundler",
25772
+ "jsx": "react-jsx",
25773
+ "jsxImportSource": "@barefootjs/jsx",
25774
+ "types": ["node"{{__PM_TYPES_ENTRY__}}],
25775
+ "strict": true,
25776
+ "skipLibCheck": true,
25777
+ "esModuleInterop": true,
25778
+ "resolveJsonModule": true,
25779
+ "noEmit": true,
25780
+ "baseUrl": ".",
25781
+ "paths": {
25782
+ "@/components/*": ["./components/*"]
25783
+ }
25784
+ },
25785
+ "include": ["**/*.ts", "**/*.tsx"],
25786
+ "exclude": ["node_modules", "dist"]
25787
+ }
25788
+ `;
25789
+ XSLATE_COUNTER_TSX = `'use client'
25790
+
25791
+ import { createSignal, createMemo } from '@barefootjs/client'
25792
+
25793
+ interface CounterProps {
25794
+ initial?: number
25795
+ }
25796
+
25797
+ export function Counter(props: CounterProps) {
25798
+ const [count, setCount] = createSignal(props.initial ?? 0)
25799
+ const doubled = createMemo(() => count() * 2)
25800
+
25801
+ return (
25802
+ <div className="counter">
25803
+ <p className="counter-value">count: {count()}</p>
25804
+ <p className="counter-doubled">doubled: {doubled()}</p>
25805
+ <div className="counter-buttons">
25806
+ <button
25807
+ className="px-4 py-2 rounded-md bg-primary text-primary-foreground"
25808
+ onClick={() => setCount((n) => n + 1)}
25809
+ >
25810
+ +1
25811
+ </button>
25812
+ <button
25813
+ className="px-4 py-2 rounded-md bg-secondary text-secondary-foreground"
25814
+ onClick={() => setCount((n) => n - 1)}
25815
+ >
25816
+ -1
25817
+ </button>
25818
+ <button
25819
+ className="px-4 py-2 rounded-md bg-muted text-muted-foreground"
25820
+ onClick={() => setCount(0)}
25821
+ >
25822
+ Reset
25823
+ </button>
25824
+ </div>
25825
+ </div>
25826
+ )
25827
+ }
25828
+ `;
25829
+ XSLATE_COUNTER_TEST_TSX = `import { describe, test, expect } from '{{__TEST_RUNNER_IMPORT__}}'
25830
+ import { readFileSync } from 'fs'
25831
+ import { resolve } from 'path'
25832
+ import { renderToTest } from '@barefootjs/test'
25833
+
25834
+ const CounterSource = readFileSync(resolve(__dirname, 'Counter.tsx'), 'utf-8')
25835
+
25836
+ describe('Counter', () => {
25837
+ const result = renderToTest(CounterSource, 'Counter.tsx')
25838
+
25839
+ test('has no compiler errors', () => {
25840
+ expect(result.errors).toEqual([])
25841
+ })
25842
+
25843
+ test('componentName is Counter', () => {
25844
+ expect(result.componentName).toBe('Counter')
25845
+ })
25846
+
25847
+ test('has expected signals', () => {
25848
+ expect(result.signals).toContain('count')
25849
+ })
25850
+
25851
+ test('renders as <div>', () => {
25852
+ expect(result.root.tag).toBe('div')
25853
+ })
25854
+
25855
+ test('has event handlers', () => {
25856
+ const all = result.findAll({})
25857
+ expect(
25858
+ all.some((n) => n.events.includes('click') || n.props['onClick'] != null),
25859
+ ).toBe(true)
25860
+ })
25861
+
25862
+ test('renders native <button> controls', () => {
25863
+ const all = result.findAll({})
25864
+ expect(all.some((n) => n.tag === 'button')).toBe(true)
25865
+ })
25866
+
25867
+ test('toStructure() shows expected tree', () => {
25868
+ const structure = result.toStructure()
25869
+ expect(structure.length).toBeGreaterThan(0)
25870
+ expect(structure).toContain('div')
25871
+ })
25872
+ })
25873
+ `;
25874
+ XSLATE_GITIGNORE = buildGitignore([
25875
+ {
25876
+ heading: "bf build outputs (regenerated by `bf build` / `bf build --watch`)",
25877
+ entries: ["dist/"]
25878
+ },
25879
+ {
25880
+ heading: "Perl dependencies + runtime scratch",
25881
+ entries: ["local/", "log/", "*.tmp"]
25882
+ }
25883
+ ]);
25884
+ XSLATE_PORT = 3003;
25885
+ XSLATE_ADAPTER = {
25886
+ label: "Text::Xslate (Perl, Plack/PSGI SSR)",
25887
+ port: XSLATE_PORT,
25888
+ files: {
25889
+ "app.psgi": XSLATE_APP_PSGI,
25890
+ "cpanfile": XSLATE_CPANFILE,
25891
+ "barefoot.config.ts": XSLATE_BAREFOOT_CONFIG_TS,
25892
+ "tsconfig.json": XSLATE_TSCONFIG,
25893
+ "uno.config.ts": unoConfigTs([
25894
+ "components/**/*.tsx",
25895
+ "dist/components/**/*.tsx"
25896
+ ]),
25897
+ "components/Counter.tsx": XSLATE_COUNTER_TSX,
25898
+ "components/Counter.test.tsx": XSLATE_COUNTER_TEST_TSX,
25899
+ "public/styles.css": STYLES_CSS,
25900
+ "public/tokens.css": TOKENS_CSS,
25901
+ "public/uno.css": UNO_CSS_PLACEHOLDER,
25902
+ "dist/components/manifest.json": COMPONENTS_MANIFEST_SEED,
25903
+ ".gitignore": XSLATE_GITIGNORE
25904
+ },
25905
+ scripts: {
25906
+ // Watchers + Starman side-by-side. The build/uno watchers do their own
25907
+ // initial build at startup, so no separate cold-build prefix is needed.
25908
+ // Starman (not plackup's default single-process server) so the
25909
+ // dev-reload SSE endpoint can stream while requests are served.
25910
+ dev: `concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "plackup -s Starman --workers 5 -p ${XSLATE_PORT} app.psgi"`,
25911
+ build: "bf build && unocss",
25912
+ start: `PLACK_ENV=production plackup -s Starman --workers 5 -p ${XSLATE_PORT} app.psgi`
25913
+ },
25914
+ dependencies: {
25915
+ "@barefootjs/client": "latest",
25916
+ "@barefootjs/xslate": "latest",
25917
+ "@barefootjs/jsx": "latest",
25918
+ "@barefootjs/shared": "latest"
25919
+ },
25920
+ devDependencies: {
25921
+ ...UNOCSS_DEV_DEPENDENCIES,
25922
+ "@barefootjs/cli": "latest",
25923
+ "@barefootjs/test": "latest",
25924
+ concurrently: "^9.0.0",
25925
+ typescript: "^5.6.0"
25926
+ },
25927
+ // The starter Counter uses native <button> elements, not the registry
25928
+ // <Button>, so no registry component needs to be fetched at init. See
25929
+ // XSLATE_COUNTER_TSX for why manifest-driven child rendering isn't on
25930
+ // the Xslate path yet.
25931
+ bundledRegistryComponents: [],
25932
+ prereqWarnings: () => perlPrereqs2(),
25933
+ // Text::Xslate / Plack / Starman are Perl dependencies, not npm ones —
25934
+ // point the user at the cpanfile so they don't trip over a missing
25935
+ // `plackup` after `npm install`.
25936
+ extraSetupSteps: [
25937
+ {
25938
+ label: "Install Perl deps for the Text::Xslate runtime (see cpanfile):",
25939
+ command: "cpanm --installdeps ."
25940
+ }
25941
+ ]
25942
+ };
25943
+ }
25944
+ });
25945
+
25725
25946
  // src/lib/templates.ts
25726
25947
  var CSS_LIBRARIES, DEFAULT_CSS_LIBRARY, ADAPTERS, DEFAULT_ADAPTER;
25727
25948
  var init_templates = __esm({
@@ -25735,6 +25956,7 @@ var init_templates = __esm({
25735
25956
  init_hono_node();
25736
25957
  init_mojo();
25737
25958
  init_nethttp();
25959
+ init_xslate();
25738
25960
  CSS_LIBRARIES = {
25739
25961
  unocss: { label: "UnoCSS" }
25740
25962
  };
@@ -25747,6 +25969,7 @@ var init_templates = __esm({
25747
25969
  chi: CHI_ADAPTER,
25748
25970
  nethttp: NETHTTP_ADAPTER,
25749
25971
  mojo: MOJO_ADAPTER,
25972
+ xslate: XSLATE_ADAPTER,
25750
25973
  csr: CSR_ADAPTER
25751
25974
  };
25752
25975
  DEFAULT_ADAPTER = "hono";
@@ -25938,27 +26161,30 @@ async function run4(args2, ctx2) {
25938
26161
  const adapter = ADAPTERS[adapterId];
25939
26162
  const cssId = await resolveCssLibrary(flags.css);
25940
26163
  const cssLibrary = CSS_LIBRARIES[cssId];
25941
- const registryHost = new URL(DEFAULT_REGISTRY_URL2).host;
25942
- const probeSpinner = startSpinner({
25943
- text: `Fetching starter components from ${registryHost}...`
25944
- });
25945
- try {
25946
- await probeRegistry(DEFAULT_REGISTRY_URL2);
25947
- probeSpinner.stop();
25948
- } catch (err) {
25949
- probeSpinner.fail(`Cannot reach ${registryHost} (BarefootJS UI registry)`);
25950
- const msg = err instanceof Error ? err.message : String(err);
25951
- console.error(` ${msg}`);
25952
- console.error(``);
25953
- console.error(`Project init pulls the starter's Button component from`);
25954
- console.error(`${DEFAULT_REGISTRY_URL2} (which the renderer wires through UnoCSS).`);
25955
- console.error(``);
25956
- console.error(`Things to try:`);
25957
- console.error(` 1. Open ${DEFAULT_REGISTRY_URL2}button.json in a browser to`);
25958
- console.error(` confirm ${registryHost} is reachable from this network.`);
25959
- console.error(` 2. If you're behind a corporate proxy, set HTTPS_PROXY.`);
25960
- console.error(` 3. Re-run the create-barefootjs command once you're connected.`);
25961
- process.exit(1);
26164
+ const bundledComponents = adapter.bundledRegistryComponents ?? ["button"];
26165
+ if (bundledComponents.length > 0) {
26166
+ const registryHost = new URL(DEFAULT_REGISTRY_URL2).host;
26167
+ const probeSpinner = startSpinner({
26168
+ text: `Fetching starter components from ${registryHost}...`
26169
+ });
26170
+ try {
26171
+ await probeRegistry(DEFAULT_REGISTRY_URL2);
26172
+ probeSpinner.stop();
26173
+ } catch (err) {
26174
+ probeSpinner.fail(`Cannot reach ${registryHost} (BarefootJS UI registry)`);
26175
+ const msg = err instanceof Error ? err.message : String(err);
26176
+ console.error(` ${msg}`);
26177
+ console.error(``);
26178
+ console.error(`Project init pulls the starter's Button component from`);
26179
+ console.error(`${DEFAULT_REGISTRY_URL2} (which the renderer wires through UnoCSS).`);
26180
+ console.error(``);
26181
+ console.error(`Things to try:`);
26182
+ console.error(` 1. Open ${DEFAULT_REGISTRY_URL2}button.json in a browser to`);
26183
+ console.error(` confirm ${registryHost} is reachable from this network.`);
26184
+ console.error(` 2. If you're behind a corporate proxy, set HTTPS_PROXY.`);
26185
+ console.error(` 3. Re-run the create-barefootjs command once you're connected.`);
26186
+ process.exit(1);
26187
+ }
25962
26188
  }
25963
26189
  const warnings = adapter.prereqWarnings();
25964
26190
  for (const w of warnings) console.warn(` ! ${w}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/cli",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "description": "CLI for agent-driven UI component discovery and scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "typescript": "^5.0.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@barefootjs/jsx": "0.9.6",
34
+ "@barefootjs/jsx": "0.10.0",
35
35
  "@types/node": "^22.0.0"
36
36
  }
37
37
  }