@barefootjs/mojolicious 0.14.0 → 0.15.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/boolean-result.d.ts.map +1 -1
- package/dist/adapter/index.js +147 -47
- package/dist/adapter/mojo-adapter.d.ts +62 -10
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +147 -47
- package/dist/index.js +147 -47
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +11 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +53 -4
- package/src/adapter/boolean-result.ts +5 -0
- package/src/adapter/mojo-adapter.ts +287 -39
- package/src/test-render.ts +98 -8
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
parseExpression as parseExpression2,
|
|
7
7
|
parseStyleObjectEntries,
|
|
8
8
|
isSupported,
|
|
9
|
+
exprToString,
|
|
10
|
+
parseProviderObjectLiteral,
|
|
9
11
|
identifierPath,
|
|
10
12
|
emitParsedExpr,
|
|
11
13
|
emitIRNode,
|
|
@@ -16,7 +18,10 @@ import {
|
|
|
16
18
|
extractArrowBodyExpression,
|
|
17
19
|
collectContextConsumers,
|
|
18
20
|
isLowerableObjectRestDestructure,
|
|
19
|
-
|
|
21
|
+
collectModuleStringConsts,
|
|
22
|
+
lookupStaticRecordLiteral,
|
|
23
|
+
searchParamsLocalNames,
|
|
24
|
+
matchSearchParamsMethodCall
|
|
20
25
|
} from "@barefootjs/jsx";
|
|
21
26
|
|
|
22
27
|
// src/adapter/boolean-result.ts
|
|
@@ -63,6 +68,8 @@ var ARIA_BOOLEAN_ATTRS = new Set([
|
|
|
63
68
|
"aria-multiselectable",
|
|
64
69
|
"aria-readonly",
|
|
65
70
|
"aria-required",
|
|
71
|
+
"aria-selected",
|
|
72
|
+
"aria-expanded",
|
|
66
73
|
"aria-checked",
|
|
67
74
|
"aria-pressed"
|
|
68
75
|
]);
|
|
@@ -71,7 +78,7 @@ function isAriaBooleanAttr(name) {
|
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
// src/adapter/mojo-adapter.ts
|
|
74
|
-
import { BF_SLOT, BF_COND } from "@barefootjs/shared";
|
|
81
|
+
import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
|
|
75
82
|
var MOJO_TEMPLATE_PRIMITIVES = {
|
|
76
83
|
"JSON.stringify": { arity: 1, emit: (args) => `bf->json(${args[0]})` },
|
|
77
84
|
String: { arity: 1, emit: (args) => `bf->string(${args[0]})` },
|
|
@@ -89,22 +96,6 @@ function resolveJsxChildrenProp(props) {
|
|
|
89
96
|
return [];
|
|
90
97
|
return prop.value.children;
|
|
91
98
|
}
|
|
92
|
-
function parsePureStringLiteral(source) {
|
|
93
|
-
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
94
|
-
const stmt = sf.statements[0];
|
|
95
|
-
if (!stmt || !ts.isVariableStatement(stmt))
|
|
96
|
-
return null;
|
|
97
|
-
const decl = stmt.declarationList.declarations[0];
|
|
98
|
-
let init = decl?.initializer;
|
|
99
|
-
while (init && ts.isParenthesizedExpression(init))
|
|
100
|
-
init = init.expression;
|
|
101
|
-
if (!init)
|
|
102
|
-
return null;
|
|
103
|
-
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
104
|
-
return init.text;
|
|
105
|
-
}
|
|
106
|
-
return evalStringArrayJoin(source);
|
|
107
|
-
}
|
|
108
99
|
function perlHashKey(name) {
|
|
109
100
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
110
101
|
}
|
|
@@ -152,7 +143,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
152
143
|
inLoop = false;
|
|
153
144
|
propsObjectName = null;
|
|
154
145
|
propsParams = [];
|
|
146
|
+
booleanTypedProps = new Set;
|
|
155
147
|
stringValueNames = new Set;
|
|
148
|
+
_searchParamsLocals = new Set;
|
|
156
149
|
moduleStringConsts = new Map;
|
|
157
150
|
localConstants = [];
|
|
158
151
|
loopBoundNames = new Map;
|
|
@@ -169,6 +162,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
169
162
|
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
170
163
|
augmentInheritedPropAccesses(ir);
|
|
171
164
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
165
|
+
this.booleanTypedProps = new Set(ir.metadata.propsParams.filter((prop) => prop.type?.primitive === "boolean" || prop.type?.raw === "boolean").map((prop) => prop.name));
|
|
172
166
|
this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
173
167
|
this.stringValueNames = new Set;
|
|
174
168
|
for (const s of ir.metadata.signals) {
|
|
@@ -180,7 +174,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
180
174
|
if (isStringTypeInfo(p.type))
|
|
181
175
|
this.stringValueNames.add(p.name);
|
|
182
176
|
}
|
|
183
|
-
this.moduleStringConsts =
|
|
177
|
+
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
178
|
+
this._searchParamsLocals = searchParamsLocalNames(ir.metadata);
|
|
184
179
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
185
180
|
this.loopBoundNames.clear();
|
|
186
181
|
this.errors = [];
|
|
@@ -210,18 +205,49 @@ class MojoAdapter extends BaseAdapter {
|
|
|
210
205
|
extension: this.extension
|
|
211
206
|
};
|
|
212
207
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
continue;
|
|
218
|
-
if (c.value === undefined)
|
|
219
|
-
continue;
|
|
220
|
-
const literal = parsePureStringLiteral(c.value);
|
|
221
|
-
if (literal !== null)
|
|
222
|
-
map.set(c.name, literal);
|
|
208
|
+
isBooleanTypedPropRef(expr) {
|
|
209
|
+
let bare = expr.trim();
|
|
210
|
+
if (this.propsObjectName && bare.startsWith(`${this.propsObjectName}.`)) {
|
|
211
|
+
bare = bare.slice(this.propsObjectName.length + 1);
|
|
223
212
|
}
|
|
224
|
-
|
|
213
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
214
|
+
return false;
|
|
215
|
+
return this.booleanTypedProps.has(bare);
|
|
216
|
+
}
|
|
217
|
+
parseUndefinedAlternateTernary(expr) {
|
|
218
|
+
const parsed = parseExpression2(expr.trim());
|
|
219
|
+
if (parsed?.kind !== "conditional")
|
|
220
|
+
return null;
|
|
221
|
+
const alt = parsed.alternate;
|
|
222
|
+
const isUndef = alt.kind === "identifier" && (alt.name === "undefined" || alt.name === "null") || alt.kind === "literal" && (alt.value === null || alt.value === undefined);
|
|
223
|
+
if (!isUndef)
|
|
224
|
+
return null;
|
|
225
|
+
return {
|
|
226
|
+
condition: exprToString(parsed.test),
|
|
227
|
+
consequent: exprToString(parsed.consequent)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
resolveLiteralConst(name) {
|
|
231
|
+
if (this.loopBoundNames?.has?.(name))
|
|
232
|
+
return null;
|
|
233
|
+
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
234
|
+
if (c?.value === undefined)
|
|
235
|
+
return null;
|
|
236
|
+
const v = c.value.trim();
|
|
237
|
+
if (/^-?\d+(\.\d+)?$/.test(v))
|
|
238
|
+
return v;
|
|
239
|
+
const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v);
|
|
240
|
+
if (strLit)
|
|
241
|
+
return `'${strLit[1].replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
resolveStaticRecordLiteral(objectName, key) {
|
|
245
|
+
if (this.loopBoundNames?.has?.(objectName))
|
|
246
|
+
return null;
|
|
247
|
+
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
248
|
+
if (!hit)
|
|
249
|
+
return null;
|
|
250
|
+
return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
225
251
|
}
|
|
226
252
|
resolveModuleStringConst(name) {
|
|
227
253
|
if (this.loopBoundNames.has(name))
|
|
@@ -289,12 +315,29 @@ class MojoAdapter extends BaseAdapter {
|
|
|
289
315
|
if (v.kind === "literal") {
|
|
290
316
|
return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
|
|
291
317
|
}
|
|
292
|
-
if (v.kind === "expression")
|
|
318
|
+
if (v.kind === "expression") {
|
|
319
|
+
const hashref = this.providerObjectLiteralPerl(v.expr);
|
|
320
|
+
if (hashref !== null)
|
|
321
|
+
return hashref;
|
|
293
322
|
return this.convertExpressionToPerl(v.expr);
|
|
323
|
+
}
|
|
294
324
|
if (v.kind === "template")
|
|
295
325
|
return this.convertTemplateLiteralPartsToPerl(v.parts);
|
|
296
326
|
return "undef";
|
|
297
327
|
}
|
|
328
|
+
providerObjectLiteralPerl(expr) {
|
|
329
|
+
const members = parseProviderObjectLiteral(expr.trim());
|
|
330
|
+
if (members === null)
|
|
331
|
+
return null;
|
|
332
|
+
const entries = members.map((m) => {
|
|
333
|
+
const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
|
|
334
|
+
if (m.kind === "function" || /^on[A-Z]/.test(m.name))
|
|
335
|
+
return `${key} => undef`;
|
|
336
|
+
const src = m.kind === "getter" ? m.body : m.expr;
|
|
337
|
+
return `${key} => ${this.convertExpressionToPerl(src)}`;
|
|
338
|
+
});
|
|
339
|
+
return `{ ${entries.join(", ")} }`;
|
|
340
|
+
}
|
|
298
341
|
contextDefaultPerl(c) {
|
|
299
342
|
const d = c.defaultValue;
|
|
300
343
|
if (d === null || d === undefined)
|
|
@@ -318,7 +361,6 @@ class MojoAdapter extends BaseAdapter {
|
|
|
318
361
|
const signals = ir.metadata.signals ?? [];
|
|
319
362
|
if (memos.length === 0 && signals.length === 0)
|
|
320
363
|
return "";
|
|
321
|
-
const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
|
|
322
364
|
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
323
365
|
const lines = [];
|
|
324
366
|
for (const signal of signals) {
|
|
@@ -328,18 +370,12 @@ class MojoAdapter extends BaseAdapter {
|
|
|
328
370
|
available.add(signal.getter);
|
|
329
371
|
}
|
|
330
372
|
for (const memo of memos) {
|
|
331
|
-
const def = ssrDefaults[memo.name];
|
|
332
|
-
const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
|
|
333
|
-
if (!isNull) {
|
|
334
|
-
available.add(memo.name);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
373
|
const body = extractArrowBodyExpression(memo.computation);
|
|
338
|
-
if (body
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
374
|
+
if (body !== null) {
|
|
375
|
+
const perl = this.tryLowerToPerl(body, available);
|
|
376
|
+
if (perl !== null)
|
|
377
|
+
lines.push(`% my $${memo.name} = ${perl};`);
|
|
378
|
+
}
|
|
343
379
|
available.add(memo.name);
|
|
344
380
|
}
|
|
345
381
|
return lines.length > 0 ? lines.join(`
|
|
@@ -374,6 +410,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
374
410
|
if (element.slotId) {
|
|
375
411
|
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
376
412
|
}
|
|
413
|
+
if (element.regionId) {
|
|
414
|
+
hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
|
|
415
|
+
}
|
|
377
416
|
const voidElements = [
|
|
378
417
|
"area",
|
|
379
418
|
"base",
|
|
@@ -654,7 +693,7 @@ ${renderedChildren}` : renderedChildren;
|
|
|
654
693
|
renderComponent(comp) {
|
|
655
694
|
const propParts = [];
|
|
656
695
|
for (const p of comp.props) {
|
|
657
|
-
if (p.name.match(/^on[A-Z]/) && p.value.kind === "expression")
|
|
696
|
+
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
658
697
|
continue;
|
|
659
698
|
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
|
|
660
699
|
if (lowered)
|
|
@@ -667,13 +706,17 @@ ${renderedChildren}` : renderedChildren;
|
|
|
667
706
|
const tplName = this.toTemplateName(comp.name);
|
|
668
707
|
const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
|
|
669
708
|
if (effectiveChildren.length > 0) {
|
|
709
|
+
const prevInLoop = this.inLoop;
|
|
710
|
+
this.inLoop = false;
|
|
670
711
|
const childrenBody = this.renderChildren(effectiveChildren);
|
|
712
|
+
this.inLoop = prevInLoop;
|
|
671
713
|
const varName = `$bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
672
714
|
return `<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`;
|
|
673
715
|
}
|
|
674
716
|
return `<%== bf->render_child('${tplName}'${propsStr}) %>`;
|
|
675
717
|
}
|
|
676
718
|
childrenCaptureCounter = 0;
|
|
719
|
+
presenceVarCounter = 0;
|
|
677
720
|
toTemplateName(componentName) {
|
|
678
721
|
return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
679
722
|
}
|
|
@@ -729,14 +772,28 @@ ${children}`;
|
|
|
729
772
|
const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
|
|
730
773
|
if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
|
|
731
774
|
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
732
|
-
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
775
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
733
776
|
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
734
777
|
}
|
|
735
|
-
if (isBooleanAttr(name)
|
|
778
|
+
if (isBooleanAttr(name)) {
|
|
736
779
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
737
780
|
}
|
|
781
|
+
if (value.presenceOrUndefined) {
|
|
782
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
783
|
+
const tmp = `$bf_pu${this.presenceVarCounter++}`;
|
|
784
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${tmp}) %>"` : `${name}="<%= ${tmp} %>"`;
|
|
785
|
+
return `<% my ${tmp} = ${perl2}; if (${tmp}) { %>${body}<% } %>`;
|
|
786
|
+
}
|
|
787
|
+
{
|
|
788
|
+
const m = this.parseUndefinedAlternateTernary(value.expr);
|
|
789
|
+
if (m) {
|
|
790
|
+
const cond = this.convertExpressionToPerl(m.condition);
|
|
791
|
+
const val = this.convertExpressionToPerl(m.consequent);
|
|
792
|
+
return `<% if (${cond}) { %>${name}="<%= ${val} %>"<% } %>`;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
738
795
|
const perl = this.convertExpressionToPerl(value.expr);
|
|
739
|
-
if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name)) {
|
|
796
|
+
if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr)) {
|
|
740
797
|
return `${name}="<%= bf->bool_str(${perl}) %>"`;
|
|
741
798
|
}
|
|
742
799
|
return `${name}="<%= ${perl} %>"`;
|
|
@@ -995,6 +1052,18 @@ ${reason}` : "";
|
|
|
995
1052
|
return n;
|
|
996
1053
|
})();
|
|
997
1054
|
const indexed = this.recordIndexAccessToPerl(initNode);
|
|
1055
|
+
if (indexed === null && ts.isElementAccessExpression(initNode) && initNode.argumentExpression && !ts.isNumericLiteral(initNode.argumentExpression) && !ts.isStringLiteral(initNode.argumentExpression)) {
|
|
1056
|
+
this.errors.push({
|
|
1057
|
+
code: "BF101",
|
|
1058
|
+
severity: "error",
|
|
1059
|
+
message: `Spread object value '${initNode.getText(sf)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
|
|
1060
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1061
|
+
suggestion: {
|
|
1062
|
+
message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
998
1067
|
const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
|
|
999
1068
|
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
1000
1069
|
}
|
|
@@ -1118,6 +1187,11 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
1118
1187
|
const recv = emit(object);
|
|
1119
1188
|
return `bf->trim(${recv})`;
|
|
1120
1189
|
}
|
|
1190
|
+
case "toFixed": {
|
|
1191
|
+
const recv = emit(object);
|
|
1192
|
+
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
1193
|
+
return `bf->to_fixed(${recv}, ${digits})`;
|
|
1194
|
+
}
|
|
1121
1195
|
case "split": {
|
|
1122
1196
|
const recv = emit(object);
|
|
1123
1197
|
if (args.length === 0) {
|
|
@@ -1220,6 +1294,10 @@ function isStringTypedOperand(expr, isStringName) {
|
|
|
1220
1294
|
}
|
|
1221
1295
|
return false;
|
|
1222
1296
|
}
|
|
1297
|
+
function emitIndexAccessPerl(object, index, emit, isStringName) {
|
|
1298
|
+
const i = emit(index);
|
|
1299
|
+
return isStringTypedOperand(index, isStringName) ? `${emit(object)}->{${i}}` : `${emit(object)}->[${i}]`;
|
|
1300
|
+
}
|
|
1223
1301
|
|
|
1224
1302
|
class MojoFilterEmitter {
|
|
1225
1303
|
param;
|
|
@@ -1253,6 +1331,9 @@ class MojoFilterEmitter {
|
|
|
1253
1331
|
}
|
|
1254
1332
|
return `${emit(object)}->{${property}}`;
|
|
1255
1333
|
}
|
|
1334
|
+
indexAccess(object, index, emit) {
|
|
1335
|
+
return emitIndexAccessPerl(object, index, emit, this.isStringName);
|
|
1336
|
+
}
|
|
1256
1337
|
call(callee, args, emit) {
|
|
1257
1338
|
if (callee.kind === "identifier" && args.length === 0) {
|
|
1258
1339
|
return `$${callee.name}`;
|
|
@@ -1353,9 +1434,14 @@ class MojoTopLevelEmitter {
|
|
|
1353
1434
|
this.adapter = adapter;
|
|
1354
1435
|
}
|
|
1355
1436
|
identifier(name) {
|
|
1437
|
+
if (name === "undefined" || name === "null")
|
|
1438
|
+
return "undef";
|
|
1356
1439
|
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1357
1440
|
if (inlined !== null)
|
|
1358
1441
|
return inlined;
|
|
1442
|
+
const literalConst = this.adapter.resolveLiteralConst(name);
|
|
1443
|
+
if (literalConst !== null)
|
|
1444
|
+
return literalConst;
|
|
1359
1445
|
return `$${name}`;
|
|
1360
1446
|
}
|
|
1361
1447
|
literal(value, literalType) {
|
|
@@ -1371,15 +1457,29 @@ class MojoTopLevelEmitter {
|
|
|
1371
1457
|
if (object.kind === "identifier" && object.name === "props") {
|
|
1372
1458
|
return `$${property}`;
|
|
1373
1459
|
}
|
|
1460
|
+
if (object.kind === "identifier") {
|
|
1461
|
+
const staticValue = this.adapter.resolveStaticRecordLiteral(object.name, property);
|
|
1462
|
+
if (staticValue !== null)
|
|
1463
|
+
return staticValue;
|
|
1464
|
+
}
|
|
1374
1465
|
const obj = emit(object);
|
|
1375
1466
|
if (property === "length")
|
|
1376
1467
|
return `scalar(@{${obj}})`;
|
|
1377
1468
|
return `${obj}->{${property}}`;
|
|
1378
1469
|
}
|
|
1470
|
+
indexAccess(object, index, emit) {
|
|
1471
|
+
return emitIndexAccessPerl(object, index, emit, (n) => this.adapter._isStringValueName(n));
|
|
1472
|
+
}
|
|
1379
1473
|
call(callee, args, emit) {
|
|
1380
1474
|
if (callee.kind === "identifier" && args.length === 0) {
|
|
1381
1475
|
return `$${callee.name}`;
|
|
1382
1476
|
}
|
|
1477
|
+
if (this.adapter._searchParamsLocals.size > 0) {
|
|
1478
|
+
const sp = matchSearchParamsMethodCall(callee, args, this.adapter._searchParamsLocals);
|
|
1479
|
+
if (sp) {
|
|
1480
|
+
return `$searchParams->${sp.method}(${sp.args.map(emit).join(", ")})`;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1383
1483
|
const path = identifierPath(callee);
|
|
1384
1484
|
const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
|
|
1385
1485
|
if (path && spec) {
|
|
@@ -1 +1 @@
|
|
|
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;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;
|
|
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;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;AAmWD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
package Mojolicious::Plugin::BarefootJS;
|
|
2
|
-
our $VERSION = "0.
|
|
2
|
+
our $VERSION = "0.14.0";
|
|
3
3
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
4
4
|
|
|
5
5
|
use Mojo::File qw(path);
|
|
@@ -85,6 +85,16 @@ sub register ($self, $app, $config = {}) {
|
|
|
85
85
|
$c->stash->{$name} = $value;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
# (#1922) Seed the request-scoped `searchParams()` reader as the
|
|
90
|
+
# `$searchParams` template var, built from the live request query —
|
|
91
|
+
# so `searchParams().get(k)` resolves the current query during SSR
|
|
92
|
+
# (the client re-reads window.location on hydration). A caller that
|
|
93
|
+
# set it by hand wins (`//=`). Harmless for components that never read
|
|
94
|
+
# it; the var simply goes unused. `$bf->search_params` lazy-loads the
|
|
95
|
+
# reader class, so the plugin needn't `use` it directly.
|
|
96
|
+
$c->stash->{searchParams} //=
|
|
97
|
+
$bf->search_params($c->req->query_params->to_string);
|
|
88
98
|
});
|
|
89
99
|
}
|
|
90
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/shared": "0.
|
|
55
|
+
"@barefootjs/shared": "0.15.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -60,6 +60,6 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
63
|
-
"@barefootjs/jsx": "0.
|
|
63
|
+
"@barefootjs/jsx": "0.15.0"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -17,10 +17,13 @@ runAdapterConformanceTests({
|
|
|
17
17
|
name: 'mojo',
|
|
18
18
|
factory: () => new MojoAdapter(),
|
|
19
19
|
render: renderMojoComponent,
|
|
20
|
-
// No JSX-render skips: every shared conformance fixture
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
20
|
+
// No JSX-render skips: every shared conformance fixture — including
|
|
21
|
+
// the composed `site/ui` demo corpus (#1467 / #1897) — renders to
|
|
22
|
+
// Hono parity on real Mojolicious. `data-table` came off via the
|
|
23
|
+
// body-children `inLoop` reset (#1896): the loop-item component
|
|
24
|
+
// (TableRow) still gets `ComponentName_<random>` scope IDs, but its
|
|
25
|
+
// body children (TableCell) now receive `_bf_slot` for deterministic
|
|
26
|
+
// parent-scope-derived IDs matching Hono.
|
|
24
27
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
25
28
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
26
29
|
// (not by the shared fixtures) so adding a new adapter doesn't
|
|
@@ -62,6 +65,21 @@ runAdapterConformanceTests({
|
|
|
62
65
|
// (`cn\`base \${tone()}\``) — same family as #1322 above and refused
|
|
63
66
|
// via the same gate.
|
|
64
67
|
'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
|
|
68
|
+
// #1467 demo-corpus context providers (`radio-group`, `accordion`,
|
|
69
|
+
// `dialog`, `popover`, `select`, `dropdown-menu`, `combobox`,
|
|
70
|
+
// `command`) are no longer pinned — an object-literal provider value
|
|
71
|
+
// (`{ open: () => props.open ?? false, onOpenChange: (v) => {…} }`)
|
|
72
|
+
// lowers to a Perl hashref via `parseProviderObjectLiteral` (#1897):
|
|
73
|
+
// getter members snapshot their body's SSR value, handler /
|
|
74
|
+
// function-shaped members lower to `undef`. The command demo's
|
|
75
|
+
// `ref={(el) => {…}}` function prop on an imported component is
|
|
76
|
+
// skipped at SSR like `on*` handlers.
|
|
77
|
+
//
|
|
78
|
+
// #1467 Phase 2e: `data-table` is no longer pinned here either — it
|
|
79
|
+
// compiles clean (`selected()[index]` → `index-access`,
|
|
80
|
+
// `.toFixed(2)` → `bf->to_fixed`, `/* @client */` memo SSR-folded)
|
|
81
|
+
// and is pinned in `skipJsx` above on the keyed-loop scope-ID
|
|
82
|
+
// divergence alone (#1896), not a BF101.
|
|
65
83
|
// #1443: `[a, b].filter(Boolean).join(' ')` (the registry Slot's
|
|
66
84
|
// shape) now lowers to `join(' ', @{[grep { $_ } @{[$a, $b]}]})`.
|
|
67
85
|
// No BF101 expected — pinned positively via the
|
|
@@ -135,6 +153,8 @@ runAdapterConformanceTests({
|
|
|
135
153
|
// intentionally elide a slot id from the SSR template that the IR
|
|
136
154
|
// still declares (s6). See hono-adapter.test for the contract.
|
|
137
155
|
'todo-app',
|
|
156
|
+
// #1467 Phase 2e: same `/* @client */` keyed-map elision (data-table).
|
|
157
|
+
'data-table',
|
|
138
158
|
]),
|
|
139
159
|
onRenderError: (err, id) => {
|
|
140
160
|
if (err instanceof PerlNotAvailableError) {
|
|
@@ -209,6 +229,35 @@ function Box({ k, v }: { k?: string; v?: string }) {
|
|
|
209
229
|
})
|
|
210
230
|
})
|
|
211
231
|
|
|
232
|
+
describe('MojoAdapter - searchParams() env-signal lowering (#1922)', () => {
|
|
233
|
+
// `searchParams().get(k)` is an env-signal method call: it must lower to a
|
|
234
|
+
// real method call on the per-request `$searchParams` reader, not the
|
|
235
|
+
// generic hash deref `$searchParams->{get}` (which drops the arg).
|
|
236
|
+
test('lowers searchParams().get(k) to a method call on $searchParams', () => {
|
|
237
|
+
const { template } = compileAndGenerate(`
|
|
238
|
+
import { searchParams } from '@barefootjs/client'
|
|
239
|
+
function SortLabel() {
|
|
240
|
+
return <p>{searchParams().get('sort') ?? 'none'}</p>
|
|
241
|
+
}
|
|
242
|
+
`)
|
|
243
|
+
expect(template).toContain("($searchParams->get('sort') // 'none')")
|
|
244
|
+
expect(template).not.toContain('$searchParams->{get}')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// An aliased import binds the env signal to a different local name; the
|
|
248
|
+
// expression reads `sp()`, but it still lowers to the canonical
|
|
249
|
+
// `$searchParams` reader (the harness/plugin seed that fixed var).
|
|
250
|
+
test('matches an aliased import (`searchParams as sp`) and emits canonical $searchParams', () => {
|
|
251
|
+
const { template } = compileAndGenerate(`
|
|
252
|
+
import { searchParams as sp } from '@barefootjs/client'
|
|
253
|
+
function SortLabel() {
|
|
254
|
+
return <p>{sp().get('sort') ?? 'none'}</p>
|
|
255
|
+
}
|
|
256
|
+
`)
|
|
257
|
+
expect(template).toContain("($searchParams->get('sort') // 'none')")
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
212
261
|
describe('MojoAdapter - local-const conditional-spread resolution (#checkbox icon)', () => {
|
|
213
262
|
// A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, spread as
|
|
214
263
|
// a bare identifier (`{...attrs}`), resolves through the same Perl
|
|
@@ -112,6 +112,11 @@ const ARIA_BOOLEAN_ATTRS = new Set([
|
|
|
112
112
|
'aria-multiselectable',
|
|
113
113
|
'aria-readonly',
|
|
114
114
|
'aria-required',
|
|
115
|
+
// true | false | undefined (absent) — selection / disclosure state
|
|
116
|
+
// (#1897: tabs' `aria-selected={props.selected ?? false}` rendered the
|
|
117
|
+
// Perl-native `1`/`0` without this).
|
|
118
|
+
'aria-selected',
|
|
119
|
+
'aria-expanded',
|
|
115
120
|
// Tri-state (true | false | mixed). The `bool_str` helper only
|
|
116
121
|
// maps Perl truthy / falsy to true / false — a fixture that wants
|
|
117
122
|
// the literal `"mixed"` would bind a string-valued JSX attr
|