@barefootjs/mojolicious 0.17.0 → 0.18.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/expr/array-method.d.ts +13 -1
- package/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +9 -4
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +100 -68
- package/dist/adapter/lib/ir-scope.d.ts +0 -7
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -1
- package/dist/adapter/memo/seed.d.ts +10 -19
- package/dist/adapter/memo/seed.d.ts.map +1 -1
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +100 -68
- package/dist/conformance-pins.d.ts +10 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +116 -68
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +14 -5
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +55 -33
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +180 -135
- package/src/__tests__/multi-component-registry.test.ts +215 -0
- package/src/__tests__/stock-route.test.ts +203 -0
- package/src/adapter/expr/array-method.ts +43 -1
- package/src/adapter/expr/emitters.ts +59 -15
- package/src/adapter/lib/ir-scope.ts +0 -13
- package/src/adapter/memo/seed.ts +20 -73
- package/src/adapter/mojo-adapter.ts +154 -34
- package/src/conformance-pins.ts +138 -0
- package/src/index.ts +1 -0
- package/src/test-render.ts +8 -0
package/dist/index.js
CHANGED
|
@@ -2,23 +2,24 @@
|
|
|
2
2
|
import {
|
|
3
3
|
BaseAdapter,
|
|
4
4
|
isBooleanAttr,
|
|
5
|
-
parseExpression as
|
|
5
|
+
parseExpression as parseExpression2,
|
|
6
6
|
stringifyParsedExpr as stringifyParsedExpr2,
|
|
7
7
|
parseStyleObjectEntries,
|
|
8
|
-
isSupported
|
|
8
|
+
isSupported,
|
|
9
9
|
exprToString,
|
|
10
10
|
parseProviderObjectLiteral,
|
|
11
11
|
emitParsedExpr as emitParsedExpr2,
|
|
12
12
|
emitIRNode,
|
|
13
13
|
emitAttrValue,
|
|
14
14
|
augmentInheritedPropAccesses,
|
|
15
|
-
isLowerableObjectRestDestructure,
|
|
16
15
|
collectModuleStringConsts,
|
|
17
16
|
lookupStaticRecordLiteral,
|
|
18
17
|
searchParamsLocalNames,
|
|
19
18
|
prepareLoweringMatchers,
|
|
20
19
|
queryHrefArgs,
|
|
21
|
-
|
|
20
|
+
isValidHelperId,
|
|
21
|
+
sortComparatorFromArrow as sortComparatorFromArrow2,
|
|
22
|
+
isLowerableLoopDestructure
|
|
22
23
|
} from "@barefootjs/jsx";
|
|
23
24
|
|
|
24
25
|
// src/adapter/boolean-result.ts
|
|
@@ -128,13 +129,6 @@ function collectRootScopeNodes(node) {
|
|
|
128
129
|
visit(node);
|
|
129
130
|
return out;
|
|
130
131
|
}
|
|
131
|
-
function referencedVarsAreAvailable(expr, available) {
|
|
132
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
133
|
-
if (!available.has(m[1]))
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
132
|
|
|
139
133
|
// src/adapter/expr/array-method.ts
|
|
140
134
|
import {
|
|
@@ -307,6 +301,13 @@ function renderFlatMapEval(recv, body, param, emit) {
|
|
|
307
301
|
const env = emitEvalEnvArg(body, [param], emit);
|
|
308
302
|
return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
309
303
|
}
|
|
304
|
+
function renderMapEval(recv, body, param, emit) {
|
|
305
|
+
const json = serializeParsedExpr(body);
|
|
306
|
+
if (json === null)
|
|
307
|
+
return null;
|
|
308
|
+
const env = emitEvalEnvArg(body, [param], emit);
|
|
309
|
+
return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
310
|
+
}
|
|
310
311
|
function renderSortMethod(recv, c) {
|
|
311
312
|
const keyHashes = c.keys.map((k) => {
|
|
312
313
|
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
@@ -314,7 +315,10 @@ function renderSortMethod(recv, c) {
|
|
|
314
315
|
});
|
|
315
316
|
return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
316
317
|
}
|
|
317
|
-
function renderFlatMethod(recv, depth) {
|
|
318
|
+
function renderFlatMethod(recv, depth, emit) {
|
|
319
|
+
if (typeof depth === "object") {
|
|
320
|
+
return `bf->flat_dynamic(${recv}, ${emit(depth.expr)})`;
|
|
321
|
+
}
|
|
318
322
|
const d = depth === "infinity" ? -1 : depth;
|
|
319
323
|
return `bf->flat(${recv}, ${d})`;
|
|
320
324
|
}
|
|
@@ -360,10 +364,12 @@ class MojoFilterEmitter {
|
|
|
360
364
|
param;
|
|
361
365
|
localVarMap;
|
|
362
366
|
isStringName;
|
|
363
|
-
|
|
367
|
+
onUnsupported;
|
|
368
|
+
constructor(param, localVarMap, isStringName = () => false, onUnsupported) {
|
|
364
369
|
this.param = param;
|
|
365
370
|
this.localVarMap = localVarMap;
|
|
366
371
|
this.isStringName = isStringName;
|
|
372
|
+
this.onUnsupported = onUnsupported;
|
|
367
373
|
}
|
|
368
374
|
identifier(name) {
|
|
369
375
|
if (name === this.param)
|
|
@@ -442,12 +448,14 @@ class MojoFilterEmitter {
|
|
|
442
448
|
return `(${l} // ${r})`;
|
|
443
449
|
}
|
|
444
450
|
callbackMethod(method, object, arrow, _restArgs, emit) {
|
|
445
|
-
if (!PREDICATE_METHODS.has(method))
|
|
451
|
+
if (!PREDICATE_METHODS.has(method)) {
|
|
452
|
+
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
446
453
|
return "1";
|
|
454
|
+
}
|
|
447
455
|
const param = arrow.params[0];
|
|
448
456
|
const predicate = arrow.body;
|
|
449
457
|
const arrayExpr = emit(object);
|
|
450
|
-
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
|
|
458
|
+
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName, this.onUnsupported));
|
|
451
459
|
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
452
460
|
if (method === "filter")
|
|
453
461
|
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
@@ -455,6 +463,7 @@ class MojoFilterEmitter {
|
|
|
455
463
|
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
456
464
|
if (method === "some")
|
|
457
465
|
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
466
|
+
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
458
467
|
return arrayExpr;
|
|
459
468
|
}
|
|
460
469
|
arrayLiteral(elements, emit) {
|
|
@@ -464,7 +473,7 @@ class MojoFilterEmitter {
|
|
|
464
473
|
return renderArrayMethod(method, object, args, emit);
|
|
465
474
|
}
|
|
466
475
|
flatMethod(object, depth, emit) {
|
|
467
|
-
return renderFlatMethod(emit(object), depth);
|
|
476
|
+
return renderFlatMethod(emit(object), depth, emit);
|
|
468
477
|
}
|
|
469
478
|
conditional(_test, _consequent, _alternate) {
|
|
470
479
|
return "1";
|
|
@@ -620,6 +629,13 @@ class MojoTopLevelEmitter {
|
|
|
620
629
|
this.ctx._recordExprBF101(`.flatMap(...) projection is not lowerable to a template flat-map`, `Pre-compute the projection in the route handler, or mark the loop @client-only.`);
|
|
621
630
|
return "''";
|
|
622
631
|
}
|
|
632
|
+
if (method === "map") {
|
|
633
|
+
const evalForm = renderMapEval(recv, body, params[0], emit);
|
|
634
|
+
if (evalForm !== null)
|
|
635
|
+
return evalForm;
|
|
636
|
+
this.ctx._recordExprBF101(`.map(...) projection is not lowerable to a template map`, `Pre-compute the projection in the route handler, or mark the position @client-only.`);
|
|
637
|
+
return "''";
|
|
638
|
+
}
|
|
623
639
|
const cb = {
|
|
624
640
|
method,
|
|
625
641
|
object,
|
|
@@ -672,7 +688,7 @@ class MojoTopLevelEmitter {
|
|
|
672
688
|
return renderArrayMethod(method, object, args, emit);
|
|
673
689
|
}
|
|
674
690
|
flatMethod(object, depth, emit) {
|
|
675
|
-
return renderFlatMethod(emit(object), depth);
|
|
691
|
+
return renderFlatMethod(emit(object), depth, emit);
|
|
676
692
|
}
|
|
677
693
|
conditional(test, consequent, alternate, emit) {
|
|
678
694
|
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
|
|
@@ -703,8 +719,8 @@ class MojoTopLevelEmitter {
|
|
|
703
719
|
unsupported(_raw, _reason) {
|
|
704
720
|
return "''";
|
|
705
721
|
}
|
|
706
|
-
objectLiteral(
|
|
707
|
-
return "''";
|
|
722
|
+
objectLiteral(properties, _raw, _emit) {
|
|
723
|
+
return properties.length === 0 ? "{}" : "''";
|
|
708
724
|
}
|
|
709
725
|
}
|
|
710
726
|
|
|
@@ -863,9 +879,7 @@ function recordIndexAccessToPerl(ctx, val) {
|
|
|
863
879
|
// src/adapter/memo/seed.ts
|
|
864
880
|
import {
|
|
865
881
|
collectContextConsumers,
|
|
866
|
-
|
|
867
|
-
isSupported,
|
|
868
|
-
parseExpression as parseExpression2
|
|
882
|
+
computeSsrSeedPlan
|
|
869
883
|
} from "@barefootjs/jsx";
|
|
870
884
|
function contextDefaultPerl(c) {
|
|
871
885
|
const d = c.defaultValue;
|
|
@@ -886,42 +900,20 @@ function generateContextConsumerSeed(ir) {
|
|
|
886
900
|
`;
|
|
887
901
|
}
|
|
888
902
|
function generateDerivedMemoSeed(ctx, ir) {
|
|
889
|
-
const
|
|
890
|
-
const signals = ir.metadata.signals ?? [];
|
|
891
|
-
if (memos.length === 0 && signals.length === 0)
|
|
892
|
-
return "";
|
|
893
|
-
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
903
|
+
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata);
|
|
894
904
|
const lines = [];
|
|
895
|
-
for (const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const body = extractArrowBodyExpression(memo.computation);
|
|
903
|
-
if (body !== null) {
|
|
904
|
-
const perl = tryLowerToPerl(ctx, body, available);
|
|
905
|
-
if (perl !== null)
|
|
906
|
-
lines.push(`% my $${memo.name} = ${perl};`);
|
|
907
|
-
}
|
|
908
|
-
available.add(memo.name);
|
|
905
|
+
for (const step of plan.steps) {
|
|
906
|
+
if (step.kind !== "derived")
|
|
907
|
+
continue;
|
|
908
|
+
const perl = ctx.convertExpressionToPerl(step.expr, step.parsed);
|
|
909
|
+
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
910
|
+
continue;
|
|
911
|
+
lines.push(`% my $${step.name} = ${perl};`);
|
|
909
912
|
}
|
|
910
913
|
return lines.length > 0 ? lines.join(`
|
|
911
914
|
`) + `
|
|
912
915
|
` : "";
|
|
913
916
|
}
|
|
914
|
-
function tryLowerToPerl(ctx, expr, available) {
|
|
915
|
-
const trimmed = expr.trim();
|
|
916
|
-
if (!trimmed)
|
|
917
|
-
return null;
|
|
918
|
-
if (!isSupported(parseExpression2(trimmed)).supported)
|
|
919
|
-
return null;
|
|
920
|
-
const perl = ctx.convertExpressionToPerl(trimmed);
|
|
921
|
-
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
922
|
-
return null;
|
|
923
|
-
return referencedVarsAreAvailable(perl, available) ? perl : null;
|
|
924
|
-
}
|
|
925
917
|
|
|
926
918
|
// src/adapter/value/parsed-literal.ts
|
|
927
919
|
function isStringTypeInfo(type) {
|
|
@@ -963,6 +955,17 @@ function collectStringValueNames(ir) {
|
|
|
963
955
|
}
|
|
964
956
|
|
|
965
957
|
// src/adapter/mojo-adapter.ts
|
|
958
|
+
function perlSegmentAccessor(base, segments) {
|
|
959
|
+
let expr = base;
|
|
960
|
+
for (const seg of segments) {
|
|
961
|
+
expr += seg.kind === "field" ? `->{${seg.isIdent ? seg.key : perlHashKey(seg.key)}}` : `->[${seg.index}]`;
|
|
962
|
+
}
|
|
963
|
+
return expr;
|
|
964
|
+
}
|
|
965
|
+
function perlStringLiteral(s) {
|
|
966
|
+
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
967
|
+
}
|
|
968
|
+
|
|
966
969
|
class MojoAdapter extends BaseAdapter {
|
|
967
970
|
name = "mojolicious";
|
|
968
971
|
extension = ".html.ep";
|
|
@@ -1043,7 +1046,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
1043
1046
|
return this.booleanTypedProps.has(bare);
|
|
1044
1047
|
}
|
|
1045
1048
|
parseUndefinedAlternateTernary(expr) {
|
|
1046
|
-
const parsed =
|
|
1049
|
+
const parsed = parseExpression2(expr.trim());
|
|
1047
1050
|
if (parsed?.kind !== "conditional")
|
|
1048
1051
|
return null;
|
|
1049
1052
|
const alt = parsed.alternate;
|
|
@@ -1291,24 +1294,41 @@ ${whenTrue}
|
|
|
1291
1294
|
return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
|
|
1292
1295
|
}
|
|
1293
1296
|
renderLoop(loop) {
|
|
1294
|
-
if (loop.clientOnly)
|
|
1295
|
-
return ""
|
|
1297
|
+
if (loop.clientOnly) {
|
|
1298
|
+
return `<%== bf->comment("loop:${loop.markerId}") %><%== bf->comment("/loop:${loop.markerId}") %>`;
|
|
1299
|
+
}
|
|
1296
1300
|
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
|
|
1297
|
-
const supportableDestructure = destructure &&
|
|
1301
|
+
const supportableDestructure = destructure && isLowerableLoopDestructure(loop);
|
|
1298
1302
|
if (destructure && !supportableDestructure) {
|
|
1299
1303
|
this.errors.push({
|
|
1300
1304
|
code: "BF104",
|
|
1301
1305
|
severity: "error",
|
|
1302
|
-
message: `Loop callback uses
|
|
1306
|
+
message: `Loop callback uses a destructure pattern (\`${loop.param}\`) that the Mojo adapter cannot lower — see the diagnostic detail for the specific shape.`,
|
|
1303
1307
|
loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1304
1308
|
suggestion: {
|
|
1305
1309
|
message: `Options:
|
|
1306
|
-
` + ` 1.
|
|
1307
|
-
` + ` 2.
|
|
1308
|
-
` + ` 3.
|
|
1310
|
+
` + ` 1. If this is an object-rest binding (\`{ ...rest }\`), only reading \`rest.field\` or spreading \`{...rest}\` onto an intrinsic element lowers — other uses (passing \`rest\` to a function, rendering it as text) need the client runtime.
|
|
1311
|
+
` + ` 2. If this is chained \`.filter().map(({ ... }) => ...)\`, hoist the destructure into a variable inside the callback body instead.
|
|
1312
|
+
` + ` 3. Mark the loop position as @client-only so the destructure runs in JS on the client.
|
|
1313
|
+
` + ` 4. Move the loop into a primitive that the adapter registers explicitly.`
|
|
1309
1314
|
}
|
|
1310
1315
|
});
|
|
1311
1316
|
}
|
|
1317
|
+
const arrayName = loop.array.trim();
|
|
1318
|
+
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
1319
|
+
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
1320
|
+
if (arrayConst && !arrayConst.isModule && this.resolveLiteralConst(arrayName) === null) {
|
|
1321
|
+
this.errors.push({
|
|
1322
|
+
code: "BF101",
|
|
1323
|
+
severity: "error",
|
|
1324
|
+
message: `Loop array \`${arrayName}\` is a local computed value (\`${arrayConst.value}\`) that the Mojo adapter cannot bind as a template variable — only numeric/string-literal locals inline at their use site.`,
|
|
1325
|
+
loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1326
|
+
suggestion: {
|
|
1327
|
+
message: "Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client."
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1312
1332
|
const rawArray = this.convertExpressionToPerl(loop.array);
|
|
1313
1333
|
let sortedHoist = null;
|
|
1314
1334
|
let array = rawArray;
|
|
@@ -1363,7 +1383,15 @@ ${renderedChildren}` : renderedChildren;
|
|
|
1363
1383
|
if (supportableDestructure) {
|
|
1364
1384
|
lines.push(`% my $__bf_item = ${array}->[${indexVar}];`);
|
|
1365
1385
|
for (const b of loop.paramBindings ?? []) {
|
|
1366
|
-
|
|
1386
|
+
const parent = perlSegmentAccessor("$__bf_item", b.segments ?? []);
|
|
1387
|
+
if (b.rest?.kind === "object") {
|
|
1388
|
+
const exclude = b.rest.exclude.map((k) => perlStringLiteral(k.key)).join(", ");
|
|
1389
|
+
lines.push(`% my $${b.name} = bf->omit(${parent}, [${exclude}]);`);
|
|
1390
|
+
} else if (b.rest?.kind === "array") {
|
|
1391
|
+
lines.push(`% my $${b.name} = bf->slice(${parent}, ${b.rest.from}, undef);`);
|
|
1392
|
+
} else {
|
|
1393
|
+
lines.push(`% my $${b.name} = ${perlSegmentAccessor("$__bf_item", b.segments ?? [])};`);
|
|
1394
|
+
}
|
|
1367
1395
|
}
|
|
1368
1396
|
} else {
|
|
1369
1397
|
lines.push(`% my $${param} = ${array}->[${indexVar}];`);
|
|
@@ -1547,7 +1575,7 @@ ${children}`;
|
|
|
1547
1575
|
if (localConst?.value !== undefined) {
|
|
1548
1576
|
const initTrimmed = localConst.value.trim();
|
|
1549
1577
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
1550
|
-
const resolved = conditionalSpreadToPerl(this.spreadCtx,
|
|
1578
|
+
const resolved = conditionalSpreadToPerl(this.spreadCtx, parseExpression2(initTrimmed));
|
|
1551
1579
|
if (resolved !== null) {
|
|
1552
1580
|
return `<%== bf->spread_attrs(${resolved}) %>`;
|
|
1553
1581
|
}
|
|
@@ -1565,7 +1593,7 @@ ${children}`;
|
|
|
1565
1593
|
if (!entries)
|
|
1566
1594
|
return null;
|
|
1567
1595
|
for (const e of entries) {
|
|
1568
|
-
if (e.kind === "expr" && !
|
|
1596
|
+
if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
|
|
1569
1597
|
return null;
|
|
1570
1598
|
}
|
|
1571
1599
|
return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<%= ${this.convertExpressionToPerl(e.expr)} %>`).join(";");
|
|
@@ -1601,7 +1629,7 @@ ${children}`;
|
|
|
1601
1629
|
return `${BF_COND}="${condId}"`;
|
|
1602
1630
|
}
|
|
1603
1631
|
renderPerlFilterExpr(expr, param, localVarMap = new Map) {
|
|
1604
|
-
return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
|
|
1632
|
+
return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
1605
1633
|
}
|
|
1606
1634
|
convertTemplateLiteralPartsToPerl(literalParts) {
|
|
1607
1635
|
const parts = [];
|
|
@@ -1646,8 +1674,8 @@ ${children}`;
|
|
|
1646
1674
|
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
1647
1675
|
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
1648
1676
|
return false;
|
|
1649
|
-
const parsed =
|
|
1650
|
-
const support =
|
|
1677
|
+
const parsed = parseExpression2(expr.trim());
|
|
1678
|
+
const support = isSupported(parsed);
|
|
1651
1679
|
if (parsed.kind !== "unsupported" && support.supported)
|
|
1652
1680
|
return false;
|
|
1653
1681
|
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
@@ -1695,7 +1723,7 @@ ${reason}` : "";
|
|
|
1695
1723
|
const trimmed = expr.trim();
|
|
1696
1724
|
if (trimmed === "")
|
|
1697
1725
|
return "''";
|
|
1698
|
-
parsed =
|
|
1726
|
+
parsed = parseExpression2(trimmed);
|
|
1699
1727
|
}
|
|
1700
1728
|
if (parsed.kind === "call") {
|
|
1701
1729
|
for (const matcher of this._loweringMatchers) {
|
|
@@ -1704,9 +1732,13 @@ ${reason}` : "";
|
|
|
1704
1732
|
const argsGo = queryHrefArgs(node, (n) => this.renderParsedExprToPerl(n));
|
|
1705
1733
|
return `bf->query(${argsGo.join(", ")})`;
|
|
1706
1734
|
}
|
|
1735
|
+
if (node?.kind === "helper-call" && isValidHelperId(node.helper)) {
|
|
1736
|
+
const argsX = node.args.map((a) => this.renderParsedExprToPerl(a));
|
|
1737
|
+
return `bf->${node.helper}(${argsX.join(", ")})`;
|
|
1738
|
+
}
|
|
1707
1739
|
}
|
|
1708
1740
|
}
|
|
1709
|
-
const support =
|
|
1741
|
+
const support = isSupported(parsed);
|
|
1710
1742
|
if (!support.supported) {
|
|
1711
1743
|
this.errors.push({
|
|
1712
1744
|
code: "BF101",
|
|
@@ -1755,7 +1787,23 @@ Options:
|
|
|
1755
1787
|
}
|
|
1756
1788
|
}
|
|
1757
1789
|
var mojoAdapter = new MojoAdapter;
|
|
1790
|
+
// src/conformance-pins.ts
|
|
1791
|
+
var conformancePins = {
|
|
1792
|
+
"static-array-children": [{ code: "BF103", severity: "error" }],
|
|
1793
|
+
"todo-app": [{ code: "BF103", severity: "error" }],
|
|
1794
|
+
"todo-app-ssr": [{ code: "BF103", severity: "error" }],
|
|
1795
|
+
"static-array-from-props": [{ code: "BF101", severity: "error" }],
|
|
1796
|
+
"static-array-from-props-with-component": [
|
|
1797
|
+
{ code: "BF103", severity: "error" },
|
|
1798
|
+
{ code: "BF101", severity: "error" }
|
|
1799
|
+
],
|
|
1800
|
+
"filter-nested-find-predicate": [
|
|
1801
|
+
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
1802
|
+
],
|
|
1803
|
+
"array-map-function-reference": [{ code: "BF101", severity: "error" }]
|
|
1804
|
+
};
|
|
1758
1805
|
export {
|
|
1759
1806
|
mojoAdapter,
|
|
1807
|
+
conformancePins,
|
|
1760
1808
|
MojoAdapter
|
|
1761
1809
|
};
|
|
@@ -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,
|
|
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,CAwNjF;AAqWD;;;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 BarefootJS::Backend::Mojo;
|
|
2
|
-
our $VERSION = "0.
|
|
2
|
+
our $VERSION = "0.17.1";
|
|
3
3
|
use Mojo::Base -base, -signatures;
|
|
4
4
|
|
|
5
5
|
use Mojo::ByteStream qw(b);
|
|
@@ -78,13 +78,22 @@ sub materialize ($self, $value) {
|
|
|
78
78
|
# instance for that render. The Mojo `bf` helper resolves the current
|
|
79
79
|
# instance off `$c->stash->{'bf.instance'}`; swap it for the duration of
|
|
80
80
|
# the nested render and restore it afterwards so sibling renders are
|
|
81
|
-
# unaffected.
|
|
81
|
+
# unaffected. `local` (rather than a manual swap) keeps the restore
|
|
82
|
+
# exception-safe: a die inside the nested render — e.g. a grandchild with
|
|
83
|
+
# no registered renderer — propagates without leaving the child instance
|
|
84
|
+
# bound as the request's active one.
|
|
82
85
|
sub render_named ($self, $template_name, $child_bf, $vars) {
|
|
83
86
|
my $c = $self->c;
|
|
84
|
-
|
|
85
|
-
$c->stash->{'bf.instance'} = $child_bf;
|
|
87
|
+
local $c->stash->{'bf.instance'} = $child_bf;
|
|
86
88
|
my $html = $c->render_to_string(template => $template_name, %$vars);
|
|
87
|
-
|
|
89
|
+
# `render_to_string` returns undef — it does NOT die — when the named
|
|
90
|
+
# template can't be rendered (typically: the template file is missing
|
|
91
|
+
# from the renderer paths). The calling template's `<%==` would emit
|
|
92
|
+
# that as an empty string, silently dropping the whole child subtree
|
|
93
|
+
# from the page (#2132). Fail the render loudly instead.
|
|
94
|
+
die qq{BarefootJS: child template "$template_name" rendered no output }
|
|
95
|
+
. qq{(is the template missing from the renderer paths?)\n}
|
|
96
|
+
unless defined $html;
|
|
88
97
|
return $html;
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
package Mojolicious::Plugin::BarefootJS;
|
|
2
|
-
our $VERSION = "0.
|
|
2
|
+
our $VERSION = "0.17.1";
|
|
3
3
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
4
4
|
|
|
5
5
|
use Mojo::File qw(path);
|
|
@@ -31,28 +31,64 @@ sub register ($self, $app, $config = {}) {
|
|
|
31
31
|
$c->stash->{'bf.instance'} //= BarefootJS->new($c, $config);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# `
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
return if exists $config->{manifest_path} && !defined $config->{manifest_path};
|
|
35
|
+
my $manifest_path = path(
|
|
36
|
+
$config->{manifest_path} // $app->home->child('dist/templates/manifest.json'));
|
|
37
|
+
|
|
38
|
+
# Manifest resolution is lazy, re-checked per render (#2126). The
|
|
39
|
+
# scaffold's dev script starts `bf build --watch` and the web server
|
|
40
|
+
# concurrently, so the app routinely boots before the first build has
|
|
41
|
+
# written `manifest.json`. Loading once at register time turned that
|
|
42
|
+
# startup race into a permanent failure: auto-init stayed disabled for
|
|
43
|
+
# the server's lifetime and every top-level render died under strict
|
|
44
|
+
# (`Global symbol "$initial" requires explicit package name`). The
|
|
45
|
+
# cache is keyed on the file's (mtime, size), so the steady-state cost
|
|
46
|
+
# is one stat() per render — and `bf build --watch` rebuilds (new
|
|
47
|
+
# ssrDefaults, `bf add`ed components) are picked up without a restart.
|
|
48
|
+
my ($cached_sig, $manifest, $is_child_entry);
|
|
49
|
+
my $load_manifest = sub {
|
|
50
|
+
my ($size, $mtime) = (stat "$manifest_path")[7, 9];
|
|
51
|
+
unless (defined $mtime) {
|
|
52
|
+
# Missing (or unreadable) — drop the cache so the manifest is
|
|
53
|
+
# re-read as soon as the first build writes it.
|
|
54
|
+
($cached_sig, $manifest, $is_child_entry) = (undef, undef, undef);
|
|
55
|
+
return undef;
|
|
56
|
+
}
|
|
57
|
+
my $sig = "$mtime/$size";
|
|
58
|
+
return $manifest if defined $cached_sig && $cached_sig eq $sig;
|
|
59
|
+
# Cache parse failures under the same signature so a broken file
|
|
60
|
+
# warns once, not on every render, and retries when it changes.
|
|
61
|
+
$cached_sig = $sig;
|
|
62
|
+
($manifest, $is_child_entry) = (undef, undef);
|
|
63
|
+
my $m = eval { decode_json($manifest_path->slurp) };
|
|
64
|
+
if ($@ || ref($m) ne 'HASH') {
|
|
65
|
+
$app->log->warn("BarefootJS: cannot parse manifest at $manifest_path: $@") if $@;
|
|
66
|
+
return undef;
|
|
67
|
+
}
|
|
68
|
+
$manifest = $m;
|
|
69
|
+
|
|
70
|
+
# Cache the set of UI-registry slot keys so we can answer
|
|
71
|
+
# "is this template name a child or a top-level page?" with a
|
|
72
|
+
# single hash lookup at render time. Top-level entries are
|
|
73
|
+
# everything that isn't `__barefoot__` and doesn't match
|
|
74
|
+
# `ui/<name>/index` — the same partition `register_components_from_manifest`
|
|
75
|
+
# applies internally.
|
|
76
|
+
$is_child_entry = {};
|
|
77
|
+
for my $entry_name (keys %$manifest) {
|
|
78
|
+
next if $entry_name eq '__barefoot__';
|
|
79
|
+
next unless $entry_name =~ m{^ui/[^/]+/index$};
|
|
80
|
+
$is_child_entry->{$entry_name} = 1;
|
|
81
|
+
}
|
|
82
|
+
return $manifest;
|
|
83
|
+
};
|
|
49
84
|
|
|
50
85
|
$app->hook(before_render => sub ($c, $args) {
|
|
51
86
|
my $template = $args->{template};
|
|
52
87
|
return unless defined $template && length $template;
|
|
53
|
-
my $
|
|
88
|
+
my $m = $load_manifest->() or return;
|
|
89
|
+
my $entry = $m->{$template};
|
|
54
90
|
return unless $entry;
|
|
55
|
-
return if $is_child_entry{$template};
|
|
91
|
+
return if $is_child_entry->{$template};
|
|
56
92
|
# Idempotency guard for nested renders. A controller might
|
|
57
93
|
# call `render_to_string` inside an action and then `render`
|
|
58
94
|
# — without this we'd re-init `bf` on the second pass and
|
|
@@ -72,7 +108,7 @@ sub register ($self, $app, $config = {}) {
|
|
|
72
108
|
$c->stash->{'bf.auto_init_done'} = 1;
|
|
73
109
|
|
|
74
110
|
$bf->_scope_id($template . '_' . substr(rand() =~ s/^0\.//r, 0, 6));
|
|
75
|
-
$bf->register_components_from_manifest($
|
|
111
|
+
$bf->register_components_from_manifest($m);
|
|
76
112
|
|
|
77
113
|
# Seed each ssrDefault into the stash unless the caller has
|
|
78
114
|
# already supplied a value for that key — callers always win.
|
|
@@ -98,20 +134,6 @@ sub register ($self, $app, $config = {}) {
|
|
|
98
134
|
});
|
|
99
135
|
}
|
|
100
136
|
|
|
101
|
-
sub _load_manifest ($app, $config) {
|
|
102
|
-
return undef if exists $config->{manifest_path} && !defined $config->{manifest_path};
|
|
103
|
-
my $manifest_path = $config->{manifest_path}
|
|
104
|
-
// $app->home->child('dist/templates/manifest.json');
|
|
105
|
-
my $file = path($manifest_path);
|
|
106
|
-
return undef unless -r $file;
|
|
107
|
-
my $manifest = eval { decode_json($file->slurp) };
|
|
108
|
-
if ($@ || ref($manifest) ne 'HASH') {
|
|
109
|
-
$app->log->warn("BarefootJS: cannot parse manifest at $file: $@") if $@;
|
|
110
|
-
return undef;
|
|
111
|
-
}
|
|
112
|
-
return $manifest;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
137
|
1;
|
|
116
138
|
__END__
|
|
117
139
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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.18.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.18.0"
|
|
64
64
|
}
|
|
65
65
|
}
|