@barefootjs/mojolicious 0.5.0 → 0.5.1

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.
@@ -400,8 +400,10 @@ ${whenTrue}
400
400
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
401
401
  const prevInLoop = this.inLoop;
402
402
  this.inLoop = true;
403
- const children = this.renderChildren(loop.children);
403
+ const renderedChildren = this.renderChildren(loop.children);
404
404
  this.inLoop = prevInLoop;
405
+ const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
406
+ ${renderedChildren}` : renderedChildren;
405
407
  const lines = [];
406
408
  lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
407
409
  if (sortedHoist && loop.sortComparator) {
@@ -1 +1 @@
1
- {"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
package/dist/build.js CHANGED
@@ -400,8 +400,10 @@ ${whenTrue}
400
400
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
401
401
  const prevInLoop = this.inLoop;
402
402
  this.inLoop = true;
403
- const children = this.renderChildren(loop.children);
403
+ const renderedChildren = this.renderChildren(loop.children);
404
404
  this.inLoop = prevInLoop;
405
+ const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
406
+ ${renderedChildren}` : renderedChildren;
405
407
  const lines = [];
406
408
  lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
407
409
  if (sortedHoist && loop.sortComparator) {
package/dist/index.js CHANGED
@@ -400,8 +400,10 @@ ${whenTrue}
400
400
  const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
401
401
  const prevInLoop = this.inLoop;
402
402
  this.inLoop = true;
403
- const children = this.renderChildren(loop.children);
403
+ const renderedChildren = this.renderChildren(loop.children);
404
404
  this.inLoop = prevInLoop;
405
+ const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
406
+ ${renderedChildren}` : renderedChildren;
405
407
  const lines = [];
406
408
  lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
407
409
  if (sortedHoist && loop.sortComparator) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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,14 +52,14 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.5.0"
55
+ "@barefootjs/shared": "0.5.1"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@barefootjs/jsx": ">=0.2.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@barefootjs/adapter-tests": "0.1.0",
62
- "@barefootjs/jsx": "0.5.0",
62
+ "@barefootjs/jsx": "0.5.1",
63
63
  "typescript": "^5.0.0"
64
64
  }
65
65
  }
@@ -69,6 +69,19 @@ runAdapterConformanceTests({
69
69
  'toggle-shared',
70
70
  'reactive-props',
71
71
  'props-reactivity-comparison',
72
+ // #1665 whole-item loop conditional. The Mojo adapter correctly emits the
73
+ // per-item `<!--bf-loop-i:KEY-->` anchor, `data-key`, and the conditional
74
+ // markers (verified by template-structure tests), but the fixture's
75
+ // `sel() === t.id` string comparison lowers to Perl numeric `==`
76
+ // (`"b" == "a"` → `0 == 0` → true), so the perl-executed render renders
77
+ // every item's true branch instead of only the matching one. Selecting
78
+ // `eq` vs `==` from operand types is a separate pre-existing Mojo
79
+ // limitation (same family as the skipped `logical-or-jsx` /
80
+ // `nullish-coalescing-jsx` map shapes); the anchored SSR shape itself is
81
+ // covered cross-adapter by Hono + CSR conformance and the runtime
82
+ // hydration tests. Remove this skip once the Mojo limitation is fixed:
83
+ // https://github.com/piconic-ai/barefootjs/issues/1672
84
+ 'loop-item-conditional',
72
85
  ],
73
86
  // Per-fixture build-time contracts for shapes the Mojo adapter
74
87
  // intentionally refuses to lower. Owned by this adapter test file
@@ -958,3 +971,165 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
958
971
  })
959
972
  }
960
973
  })
974
+
975
+ // =============================================================================
976
+ // #1448 — `/* @client */` escape hatch for STILL-UNSUPPORTED methods
977
+ // =============================================================================
978
+ //
979
+ // Mojo sibling of the Go block: #1448 documents `/* @client */` as the
980
+ // universal workaround for any Array/String method the template
981
+ // adapters can't lower. This pins that contract for the Mojo adapter —
982
+ // wrapping the unsupported expression in the directive must clear the
983
+ // BF021/BF101 build error the bare form raises and emit a client-only
984
+ // placeholder so the Mojo SSR pass renders valid `.html.ep` that the
985
+ // client runtime fills at hydration.
986
+ //
987
+ // Same silent-footgun caveat as Go: the unsupported *string* methods
988
+ // raise NO build diagnostic — bare `.startsWith` / `.repeat` / … lower
989
+ // to a Perl hash-deref-and-call (`$name->{startsWith}('a')`) that
990
+ // passes the adapter gate, then dies at render with
991
+ // `Can't use string (...) as a HASH ref while "strict refs"`.
992
+ // `/* @client */` is the only escape hatch, so these tests pin it.
993
+ describe('MojoAdapter - #1448 @client escape hatch (unsupported methods)', () => {
994
+ function emit(expr: string, client: boolean) {
995
+ const marker = client ? '/* @client */ ' : ''
996
+ const adapter = new MojoAdapter()
997
+ const ir = compileToIR(`
998
+ "use client"
999
+ import { createSignal } from "@barefootjs/client"
1000
+ export function C() {
1001
+ const [items, setItems] = createSignal<{ name: string; n: number; tags: string[] }[]>([])
1002
+ const [name, setName] = createSignal("x")
1003
+ return <div>{${marker}${expr}}</div>
1004
+ }
1005
+ `, adapter)
1006
+ const template = adapter.generate(ir).template ?? ''
1007
+ return { errors: adapter.errors ?? [], template }
1008
+ }
1009
+
1010
+ function emitLoop(chain: string, client: boolean) {
1011
+ const marker = client ? '/* @client */ ' : ''
1012
+ const adapter = new MojoAdapter()
1013
+ const ir = compileToIR(`
1014
+ "use client"
1015
+ import { createSignal } from "@barefootjs/client"
1016
+ export function C() {
1017
+ const [items, setItems] = createSignal<{ name: string; n: number }[]>([])
1018
+ const myCmp = (a: { n: number }, b: { n: number }) => a.n - b.n
1019
+ return <ul>{${marker}${chain}}</ul>
1020
+ }
1021
+ `, adapter)
1022
+ const template = adapter.generate(ir).template ?? ''
1023
+ return { errors: adapter.errors ?? [], template }
1024
+ }
1025
+
1026
+ // Tier C array methods — bare form raises BF101 at build time.
1027
+ const unsupportedArray: Array<[string, string]> = [
1028
+ ['reduce', `items().reduce((a, b) => a + b.n, 0)`],
1029
+ ['flatMap', `items().flatMap(i => i.tags)`],
1030
+ ['flat', `items().flat()`],
1031
+ ]
1032
+ for (const [name, expr] of unsupportedArray) {
1033
+ test(`array .${name}: bare raises BF101, @client clears it + emits client placeholder`, () => {
1034
+ const bare = emit(expr, false)
1035
+ expect(bare.errors.some(e => e.code === 'BF101')).toBe(true)
1036
+
1037
+ const guarded = emit(expr, true)
1038
+ expect(guarded.errors).toEqual([])
1039
+ // Client-only text slot → `<%== bf->comment("client:sN") %>`.
1040
+ expect(guarded.template).toMatch(/bf->comment\("client:s\d+"\)/)
1041
+ })
1042
+ }
1043
+
1044
+ // Tier B/C string methods — bare form emits an INVALID Perl
1045
+ // hash-deref-and-call with NO build error (the silent footgun).
1046
+ const unsupportedString: Array<[string, string, string]> = [
1047
+ ['split', `name().split(",")`, '->{split}'],
1048
+ ['startsWith', `name().startsWith("a")`, '->{startsWith}'],
1049
+ ['endsWith', `name().endsWith("z")`, '->{endsWith}'],
1050
+ ['replace', `name().replace("a", "b")`, '->{replace}'],
1051
+ ['repeat', `name().repeat(3)`, '->{repeat}'],
1052
+ ['padStart', `name().padStart(5, "0")`, '->{padStart}'],
1053
+ ['padEnd', `name().padEnd(5, "0")`, '->{padEnd}'],
1054
+ ['charAt', `name().charAt(0)`, '->{charAt}'],
1055
+ ]
1056
+ for (const [name, expr, badEmit] of unsupportedString) {
1057
+ test(`string .${name}: bare emits invalid Perl deref, @client emits client placeholder`, () => {
1058
+ const bare = emit(expr, false)
1059
+ // Documents the footgun: no BF101 guard, invalid template emitted.
1060
+ expect(bare.errors.filter(e => e.code === 'BF101')).toEqual([])
1061
+ expect(bare.template).toContain(badEmit)
1062
+
1063
+ const guarded = emit(expr, true)
1064
+ expect(guarded.errors).toEqual([])
1065
+ expect(guarded.template).toMatch(/bf->comment\("client:s\d+"\)/)
1066
+ expect(guarded.template).not.toContain(badEmit)
1067
+ })
1068
+ }
1069
+
1070
+ // Tier B `.sort` / `.toSorted` follow-ups still refused with BF021.
1071
+ // The Mojo client-only loop placeholder is an empty element (the
1072
+ // client runtime repopulates it via the `bf-s` scope marker), so the
1073
+ // contract here is: no errors + the comparator never lowers + no
1074
+ // rendered `<li>` survives.
1075
+ const unsupportedSort: Array<[string, string]> = [
1076
+ ['function-reference comparator', `items().toSorted(myCmp).map(x => <li key={x.name}>{x.name}</li>)`],
1077
+ ['localeCompare locale/options arg', `items().toSorted((a, b) => a.name.localeCompare(b.name, "ja", { numeric: true })).map(x => <li key={x.name}>{x.name}</li>)`],
1078
+ ]
1079
+ for (const [label, chain] of unsupportedSort) {
1080
+ test(`sort follow-up (${label}): bare raises BF021, @client clears it`, () => {
1081
+ const bare = compileJSX(`
1082
+ "use client"
1083
+ import { createSignal } from "@barefootjs/client"
1084
+ export function C() {
1085
+ const [items, setItems] = createSignal<{ name: string; n: number }[]>([])
1086
+ const myCmp = (a: { n: number }, b: { n: number }) => a.n - b.n
1087
+ return <ul>{${chain}}</ul>
1088
+ }
1089
+ `.trimStart(), 'test.tsx', { adapter: new MojoAdapter() })
1090
+ expect(bare.errors?.some(e => e.code === 'BF021')).toBe(true)
1091
+
1092
+ const guarded = emitLoop(chain, true)
1093
+ expect(guarded.errors).toEqual([])
1094
+ // Empty client-only loop placeholder — no item rows emitted SSR.
1095
+ expect(guarded.template).not.toContain('<li')
1096
+ expect(guarded.template).not.toContain('localeCompare')
1097
+ })
1098
+ }
1099
+
1100
+ // End-to-end proof via perl + Mojolicious: the bare unsupported form
1101
+ // crashes Mojo template execution, while the `@client` form renders a
1102
+ // `<!--bf-client:sN-->` placeholder. Skipped on hosts without
1103
+ // Mojolicious installed.
1104
+ test('e2e: bare string method crashes perl render, @client renders placeholder', async () => {
1105
+ const guarded = `
1106
+ "use client"
1107
+ import { createSignal } from "@barefootjs/client"
1108
+ export function C() {
1109
+ const [name, setName] = createSignal("hello")
1110
+ return <div>{/* @client */ name().repeat(3)}</div>
1111
+ }
1112
+ `
1113
+ const bareSrc = guarded.replace('/* @client */ ', '')
1114
+ try {
1115
+ const html = await renderMojoComponent({
1116
+ source: guarded.trimStart(),
1117
+ adapter: new MojoAdapter(),
1118
+ })
1119
+ expect(html).toContain('<!--bf-client:s0-->')
1120
+ } catch (err) {
1121
+ if (err instanceof PerlNotAvailableError) {
1122
+ console.log('Skipping #1448 @client e2e: perl/Mojolicious not found')
1123
+ return
1124
+ }
1125
+ throw err
1126
+ }
1127
+ // Bare form must fail Mojo template execution (no @client guard).
1128
+ await expect(
1129
+ renderMojoComponent({
1130
+ source: bareSrc.trimStart(),
1131
+ adapter: new MojoAdapter(),
1132
+ }),
1133
+ ).rejects.toThrow(/HASH ref/)
1134
+ })
1135
+ })
@@ -598,9 +598,19 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
598
598
  : loop.index ? `$${loop.index}` : '$_i'
599
599
  const prevInLoop = this.inLoop
600
600
  this.inLoop = true
601
- const children = this.renderChildren(loop.children)
601
+ const renderedChildren = this.renderChildren(loop.children)
602
602
  this.inLoop = prevInLoop
603
603
 
604
+ // Whole-item conditional (#1665): prepend an always-present
605
+ // `<!--bf-loop-i:KEY-->` anchor before each item's (possibly empty)
606
+ // conditional content so the client's `mapArrayAnchored` can hydrate
607
+ // every SSR-rendered item by its anchor. `bf->comment` prepends `bf-`,
608
+ // so `"loop-i:" . KEY` yields `<!--bf-loop-i:KEY-->`.
609
+ const children =
610
+ loop.bodyIsItemConditional && loop.key
611
+ ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>\n${renderedChildren}`
612
+ : renderedChildren
613
+
604
614
  const lines: string[] = []
605
615
  // Scoped per-call-site marker so sibling `.map()`s under the same parent
606
616
  // each get their own reconciliation range (#1087).