@barefootjs/mojolicious 0.6.1 → 0.8.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.
- package/dist/adapter/index.js +116 -0
- package/dist/adapter/mojo-adapter.d.ts +80 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +116 -0
- package/dist/index.js +116 -0
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +91 -0
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +11 -51
- package/lib/Mojolicious/Plugin/BarefootJS.pm +56 -0
- package/package.json +7 -6
- package/src/__tests__/mojo-adapter.test.ts +124 -10
- package/src/adapter/mojo-adapter.ts +258 -1
- package/src/test-render.ts +30 -3
- package/lib/BarefootJS.pm +0 -1060
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapter/mojo-adapter.ts
|
|
2
|
+
import ts from "typescript";
|
|
2
3
|
import {
|
|
3
4
|
BaseAdapter,
|
|
4
5
|
isBooleanAttr,
|
|
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
|
|
|
80
81
|
return [];
|
|
81
82
|
return prop.value.children;
|
|
82
83
|
}
|
|
84
|
+
function parsePureStringLiteral(source) {
|
|
85
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
86
|
+
const stmt = sf.statements[0];
|
|
87
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
88
|
+
return null;
|
|
89
|
+
const decl = stmt.declarationList.declarations[0];
|
|
90
|
+
let init = decl?.initializer;
|
|
91
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
92
|
+
init = init.expression;
|
|
93
|
+
if (!init)
|
|
94
|
+
return null;
|
|
95
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
96
|
+
return init.text;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
101
|
class MojoAdapter extends BaseAdapter {
|
|
85
102
|
name = "mojolicious";
|
|
@@ -94,6 +111,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
94
111
|
propsObjectName = null;
|
|
95
112
|
propsParams = [];
|
|
96
113
|
stringValueNames = new Set;
|
|
114
|
+
moduleStringConsts = new Map;
|
|
115
|
+
loopBoundNames = new Map;
|
|
116
|
+
nullableOptionalProps = new Set;
|
|
97
117
|
constructor(options = {}) {
|
|
98
118
|
super();
|
|
99
119
|
this.options = {
|
|
@@ -105,6 +125,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
105
125
|
this.componentName = ir.metadata.componentName;
|
|
106
126
|
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
107
127
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
128
|
+
this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
108
129
|
this.stringValueNames = new Set;
|
|
109
130
|
for (const s of ir.metadata.signals) {
|
|
110
131
|
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
@@ -115,6 +136,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
115
136
|
if (isStringTypeInfo(p.type))
|
|
116
137
|
this.stringValueNames.add(p.name);
|
|
117
138
|
}
|
|
139
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
140
|
+
this.loopBoundNames.clear();
|
|
118
141
|
this.errors = [];
|
|
119
142
|
this.childrenCaptureCounter = 0;
|
|
120
143
|
if (!options?.siblingTemplatesRegistered) {
|
|
@@ -139,6 +162,27 @@ class MojoAdapter extends BaseAdapter {
|
|
|
139
162
|
extension: this.extension
|
|
140
163
|
};
|
|
141
164
|
}
|
|
165
|
+
collectModuleStringConsts(constants) {
|
|
166
|
+
const map = new Map;
|
|
167
|
+
for (const c of constants ?? []) {
|
|
168
|
+
if (!c.isModule)
|
|
169
|
+
continue;
|
|
170
|
+
if (c.value === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
const literal = parsePureStringLiteral(c.value);
|
|
173
|
+
if (literal !== null)
|
|
174
|
+
map.set(c.name, literal);
|
|
175
|
+
}
|
|
176
|
+
return map;
|
|
177
|
+
}
|
|
178
|
+
resolveModuleStringConst(name) {
|
|
179
|
+
if (this.loopBoundNames.has(name))
|
|
180
|
+
return null;
|
|
181
|
+
const value = this.moduleStringConsts.get(name);
|
|
182
|
+
if (value === undefined)
|
|
183
|
+
return null;
|
|
184
|
+
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
185
|
+
}
|
|
142
186
|
generateScriptRegistrations(ir, scriptBaseName) {
|
|
143
187
|
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
144
188
|
if (!hasInteractivity)
|
|
@@ -405,6 +449,10 @@ ${whenTrue}
|
|
|
405
449
|
}
|
|
406
450
|
const param = loop.param;
|
|
407
451
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
452
|
+
const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
|
|
453
|
+
for (const n of loopBound) {
|
|
454
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
455
|
+
}
|
|
408
456
|
const prevInLoop = this.inLoop;
|
|
409
457
|
this.inLoop = true;
|
|
410
458
|
const renderedChildren = this.renderChildren(loop.children);
|
|
@@ -438,6 +486,13 @@ ${renderedChildren}` : renderedChildren;
|
|
|
438
486
|
} else {
|
|
439
487
|
lines.push(children);
|
|
440
488
|
}
|
|
489
|
+
for (const n of loopBound) {
|
|
490
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
491
|
+
if (c <= 0)
|
|
492
|
+
this.loopBoundNames.delete(n);
|
|
493
|
+
else
|
|
494
|
+
this.loopBoundNames.set(n, c);
|
|
495
|
+
}
|
|
441
496
|
lines.push(`% }`);
|
|
442
497
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
443
498
|
return lines.join(`
|
|
@@ -529,6 +584,12 @@ ${children}`;
|
|
|
529
584
|
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
530
585
|
return "";
|
|
531
586
|
}
|
|
587
|
+
const bareId = value.expr.trim();
|
|
588
|
+
if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
|
|
589
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
590
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
591
|
+
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
592
|
+
}
|
|
532
593
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
533
594
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
534
595
|
}
|
|
@@ -549,6 +610,10 @@ ${children}`;
|
|
|
549
610
|
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
550
611
|
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
551
612
|
}
|
|
613
|
+
const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
|
|
614
|
+
if (ternaryHashref !== null) {
|
|
615
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
616
|
+
}
|
|
552
617
|
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
553
618
|
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
554
619
|
},
|
|
@@ -713,6 +778,54 @@ ${reason}` : "";
|
|
|
713
778
|
});
|
|
714
779
|
return true;
|
|
715
780
|
}
|
|
781
|
+
conditionalSpreadToPerl(expr) {
|
|
782
|
+
const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
|
|
783
|
+
if (sf.statements.length !== 1)
|
|
784
|
+
return null;
|
|
785
|
+
const stmt = sf.statements[0];
|
|
786
|
+
if (!ts.isExpressionStatement(stmt))
|
|
787
|
+
return null;
|
|
788
|
+
let node = stmt.expression;
|
|
789
|
+
while (ts.isParenthesizedExpression(node))
|
|
790
|
+
node = node.expression;
|
|
791
|
+
if (!ts.isConditionalExpression(node))
|
|
792
|
+
return null;
|
|
793
|
+
const unwrap = (e) => {
|
|
794
|
+
let n = e;
|
|
795
|
+
while (ts.isParenthesizedExpression(n))
|
|
796
|
+
n = n.expression;
|
|
797
|
+
return n;
|
|
798
|
+
};
|
|
799
|
+
const whenTrue = unwrap(node.whenTrue);
|
|
800
|
+
const whenFalse = unwrap(node.whenFalse);
|
|
801
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
|
|
805
|
+
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
|
|
806
|
+
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
|
|
807
|
+
if (truePerl === null || falsePerl === null)
|
|
808
|
+
return null;
|
|
809
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
810
|
+
}
|
|
811
|
+
objectLiteralToPerlHashref(obj, sf) {
|
|
812
|
+
const entries = [];
|
|
813
|
+
for (const prop of obj.properties) {
|
|
814
|
+
if (!ts.isPropertyAssignment(prop))
|
|
815
|
+
return null;
|
|
816
|
+
let key;
|
|
817
|
+
if (ts.isIdentifier(prop.name)) {
|
|
818
|
+
key = prop.name.text;
|
|
819
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
820
|
+
key = prop.name.text;
|
|
821
|
+
} else {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
|
|
825
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
826
|
+
}
|
|
827
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
828
|
+
}
|
|
716
829
|
convertExpressionToPerl(expr) {
|
|
717
830
|
const trimmed = expr.trim();
|
|
718
831
|
if (trimmed === "")
|
|
@@ -1056,6 +1169,9 @@ class MojoTopLevelEmitter {
|
|
|
1056
1169
|
this.adapter = adapter;
|
|
1057
1170
|
}
|
|
1058
1171
|
identifier(name) {
|
|
1172
|
+
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1173
|
+
if (inlined !== null)
|
|
1174
|
+
return inlined;
|
|
1059
1175
|
return `$${name}`;
|
|
1060
1176
|
}
|
|
1061
1177
|
literal(value, literalType) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxE;AAkCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,OAAO,iBAAiB,EAAE,eAAe,CAAA;IAClD,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MjF;AAsOD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package BarefootJS::Backend::Mojo;
|
|
2
|
+
our $VERSION = "0.01";
|
|
3
|
+
use Mojo::Base -base, -signatures;
|
|
4
|
+
|
|
5
|
+
use Mojo::ByteStream qw(b);
|
|
6
|
+
use Mojo::JSON qw(to_json);
|
|
7
|
+
use Scalar::Util qw(weaken);
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# Reference rendering backend (Mojolicious / Mojo::Template).
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
#
|
|
13
|
+
# BarefootJS.pm holds all the template-engine-agnostic logic (the JS-compat
|
|
14
|
+
# value helpers, array/string methods, hydration markers). Everything that is
|
|
15
|
+
# specific to *how a template is rendered* — JSON marshalling, raw-string
|
|
16
|
+
# marking, JSX-children materialisation, and named-template rendering — lives
|
|
17
|
+
# behind this backend object so the same runtime can drive a different Perl
|
|
18
|
+
# template engine (Text::Xslate, Template Toolkit, …) without rewriting the
|
|
19
|
+
# helper surface.
|
|
20
|
+
#
|
|
21
|
+
# A backend MUST implement:
|
|
22
|
+
# - encode_json($data) -> string
|
|
23
|
+
# - mark_raw($str) -> value the engine emits without escaping
|
|
24
|
+
# - materialize($value) -> string (resolve a captured-children ref)
|
|
25
|
+
# - render_named($name, $bf, \%vars) -> string
|
|
26
|
+
#
|
|
27
|
+
# This Mojo implementation is the reference. To target another engine, write a
|
|
28
|
+
# sibling backend (BarefootJS::Backend::Xslate, …) implementing the same four
|
|
29
|
+
# methods and pass it via `BarefootJS->new($c, { backend => $b })`.
|
|
30
|
+
|
|
31
|
+
# The Mojolicious controller. Optional: the value-marshalling helpers
|
|
32
|
+
# (`encode_json` / `mark_raw` / `materialize`) work without it; only
|
|
33
|
+
# `render_named` reaches into the controller's renderer + stash.
|
|
34
|
+
has 'c';
|
|
35
|
+
|
|
36
|
+
# Pluggable JSON encoder (#engine-abstraction). Defaults to
|
|
37
|
+
# `Mojo::JSON::to_json`, which returns a *character* string (not bytes)
|
|
38
|
+
# suitable for embedding in HTML output via `<%==` / Mojo::ByteStream.
|
|
39
|
+
#
|
|
40
|
+
# Override with any `sub ($data) { ... }` to swap in a faster XS encoder —
|
|
41
|
+
# e.g. `json_encoder => sub { Cpanel::JSON::XS->new->canonical->encode($_[0]) }`.
|
|
42
|
+
# The pure-Perl JSON::PP fallback Mojo::JSON uses can be a hot spot for large
|
|
43
|
+
# props payloads; the seam lets a host pick its own implementation without
|
|
44
|
+
# touching the runtime.
|
|
45
|
+
has 'json_encoder' => sub { \&to_json };
|
|
46
|
+
|
|
47
|
+
# Hold the controller weakly for the same reason BarefootJS does: the
|
|
48
|
+
# controller owns the bf instance (which owns this backend) via its stash,
|
|
49
|
+
# so a strong back-reference would close a per-request cycle the refcount GC
|
|
50
|
+
# can't reclaim. `render_named` only touches `$self->c` mid-render, while the
|
|
51
|
+
# controller is still alive on the request stack.
|
|
52
|
+
sub new ($class, %args) {
|
|
53
|
+
my $self = $class->SUPER::new(%args);
|
|
54
|
+
weaken($self->{c}) if $self->{c};
|
|
55
|
+
return $self;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
sub encode_json ($self, $data) {
|
|
59
|
+
return $self->json_encoder->($data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Mark a string as already-safe so the template engine emits it verbatim
|
|
63
|
+
# (no re-escaping). In Mojo this is a Mojo::ByteStream, which the calling
|
|
64
|
+
# template's `<%==` raw-emit passes through unescaped.
|
|
65
|
+
sub mark_raw ($self, $str) {
|
|
66
|
+
return b($str);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# JSX children / async fallbacks arrive via Mojo's `begin %>...<% end`
|
|
70
|
+
# capture, which produces a CODE ref returning a Mojo::ByteStream. Resolve
|
|
71
|
+
# it to a string before embedding. Plain (already-rendered) strings pass
|
|
72
|
+
# through unchanged.
|
|
73
|
+
sub materialize ($self, $value) {
|
|
74
|
+
return ref($value) eq 'CODE' ? $value->() : $value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Render a named template with `$child_bf` bound as the active runtime
|
|
78
|
+
# instance for that render. The Mojo `bf` helper resolves the current
|
|
79
|
+
# instance off `$c->stash->{'bf.instance'}`; swap it for the duration of
|
|
80
|
+
# the nested render and restore it afterwards so sibling renders are
|
|
81
|
+
# unaffected.
|
|
82
|
+
sub render_named ($self, $template_name, $child_bf, $vars) {
|
|
83
|
+
my $c = $self->c;
|
|
84
|
+
my $prev = $c->stash->{'bf.instance'};
|
|
85
|
+
$c->stash->{'bf.instance'} = $child_bf;
|
|
86
|
+
my $html = $c->render_to_string(template => $template_name, %$vars);
|
|
87
|
+
$c->stash->{'bf.instance'} = $prev;
|
|
88
|
+
return $html;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
package Mojolicious::Plugin::BarefootJS::DevReload;
|
|
2
|
+
our $VERSION = "0.01";
|
|
2
3
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
3
4
|
|
|
4
5
|
=head1 NAME
|
|
@@ -29,21 +30,12 @@ C<< enabled => 1 >> to force-enable.
|
|
|
29
30
|
use Mojo::ByteStream qw(b);
|
|
30
31
|
use Mojo::IOLoop;
|
|
31
32
|
use File::Spec;
|
|
33
|
+
use BarefootJS::DevReload ();
|
|
32
34
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
my $
|
|
37
|
-
my $BUILD_ID_FILE = 'build-id';
|
|
38
|
-
my $SCROLL_STORAGE_KEY = '__bf_devreload_scroll';
|
|
39
|
-
|
|
40
|
-
# Heartbeat < any reasonable proxy/IOLoop idle timeout so a quiet connection
|
|
41
|
-
# doesn't get reaped between rebuilds.
|
|
42
|
-
my $HEARTBEAT_S = 5;
|
|
43
|
-
|
|
44
|
-
# Polling instead of Linux::Inotify2 / Mac::FSEvents keeps the runtime
|
|
45
|
-
# dependency-free. Sub-second latency is imperceptible next to browser reload.
|
|
46
|
-
my $POLL_S = 0.5;
|
|
35
|
+
# Engine-agnostic snippet, build-id reading, and timing constants are shared
|
|
36
|
+
# with the PSGI/Plack path in BarefootJS::DevReload — one source of truth.
|
|
37
|
+
my $HEARTBEAT_S = $BarefootJS::DevReload::HEARTBEAT_S;
|
|
38
|
+
my $POLL_S = $BarefootJS::DevReload::POLL_S;
|
|
47
39
|
|
|
48
40
|
sub register ($self, $app, $config = {}) {
|
|
49
41
|
my $dist_dir = $config->{dist_dir} // 'dist';
|
|
@@ -56,7 +48,7 @@ sub register ($self, $app, $config = {}) {
|
|
|
56
48
|
# on mode — it simply returns an empty ByteStream when disabled.
|
|
57
49
|
$app->helper(bf_dev_snippet => sub ($c) {
|
|
58
50
|
return b('') unless $enabled;
|
|
59
|
-
return b(
|
|
51
|
+
return b(BarefootJS::DevReload->snippet($endpoint));
|
|
60
52
|
});
|
|
61
53
|
|
|
62
54
|
return unless $enabled;
|
|
@@ -67,9 +59,8 @@ sub register ($self, $app, $config = {}) {
|
|
|
67
59
|
my $dist_abs = File::Spec->file_name_is_absolute($dist_dir)
|
|
68
60
|
? $dist_dir
|
|
69
61
|
: $app->home->child($dist_dir)->to_string;
|
|
70
|
-
|
|
71
|
-
my $build_id_path =
|
|
72
|
-
mkdir $dev_dir unless -d $dev_dir;
|
|
62
|
+
BarefootJS::DevReload->ensure_dev_dir($dist_abs);
|
|
63
|
+
my $build_id_path = BarefootJS::DevReload->build_id_path($dist_abs);
|
|
73
64
|
|
|
74
65
|
$app->routes->get($endpoint => sub ($c) {
|
|
75
66
|
my $last_event_id = $c->req->headers->header('Last-Event-ID') // '';
|
|
@@ -82,7 +73,7 @@ sub register ($self, $app, $config = {}) {
|
|
|
82
73
|
|
|
83
74
|
$c->write("retry: 1000\n\n");
|
|
84
75
|
|
|
85
|
-
my $initial_id =
|
|
76
|
+
my $initial_id = BarefootJS::DevReload->read_build_id($build_id_path);
|
|
86
77
|
my $last_sent = '';
|
|
87
78
|
if (length $initial_id) {
|
|
88
79
|
$last_sent = $initial_id;
|
|
@@ -105,7 +96,7 @@ sub register ($self, $app, $config = {}) {
|
|
|
105
96
|
$c->write(": hb\n\n");
|
|
106
97
|
});
|
|
107
98
|
$poll_id = Mojo::IOLoop->recurring($POLL_S => sub {
|
|
108
|
-
my $id =
|
|
99
|
+
my $id = BarefootJS::DevReload->read_build_id($build_id_path);
|
|
109
100
|
return unless length $id;
|
|
110
101
|
return if $id eq $last_sent;
|
|
111
102
|
$last_sent = $id;
|
|
@@ -116,35 +107,4 @@ sub register ($self, $app, $config = {}) {
|
|
|
116
107
|
return;
|
|
117
108
|
}
|
|
118
109
|
|
|
119
|
-
sub _read_build_id ($path) {
|
|
120
|
-
return '' unless -f $path;
|
|
121
|
-
open my $fh, '<', $path or return '';
|
|
122
|
-
local $/;
|
|
123
|
-
my $content = <$fh>;
|
|
124
|
-
close $fh;
|
|
125
|
-
$content //= '';
|
|
126
|
-
$content =~ s/^\s+|\s+$//g;
|
|
127
|
-
return $content;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
sub _snippet ($endpoint) {
|
|
131
|
-
my $ep = _js_str($endpoint);
|
|
132
|
-
my $sk = _js_str($SCROLL_STORAGE_KEY);
|
|
133
|
-
# Small IIFE: EventSource subscriber + scrollY preservation. Idempotent
|
|
134
|
-
# across duplicate mounts (window.__bfDevReload guard).
|
|
135
|
-
return qq{<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;try{var s=sessionStorage.getItem($sk);if(s){sessionStorage.removeItem($sk);var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};if(document.readyState==='loading'){addEventListener('DOMContentLoaded',restore,{once:true})}else{restore()}}}}catch(e){}var es=new EventSource($ep);es.addEventListener('reload',function(){try{sessionStorage.setItem($sk,String(window.scrollY))}catch(e){}location.reload()});es.addEventListener('error',function(){})})();</script>};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
sub _js_str ($s) {
|
|
139
|
-
# Minimal JS string escape for the handful of characters that can appear
|
|
140
|
-
# in a URL path or storage key. Good enough for package-internal + trusted
|
|
141
|
-
# operator-supplied strings; never interpolate untrusted input here.
|
|
142
|
-
my $t = $s;
|
|
143
|
-
$t =~ s/\\/\\\\/g;
|
|
144
|
-
$t =~ s/"/\\"/g;
|
|
145
|
-
$t =~ s/\n/\\n/g;
|
|
146
|
-
$t =~ s/\r/\\r/g;
|
|
147
|
-
return qq{"$t"};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
110
|
1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
package Mojolicious::Plugin::BarefootJS;
|
|
2
|
+
our $VERSION = "0.01";
|
|
2
3
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
3
4
|
|
|
4
5
|
use Mojo::File qw(path);
|
|
@@ -102,3 +103,58 @@ sub _load_manifest ($app, $config) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
1;
|
|
106
|
+
__END__
|
|
107
|
+
|
|
108
|
+
=encoding utf8
|
|
109
|
+
|
|
110
|
+
=head1 NAME
|
|
111
|
+
|
|
112
|
+
Mojolicious::Plugin::BarefootJS - Mojolicious integration for BarefootJS
|
|
113
|
+
|
|
114
|
+
=head1 SYNOPSIS
|
|
115
|
+
|
|
116
|
+
# Mojolicious application
|
|
117
|
+
$self->plugin('BarefootJS');
|
|
118
|
+
|
|
119
|
+
# In a controller / template, the `bf` helper exposes a per-request
|
|
120
|
+
# BarefootJS runtime backed by BarefootJS::Backend::Mojo.
|
|
121
|
+
|
|
122
|
+
=head1 DESCRIPTION
|
|
123
|
+
|
|
124
|
+
Wires the L<BarefootJS> server runtime into L<Mojolicious>. It registers a
|
|
125
|
+
C<bf> controller helper that lazily instantiates one BarefootJS object per
|
|
126
|
+
request (rendering via L<BarefootJS::Backend::Mojo>), and supports rendering
|
|
127
|
+
compiled marked templates as Mojolicious templates.
|
|
128
|
+
|
|
129
|
+
For non-Mojolicious / PSGI hosts, see L<BarefootJS::Backend::Xslate>, which
|
|
130
|
+
drives the same runtime with Text::Xslate and no web framework.
|
|
131
|
+
|
|
132
|
+
=head1 METHODS
|
|
133
|
+
|
|
134
|
+
L<Mojolicious::Plugin::BarefootJS> inherits all methods from
|
|
135
|
+
L<Mojolicious::Plugin> and implements the following new one.
|
|
136
|
+
|
|
137
|
+
=head2 register
|
|
138
|
+
|
|
139
|
+
$plugin->register(Mojolicious->new, \%conf);
|
|
140
|
+
|
|
141
|
+
Registers the plugin (the C<bf> helper and supporting hooks) in a Mojolicious
|
|
142
|
+
application.
|
|
143
|
+
|
|
144
|
+
=head1 SEE ALSO
|
|
145
|
+
|
|
146
|
+
L<BarefootJS>, L<BarefootJS::Backend::Mojo>, L<BarefootJS::Backend::Xslate>,
|
|
147
|
+
L<Mojolicious>, L<https://github.com/piconic-ai/barefootjs>
|
|
148
|
+
|
|
149
|
+
=head1 AUTHOR
|
|
150
|
+
|
|
151
|
+
kobaken E<lt>kentafly88@gmail.comE<gt>
|
|
152
|
+
|
|
153
|
+
=head1 LICENSE
|
|
154
|
+
|
|
155
|
+
Copyright (c) 2025-present BarefootJS Contributors.
|
|
156
|
+
|
|
157
|
+
This library is free software; you can redistribute it and/or modify it under
|
|
158
|
+
the MIT License. See the F<LICENSE> file in the distribution for the full text.
|
|
159
|
+
|
|
160
|
+
=cut
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "bun run build:js && bun run build:types",
|
|
32
|
-
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared",
|
|
32
|
+
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external typescript",
|
|
33
33
|
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
34
34
|
"test": "bun test",
|
|
35
35
|
"clean": "rm -rf dist",
|
|
@@ -52,14 +52,15 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/
|
|
55
|
+
"@barefootjs/perl": "0.8.0",
|
|
56
|
+
"@barefootjs/shared": "0.8.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
|
-
"@barefootjs/jsx": ">=0.2.0"
|
|
59
|
+
"@barefootjs/jsx": ">=0.2.0",
|
|
60
|
+
"typescript": "^5.0.0"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
61
63
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
62
|
-
"@barefootjs/jsx": "0.
|
|
63
|
-
"typescript": "^5.0.0"
|
|
64
|
+
"@barefootjs/jsx": "0.8.0"
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -69,16 +69,24 @@ runAdapterConformanceTests({
|
|
|
69
69
|
'toggle-shared',
|
|
70
70
|
'reactive-props',
|
|
71
71
|
'props-reactivity-comparison',
|
|
72
|
-
// #1467 Phase
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
72
|
+
// #1467 Phase 2b: basic interactive `site/ui` primitives. Cross-adapter
|
|
73
|
+
// parity for the `site/ui` corpus is Phase 3, so these participate only
|
|
74
|
+
// in Hono SSR conformance + the fixture-hydrate runtime layer for now.
|
|
75
|
+
// (`label` and `kbd` are static helpers; `toggle` / `switch` /
|
|
76
|
+
// `checkbox` carry uncontrolled state; `input` is a pass-through
|
|
77
|
+
// native control.)
|
|
78
|
+
//
|
|
79
|
+
// `textarea` now participates: its conditional inline-object spread
|
|
80
|
+
// (`{...(describedBy ? {...} : {})}`) lowers via
|
|
81
|
+
// `conditionalSpreadToPerl`, and its optional `rows={rows}`
|
|
82
|
+
// attribute is omitted when `undef` via the `defined`-guard in
|
|
83
|
+
// `elementAttrEmitter.emitExpression`
|
|
84
|
+
// (`<% if (defined $rows) { %>rows="<%= $rows %>"<% } %>`), matching
|
|
85
|
+
// Hono's nullish-attribute omission.
|
|
86
|
+
'toggle',
|
|
87
|
+
'switch',
|
|
88
|
+
'checkbox',
|
|
89
|
+
'kbd',
|
|
82
90
|
],
|
|
83
91
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
84
92
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
@@ -239,6 +247,76 @@ function compileAndGenerate(source: string, adapter?: MojoAdapter) {
|
|
|
239
247
|
// Mojo-Specific Tests
|
|
240
248
|
// =============================================================================
|
|
241
249
|
|
|
250
|
+
describe('MojoAdapter - conditional inline-object spread (textarea aria-describedby)', () => {
|
|
251
|
+
// `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to a Perl
|
|
252
|
+
// inline ternary of hashrefs so the falsy `{}` branch OMITS the key
|
|
253
|
+
// (bf->spread_attrs does not filter empty strings). The shared
|
|
254
|
+
// fixture only exercises the falsy branch; this pins the truthy one.
|
|
255
|
+
test('emits a Perl inline ternary of hashrefs through bf->spread_attrs', () => {
|
|
256
|
+
const { template } = compileAndGenerate(`
|
|
257
|
+
function Box({ describedBy }: { describedBy?: string }) {
|
|
258
|
+
return <div {...(describedBy ? { 'aria-describedby': describedBy } : {})} />
|
|
259
|
+
}
|
|
260
|
+
`)
|
|
261
|
+
expect(template).toContain(
|
|
262
|
+
"bf->spread_attrs($describedBy ? { 'aria-describedby' => $describedBy } : {})",
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('resolves the value reference and preserves the static key for a second prop', () => {
|
|
267
|
+
const { template } = compileAndGenerate(`
|
|
268
|
+
function Box({ label }: { label: string }) {
|
|
269
|
+
return <div {...(label ? { 'data-label': label } : {})} />
|
|
270
|
+
}
|
|
271
|
+
`)
|
|
272
|
+
expect(template).toContain(
|
|
273
|
+
"bf->spread_attrs($label ? { 'data-label' => $label } : {})",
|
|
274
|
+
)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('falls back to BF101 for a computed (non-static) object key', () => {
|
|
278
|
+
const adapter = new MojoAdapter()
|
|
279
|
+
const ir = compileToIR(`
|
|
280
|
+
function Box({ k, v }: { k?: string; v?: string }) {
|
|
281
|
+
return <div {...(v ? { [k]: v } : {})} />
|
|
282
|
+
}
|
|
283
|
+
`, adapter)
|
|
284
|
+
adapter.generate(ir)
|
|
285
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
286
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('MojoAdapter - nullish optional-attribute omission (textarea rows)', () => {
|
|
291
|
+
// A no-destructure-default, nillable-typed prop is `undef` when the
|
|
292
|
+
// caller omits it; guard its bare-reference attribute with Perl
|
|
293
|
+
// `defined` so it DROPS instead of rendering `attr=""` — matching
|
|
294
|
+
// Hono's nullish-attribute omission. Concrete/defaulted props are
|
|
295
|
+
// never `undef` and stay unconditional.
|
|
296
|
+
test('guards a no-default nillable attr with a Perl defined check', () => {
|
|
297
|
+
const { template } = compileAndGenerate(`
|
|
298
|
+
function C({ rows }: { rows?: number }) {
|
|
299
|
+
return <textarea rows={rows} />
|
|
300
|
+
}
|
|
301
|
+
`)
|
|
302
|
+
expect(template).toContain('<% if (defined $rows) { %>rows="<%= $rows %>"<% } %>')
|
|
303
|
+
// Must NOT emit the bare unconditional form.
|
|
304
|
+
expect(template).not.toMatch(/(?<!\{ %>)rows="<%= \$rows %>"/)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('leaves a defaulted attr unconditional (scope did not widen)', () => {
|
|
308
|
+
const { template } = compileAndGenerate(`
|
|
309
|
+
function C({ value = '' }: { value?: string }) {
|
|
310
|
+
return <textarea value={value} />
|
|
311
|
+
}
|
|
312
|
+
`)
|
|
313
|
+
// `value` has a destructure default → never undef → unconditional,
|
|
314
|
+
// exactly like Hono's value="".
|
|
315
|
+
expect(template).toContain('value="<%= $value %>"')
|
|
316
|
+
expect(template).not.toContain('defined $value')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
242
320
|
describe('MojoAdapter - Template Generation', () => {
|
|
243
321
|
test('generates basic element with scope marker', () => {
|
|
244
322
|
const result = compileAndGenerate(`
|
|
@@ -256,6 +334,42 @@ export function Hello() {
|
|
|
256
334
|
expect(adapter.extension).toBe('.html.ep')
|
|
257
335
|
})
|
|
258
336
|
|
|
337
|
+
test('module pure-string const referenced in className inlines the literal (#1467 Phase 2b)', () => {
|
|
338
|
+
// A module-scope `const X = 'literal'` used inside a className template
|
|
339
|
+
// literal must inline its value, NOT emit `$X` against a stash variable
|
|
340
|
+
// that is never bound (the value would render empty). Hono inlines it at
|
|
341
|
+
// runtime; this restores byte-parity.
|
|
342
|
+
const result = compileAndGenerate(`
|
|
343
|
+
"use client"
|
|
344
|
+
const labelClasses = 'flex items-center group-data-[disabled=true]:opacity-50'
|
|
345
|
+
export function Label({ className = '' }: { className?: string }) {
|
|
346
|
+
return <label className={\`\${labelClasses} \${className}\`} />
|
|
347
|
+
}
|
|
348
|
+
`)
|
|
349
|
+
// Inlined as a Perl single-quoted literal, escaped tokens intact.
|
|
350
|
+
expect(result.template).toContain(
|
|
351
|
+
"'flex items-center group-data-[disabled=true]:opacity-50'",
|
|
352
|
+
)
|
|
353
|
+
// No stash-variable reference to the const.
|
|
354
|
+
expect(result.template).not.toContain('$labelClasses')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('module pure-string const is NOT inlined when shadowed by a loop variable (#1749 review)', () => {
|
|
358
|
+
// A loop param whose name matches a module const must keep its loop
|
|
359
|
+
// binding (`$label`) inside the body — the const literal must not leak
|
|
360
|
+
// in. `renderLoop` guards module-const inlining for the loop body.
|
|
361
|
+
const result = compileAndGenerate(`
|
|
362
|
+
"use client"
|
|
363
|
+
const label = 'MODULE_CONST'
|
|
364
|
+
export function List({ items }: { items: string[] }) {
|
|
365
|
+
return <ul>{items.map(label => <li>{label}</li>)}</ul>
|
|
366
|
+
}
|
|
367
|
+
`)
|
|
368
|
+
// Inside the loop the param wins — emit the loop variable, not the const.
|
|
369
|
+
expect(result.template).toContain('$label')
|
|
370
|
+
expect(result.template).not.toContain('MODULE_CONST')
|
|
371
|
+
})
|
|
372
|
+
|
|
259
373
|
test('generates conditional with Perl if/else', () => {
|
|
260
374
|
const result = compileAndGenerate(`
|
|
261
375
|
"use client"
|