@barefootjs/mojolicious 0.5.1 → 0.5.3
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 +106 -144
- package/dist/adapter/mojo-adapter.d.ts +16 -46
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +106 -144
- package/dist/index.js +106 -144
- package/dist/test-render.d.ts +5 -0
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/evaluate-signal-init.test.ts +35 -0
- package/src/__tests__/mojo-adapter.test.ts +189 -72
- package/src/adapter/mojo-adapter.ts +202 -293
- package/src/test-render.ts +62 -37
|
@@ -69,19 +69,16 @@ runAdapterConformanceTests({
|
|
|
69
69
|
'toggle-shared',
|
|
70
70
|
'reactive-props',
|
|
71
71
|
'props-reactivity-comparison',
|
|
72
|
-
// #
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
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
|
+
// #1467 Phase 2a: first `site/ui` source-root fixture. Button
|
|
73
|
+
// compiles cleanly on the Mojo adapter (no diagnostic), but its
|
|
74
|
+
// variant/size class composition (`Record<…>[key]` indexed object
|
|
75
|
+
// literals) and `applyRestAttrs` spread haven't been validated for
|
|
76
|
+
// byte-parity against the Hono reference under Mojo's template
|
|
77
|
+
// semantics. Cross-adapter parity for the `site/ui` corpus is
|
|
78
|
+
// explicitly Phase 3 ("cross-adapter parity (Mojo/Go templates)"),
|
|
79
|
+
// so Button participates only in Hono SSR conformance + the
|
|
80
|
+
// fixture-hydrate runtime layer for now.
|
|
81
|
+
'button',
|
|
85
82
|
],
|
|
86
83
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
87
84
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
@@ -289,6 +286,40 @@ export function List() {
|
|
|
289
286
|
expect(result.template).toMatch(/bf->comment\("\/loop:[^"]+"\)/)
|
|
290
287
|
})
|
|
291
288
|
|
|
289
|
+
test('compares string signals with Perl `eq`, not numeric `==` (#1672)', () => {
|
|
290
|
+
// `sel() === t.id` where `sel` is a string signal must lower to `eq`.
|
|
291
|
+
// Perl numeric `==` coerces non-numeric strings to 0, so `"b" == "a"` is
|
|
292
|
+
// true and every loop item would render its true branch.
|
|
293
|
+
const result = compileAndGenerate(`
|
|
294
|
+
"use client"
|
|
295
|
+
import { createSignal } from "@barefootjs/client"
|
|
296
|
+
|
|
297
|
+
export function LoopItemConditional() {
|
|
298
|
+
const [items] = createSignal([{ id: "a" }, { id: "b" }, { id: "c" }])
|
|
299
|
+
const [sel] = createSignal("b")
|
|
300
|
+
return <ul>{items().map(t => sel() === t.id && <li key={t.id}>{t.id}</li>)}</ul>
|
|
301
|
+
}
|
|
302
|
+
`)
|
|
303
|
+
expect(result.template).toContain('$sel eq $t->{id}')
|
|
304
|
+
expect(result.template).not.toContain('$sel == $t->{id}')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('compares number signals with Perl `==` (#1672)', () => {
|
|
308
|
+
// A numeric signal comparison must stay `==`, not flip to `eq`.
|
|
309
|
+
const result = compileAndGenerate(`
|
|
310
|
+
"use client"
|
|
311
|
+
import { createSignal } from "@barefootjs/client"
|
|
312
|
+
|
|
313
|
+
export function L() {
|
|
314
|
+
const [items] = createSignal([{ n: 1 }, { n: 2 }])
|
|
315
|
+
const [sel] = createSignal(2)
|
|
316
|
+
return <ul>{items().map(t => sel() === t.n && <li key={t.n}>{t.n}</li>)}</ul>
|
|
317
|
+
}
|
|
318
|
+
`)
|
|
319
|
+
expect(result.template).toContain('$sel == $t->{n}')
|
|
320
|
+
expect(result.template).not.toContain('$sel eq $t->{n}')
|
|
321
|
+
})
|
|
322
|
+
|
|
292
323
|
test('generates script registration for client components', () => {
|
|
293
324
|
const result = compileAndGenerate(`
|
|
294
325
|
"use client"
|
|
@@ -979,17 +1010,19 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
979
1010
|
// Mojo sibling of the Go block: #1448 documents `/* @client */` as the
|
|
980
1011
|
// universal workaround for any Array/String method the template
|
|
981
1012
|
// adapters can't lower. This pins that contract for the Mojo adapter —
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
// placeholder so the Mojo SSR pass renders valid `.html.ep`
|
|
985
|
-
//
|
|
1013
|
+
// the BARE form must surface a BF021/BF101 build error, and wrapping
|
|
1014
|
+
// the expression in the directive must clear it and emit a client-only
|
|
1015
|
+
// placeholder so the Mojo SSR pass renders valid `.html.ep` the client
|
|
1016
|
+
// runtime fills at hydration.
|
|
986
1017
|
//
|
|
987
|
-
//
|
|
988
|
-
// raise NO build diagnostic — bare `.startsWith` / `.repeat` / …
|
|
989
|
-
// to a Perl hash-deref-and-call
|
|
990
|
-
//
|
|
991
|
-
// `Can't use string (...) as a HASH ref while "strict
|
|
992
|
-
//
|
|
1018
|
+
// History (#1448 follow-up): the unsupported *string* methods used to
|
|
1019
|
+
// raise NO build diagnostic — bare `.startsWith` / `.repeat` / … fell
|
|
1020
|
+
// into the regex pipeline and lowered to a Perl hash-deref-and-call
|
|
1021
|
+
// (`$name->{startsWith}('a')`) that passed the gate, then died at
|
|
1022
|
+
// render with `Can't use string (...) as a HASH ref while "strict
|
|
1023
|
+
// refs"`. They are now routed through the AST path in
|
|
1024
|
+
// `convertExpressionToPerl` so `isSupported`'s `UNSUPPORTED_METHODS`
|
|
1025
|
+
// gate fires BF101 — parity with the Go adapter. These tests pin it.
|
|
993
1026
|
describe('MojoAdapter - #1448 @client escape hatch (unsupported methods)', () => {
|
|
994
1027
|
function emit(expr: string, client: boolean) {
|
|
995
1028
|
const marker = client ? '/* @client */ ' : ''
|
|
@@ -1023,49 +1056,85 @@ export function C() {
|
|
|
1023
1056
|
return { errors: adapter.errors ?? [], template }
|
|
1024
1057
|
}
|
|
1025
1058
|
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1059
|
+
// Unsupported methods that surface as BF101 at build time: Tier C
|
|
1060
|
+
// array methods + Tier B/C string methods. `badEmit` is the invalid
|
|
1061
|
+
// Perl fragment that must NOT survive into the template (the pre-fix
|
|
1062
|
+
// silent-footgun output for the string rows).
|
|
1063
|
+
const unsupported: Array<{ name: string; expr: string; badEmit: string }> = [
|
|
1064
|
+
// Tier C array methods.
|
|
1065
|
+
{ name: 'reduce', expr: `items().reduce((a, b) => a + b.n, 0)`, badEmit: '->{reduce}' },
|
|
1066
|
+
{ name: 'flatMap', expr: `items().flatMap(i => i.tags)`, badEmit: '->{flatMap}' },
|
|
1067
|
+
{ name: 'flat', expr: `items().flat()`, badEmit: '->{flat}' },
|
|
1068
|
+
// Tier B/C string methods — previously slipped through with no
|
|
1069
|
+
// diagnostic; now routed through the AST / `isSupported` gate.
|
|
1070
|
+
{ name: 'split', expr: `name().split(",")`, badEmit: '->{split}' },
|
|
1071
|
+
{ name: 'startsWith', expr: `name().startsWith("a")`, badEmit: '->{startsWith}' },
|
|
1072
|
+
{ name: 'endsWith', expr: `name().endsWith("z")`, badEmit: '->{endsWith}' },
|
|
1073
|
+
{ name: 'replace', expr: `name().replace("a", "b")`, badEmit: '->{replace}' },
|
|
1074
|
+
{ name: 'repeat', expr: `name().repeat(3)`, badEmit: '->{repeat}' },
|
|
1075
|
+
{ name: 'padStart', expr: `name().padStart(5, "0")`, badEmit: '->{padStart}' },
|
|
1076
|
+
{ name: 'padEnd', expr: `name().padEnd(5, "0")`, badEmit: '->{padEnd}' },
|
|
1077
|
+
{ name: 'charAt', expr: `name().charAt(0)`, badEmit: '->{charAt}' },
|
|
1031
1078
|
]
|
|
1032
|
-
for (const
|
|
1033
|
-
test(
|
|
1079
|
+
for (const { name, expr, badEmit } of unsupported) {
|
|
1080
|
+
test(`.${name}: bare raises BF101, @client clears it + emits client placeholder`, () => {
|
|
1034
1081
|
const bare = emit(expr, false)
|
|
1035
1082
|
expect(bare.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1083
|
+
// The invalid deref-and-call must NOT leak into the template;
|
|
1084
|
+
// the adapter degrades to a safe empty slot alongside the error.
|
|
1085
|
+
expect(bare.template).not.toContain(badEmit)
|
|
1036
1086
|
|
|
1037
1087
|
const guarded = emit(expr, true)
|
|
1038
1088
|
expect(guarded.errors).toEqual([])
|
|
1039
1089
|
// Client-only text slot → `<%== bf->comment("client:sN") %>`.
|
|
1040
1090
|
expect(guarded.template).toMatch(/bf->comment\("client:s\d+"\)/)
|
|
1091
|
+
expect(guarded.template).not.toContain(badEmit)
|
|
1041
1092
|
})
|
|
1042
1093
|
}
|
|
1043
1094
|
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1095
|
+
// Routing guard regression: the unsupported-string-method regex is an
|
|
1096
|
+
// unanchored substring test, and these names (`replace`, `split`, …)
|
|
1097
|
+
// are ordinary words that also appear inside string literals. A
|
|
1098
|
+
// SUPPORTED expression whose literal merely contains `.replace(` must
|
|
1099
|
+
// NOT be diverted onto the AST path — doing so would bypass
|
|
1100
|
+
// `rewriteTemplatePrimitives` and silently emit broken Perl
|
|
1101
|
+
// (`$JSON->{stringify} + '.replace('`). The `isSupported` gate on the
|
|
1102
|
+
// regex keeps such expressions on the normal pipeline.
|
|
1103
|
+
test('string-method regex does not misroute a supported expr with a method name inside a literal', () => {
|
|
1104
|
+
const adapter = new MojoAdapter()
|
|
1105
|
+
const ir = compileToIR(`
|
|
1106
|
+
"use client"
|
|
1107
|
+
import { createSignal } from "@barefootjs/client"
|
|
1108
|
+
export function C(props: { config: string }) {
|
|
1109
|
+
return <div>{JSON.stringify(props.config) + ".replace("}</div>
|
|
1110
|
+
}
|
|
1111
|
+
`, adapter)
|
|
1112
|
+
const template = adapter.generate(ir).template ?? ''
|
|
1113
|
+
expect(adapter.errors ?? []).toEqual([])
|
|
1114
|
+
// templatePrimitive lowering preserved...
|
|
1115
|
+
expect(template).toContain('bf->json($config)')
|
|
1116
|
+
// ...and the literal is NOT mangled into a hash-deref.
|
|
1117
|
+
expect(template).not.toContain('$JSON->{stringify}')
|
|
1118
|
+
})
|
|
1062
1119
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1120
|
+
// Predicate-level use of an unsupported string method also fails the
|
|
1121
|
+
// build loudly (intended): a `.filter(t => t.name.startsWith("a"))`
|
|
1122
|
+
// whose predicate calls one of the gated methods now refuses the whole
|
|
1123
|
+
// loop with BF101 (via the shared `isSupported` predicate gate in
|
|
1124
|
+
// jsx-to-ir) rather than lowering to a broken `->{startsWith}` inside
|
|
1125
|
+
// the grep. Pinning this so the loud-failure contract can't silently
|
|
1126
|
+
// regress back to the old emit-broken-template behaviour.
|
|
1127
|
+
test('unsupported string method inside a .filter() predicate raises BF101', () => {
|
|
1128
|
+
const result = compileJSX(`
|
|
1129
|
+
"use client"
|
|
1130
|
+
import { createSignal } from "@barefootjs/client"
|
|
1131
|
+
export function C() {
|
|
1132
|
+
const [items, setItems] = createSignal<{ name: string }[]>([])
|
|
1133
|
+
return <ul>{items().filter(t => t.name.startsWith("a")).map(t => <li key={t.name}>{t.name}</li>)}</ul>
|
|
1134
|
+
}
|
|
1135
|
+
`.trimStart(), 'test.tsx', { adapter: new MojoAdapter() })
|
|
1136
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
1137
|
+
})
|
|
1069
1138
|
|
|
1070
1139
|
// Tier B `.sort` / `.toSorted` follow-ups still refused with BF021.
|
|
1071
1140
|
// The Mojo client-only loop placeholder is an empty element (the
|
|
@@ -1097,23 +1166,25 @@ export function C() {
|
|
|
1097
1166
|
})
|
|
1098
1167
|
}
|
|
1099
1168
|
|
|
1100
|
-
// End-to-end proof via perl + Mojolicious: the
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
|
|
1105
|
-
|
|
1169
|
+
// End-to-end proof via perl + Mojolicious: the `@client` form renders
|
|
1170
|
+
// a `<!--bf-client:sN-->` placeholder. The bare form is now caught at
|
|
1171
|
+
// build with BF101 and degrades to an empty, render-safe slot (no
|
|
1172
|
+
// more `HASH ref` crash), so we assert the build error rather than a
|
|
1173
|
+
// render crash. Skipped on hosts without Mojolicious installed.
|
|
1174
|
+
test('e2e: @client renders placeholder; bare is caught at build with BF101', async () => {
|
|
1175
|
+
const bare = emit(`name().repeat(3)`, false)
|
|
1176
|
+
expect(bare.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1177
|
+
|
|
1178
|
+
try {
|
|
1179
|
+
const html = await renderMojoComponent({
|
|
1180
|
+
source: `
|
|
1106
1181
|
"use client"
|
|
1107
1182
|
import { createSignal } from "@barefootjs/client"
|
|
1108
1183
|
export function C() {
|
|
1109
1184
|
const [name, setName] = createSignal("hello")
|
|
1110
1185
|
return <div>{/* @client */ name().repeat(3)}</div>
|
|
1111
1186
|
}
|
|
1112
|
-
|
|
1113
|
-
const bareSrc = guarded.replace('/* @client */ ', '')
|
|
1114
|
-
try {
|
|
1115
|
-
const html = await renderMojoComponent({
|
|
1116
|
-
source: guarded.trimStart(),
|
|
1187
|
+
`.trimStart(),
|
|
1117
1188
|
adapter: new MojoAdapter(),
|
|
1118
1189
|
})
|
|
1119
1190
|
expect(html).toContain('<!--bf-client:s0-->')
|
|
@@ -1124,12 +1195,58 @@ export function C() {
|
|
|
1124
1195
|
}
|
|
1125
1196
|
throw err
|
|
1126
1197
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1198
|
+
})
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
// =============================================================================
|
|
1202
|
+
// #1682: parse-first expression lowering regressions
|
|
1203
|
+
// =============================================================================
|
|
1204
|
+
// The parse-first refactor routes every supported expression through the
|
|
1205
|
+
// AST emitter. These pin the four behaviours the Copilot review surfaced
|
|
1206
|
+
// so they can't silently regress.
|
|
1207
|
+
describe('MojoAdapter - #1682 parse-first lowering', () => {
|
|
1208
|
+
function gen(inner: string) {
|
|
1209
|
+
const adapter = new MojoAdapter()
|
|
1210
|
+
const out = compileAndGenerate(`
|
|
1211
|
+
"use client"
|
|
1212
|
+
import { createSignal } from "@barefootjs/client"
|
|
1213
|
+
export function C() {
|
|
1214
|
+
const [role, setRole] = createSignal("admin")
|
|
1215
|
+
const [count, setCount] = createSignal(1)
|
|
1216
|
+
const [obj, setObj] = createSignal({ a: 1 })
|
|
1217
|
+
const [it, setIt] = createSignal("x")
|
|
1218
|
+
return <div>{${inner}}</div>
|
|
1219
|
+
}
|
|
1220
|
+
`, adapter)
|
|
1221
|
+
return { template: out.template ?? '', errors: adapter.errors ?? [] }
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
test('string === lowers to Perl eq with the literal on EITHER operand', () => {
|
|
1225
|
+
// Reversed literal (`"admin" === role()`) must still use string `eq`,
|
|
1226
|
+
// not numeric `==` (which coerces both sides to 0 in Perl).
|
|
1227
|
+
const { template } = gen('"admin" === role() ? "A" : "B"')
|
|
1228
|
+
expect(template).toContain("'admin' eq $role")
|
|
1229
|
+
expect(template).not.toContain("'admin' ==")
|
|
1230
|
+
expect(template).not.toContain("== 'admin'")
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
test('template literal with a complex expr lowers to Perl concatenation', () => {
|
|
1234
|
+
// Double-quote interpolation would leave `+ 1` unevaluated; concat
|
|
1235
|
+
// (with parens for precedence) evaluates the arithmetic.
|
|
1236
|
+
const { template } = gen('`n=${count() + 1}`')
|
|
1237
|
+
expect(template).toContain('"n=" . ($count + 1)')
|
|
1238
|
+
})
|
|
1239
|
+
|
|
1240
|
+
test('static template-literal text escapes Perl $ and @ sigils', () => {
|
|
1241
|
+
const { template } = gen('`Price: $${it()} @user`')
|
|
1242
|
+
expect(template).toContain('\\$')
|
|
1243
|
+
expect(template).toContain('\\@user')
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
test('wrong-arity templatePrimitive records BF101 and emits no hash-deref', () => {
|
|
1247
|
+
const { template, errors } = gen('JSON.stringify(obj(), null)')
|
|
1248
|
+
expect(errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1249
|
+
// The invalid `$JSON->{stringify}` hash-deref must NOT leak out.
|
|
1250
|
+
expect(template).not.toContain('$JSON->{stringify}')
|
|
1134
1251
|
})
|
|
1135
1252
|
})
|