@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
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Generates Mojolicious EP template files (.html.ep) from BarefootJS IR.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import ts from 'typescript'
|
|
7
8
|
import type {
|
|
8
9
|
ComponentIR,
|
|
10
|
+
IRMetadata,
|
|
9
11
|
IRNode,
|
|
10
12
|
IRElement,
|
|
11
13
|
IRText,
|
|
@@ -117,6 +119,32 @@ export interface MojoAdapterOptions {
|
|
|
117
119
|
barefootJsPath?: string
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Parse a const initializer's source text. Returns the unescaped string
|
|
124
|
+
* value when the whole initializer is a single string literal (or a
|
|
125
|
+
* no-substitution template literal), else `null`. Uses the TS parser so
|
|
126
|
+
* escapes/quotes resolve exactly as JS would, matching the value the Hono
|
|
127
|
+
* reference inlines at runtime.
|
|
128
|
+
*/
|
|
129
|
+
function parsePureStringLiteral(source: string): string | null {
|
|
130
|
+
const sf = ts.createSourceFile(
|
|
131
|
+
'__const.ts',
|
|
132
|
+
`const __x = (${source});`,
|
|
133
|
+
ts.ScriptTarget.Latest,
|
|
134
|
+
/*setParentNodes*/ false,
|
|
135
|
+
)
|
|
136
|
+
const stmt = sf.statements[0]
|
|
137
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
138
|
+
const decl = stmt.declarationList.declarations[0]
|
|
139
|
+
let init = decl?.initializer
|
|
140
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
141
|
+
if (!init) return null
|
|
142
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
143
|
+
return init.text
|
|
144
|
+
}
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
120
148
|
export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRenderCtx> {
|
|
121
149
|
name = 'mojolicious'
|
|
122
150
|
extension = '.html.ep'
|
|
@@ -159,6 +187,43 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
159
187
|
* true — selecting the string operator from the operand's type avoids that.
|
|
160
188
|
*/
|
|
161
189
|
private stringValueNames: Set<string> = new Set()
|
|
190
|
+
/**
|
|
191
|
+
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
192
|
+
* file top-level), keyed by name → resolved literal value. Populated at
|
|
193
|
+
* `generate()` entry from `ir.metadata.localConstants`. When an identifier
|
|
194
|
+
* in an expression resolves to one of these, the adapter inlines the
|
|
195
|
+
* literal instead of emitting `$X` against a stash variable that is never
|
|
196
|
+
* bound (a module const isn't a prop, signal, or local — the value would
|
|
197
|
+
* render empty). Hono inlines it for free; this restores parity. Only
|
|
198
|
+
* module-scope pure string literals qualify (see `collectModuleStringConsts`).
|
|
199
|
+
*/
|
|
200
|
+
private moduleStringConsts: Map<string, string> = new Map()
|
|
201
|
+
/**
|
|
202
|
+
* Names currently bound by an enclosing loop body — the `my $<param>` and
|
|
203
|
+
* `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
|
|
204
|
+
* loops compose. `resolveModuleStringConst` consults this so a loop
|
|
205
|
+
* variable whose name happens to match a module string const is NOT
|
|
206
|
+
* inlined as the const literal (mirrors the Go adapter's loop-param /
|
|
207
|
+
* loop-var shadowing guards). (#1749 review)
|
|
208
|
+
*/
|
|
209
|
+
private loopBoundNames: Map<string, number> = new Map()
|
|
210
|
+
/**
|
|
211
|
+
* Prop names whose value is `undef` in the template body when the caller
|
|
212
|
+
* omits them — so a bare-reference attribute should be dropped rather
|
|
213
|
+
* than rendered as `attr=""`. The actual population criterion (see
|
|
214
|
+
* `generate()`) is: NO destructure default (`defaultValue === undefined`)
|
|
215
|
+
* AND non-rest (`!isRest`) AND non-primitive type (`type.kind !==
|
|
216
|
+
* 'primitive'`). It deliberately does NOT consult `p.optional`: the
|
|
217
|
+
* analyzer derives `optional` from the presence of a default initializer,
|
|
218
|
+
* not the `?` token, so it's not the right witness here. Excluding
|
|
219
|
+
* concrete primitives (`string`/`number`/`boolean`) mirrors the Go
|
|
220
|
+
* adapter's scope, which guards only `interface{}` (nillable) fields.
|
|
221
|
+
* Used by `elementAttrEmitter.emitExpression` to guard such an attribute
|
|
222
|
+
* with a Perl `defined $x` check (`<textarea>` omits `rows`), matching
|
|
223
|
+
* Hono's nullish-attribute omission. Concrete/defaulted props are
|
|
224
|
+
* excluded and always emit unconditionally.
|
|
225
|
+
*/
|
|
226
|
+
private nullableOptionalProps: Set<string> = new Set()
|
|
162
227
|
|
|
163
228
|
constructor(options: MojoAdapterOptions = {}) {
|
|
164
229
|
super()
|
|
@@ -172,6 +237,31 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
172
237
|
this.componentName = ir.metadata.componentName
|
|
173
238
|
this.propsObjectName = ir.metadata.propsObjectName ?? null
|
|
174
239
|
this.propsParams = ir.metadata.propsParams.map(p => ({ name: p.name }))
|
|
240
|
+
// No-destructure-default props → `undef` when the caller omits them
|
|
241
|
+
// → guard their bare-reference attribute emission with Perl `defined`
|
|
242
|
+
// so the attribute drops instead of rendering `attr=""` (Hono-style
|
|
243
|
+
// nullish omission). A prop WITH a destructure default (`value = ''`)
|
|
244
|
+
// is never `undef` in the body and must stay unconditional, so it is
|
|
245
|
+
// excluded. This mirrors the Go adapter's nillable-field guard: there
|
|
246
|
+
// the witness is the resolved `interface{}` field type; here it is
|
|
247
|
+
// the absence of a default (the analyzer reports `rows` — a
|
|
248
|
+
// `TextareaHTMLAttributes` member destructured without a default — as
|
|
249
|
+
// no-default, `type.kind: 'unknown'`).
|
|
250
|
+
// Excludes concrete-primitive types (`string`/`number`/`boolean`)
|
|
251
|
+
// to match the Go adapter's scope, which guards only `interface{}`
|
|
252
|
+
// (nillable) fields and leaves concrete fields unconditional. So a
|
|
253
|
+
// required, no-default `string` prop still emits `attr=""` like Hono,
|
|
254
|
+
// and only nillable (`unknown`/object/array) no-default props guard.
|
|
255
|
+
this.nullableOptionalProps = new Set(
|
|
256
|
+
ir.metadata.propsParams
|
|
257
|
+
.filter(
|
|
258
|
+
p =>
|
|
259
|
+
p.defaultValue === undefined &&
|
|
260
|
+
!p.isRest &&
|
|
261
|
+
p.type?.kind !== 'primitive',
|
|
262
|
+
)
|
|
263
|
+
.map(p => p.name),
|
|
264
|
+
)
|
|
175
265
|
// Record string-typed signals and props so equality comparisons against
|
|
176
266
|
// them lower to `eq`/`ne` (#1672). A signal is string-typed when its
|
|
177
267
|
// inferred type is `string` (the analyzer infers this from a string-literal
|
|
@@ -186,6 +276,8 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
186
276
|
for (const p of ir.metadata.propsParams) {
|
|
187
277
|
if (isStringTypeInfo(p.type)) this.stringValueNames.add(p.name)
|
|
188
278
|
}
|
|
279
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
280
|
+
this.loopBoundNames.clear()
|
|
189
281
|
this.errors = []
|
|
190
282
|
this.childrenCaptureCounter = 0
|
|
191
283
|
|
|
@@ -238,6 +330,41 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
238
330
|
}
|
|
239
331
|
}
|
|
240
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Build the module pure-string-const map from the IR's localConstants.
|
|
335
|
+
* A const qualifies only when it is module-scope (`isModule`) and its
|
|
336
|
+
* initializer parses to a single string literal (`ts.StringLiteral` or
|
|
337
|
+
* `ts.NoSubstitutionTemplateLiteral`). Template literals with `${}`,
|
|
338
|
+
* numeric/object initializers, `Record<T,string>` maps, memos, and
|
|
339
|
+
* signals are all excluded — only a pure compile-time string can be
|
|
340
|
+
* inlined byte-for-byte.
|
|
341
|
+
*/
|
|
342
|
+
private collectModuleStringConsts(constants: IRMetadata['localConstants']): Map<string, string> {
|
|
343
|
+
const map = new Map<string, string>()
|
|
344
|
+
for (const c of constants ?? []) {
|
|
345
|
+
if (!c.isModule) continue
|
|
346
|
+
if (c.value === undefined) continue
|
|
347
|
+
const literal = parsePureStringLiteral(c.value)
|
|
348
|
+
if (literal !== null) map.set(c.name, literal)
|
|
349
|
+
}
|
|
350
|
+
return map
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Resolve an identifier to its inlined Perl single-quoted string literal
|
|
355
|
+
* when it names a module pure-string const, else `null` (the caller then
|
|
356
|
+
* falls back to its normal `$name` stash lowering). Returns the Perl
|
|
357
|
+
* literal form `'<escaped>'` ready to drop into an expression.
|
|
358
|
+
*/
|
|
359
|
+
resolveModuleStringConst(name: string): string | null {
|
|
360
|
+
// A loop body introduces `my $<param>` / `my $<index>` bindings that
|
|
361
|
+
// shadow a module const of the same name — never inline inside one.
|
|
362
|
+
if (this.loopBoundNames.has(name)) return null
|
|
363
|
+
const value = this.moduleStringConsts.get(name)
|
|
364
|
+
if (value === undefined) return null
|
|
365
|
+
return `'${value.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
366
|
+
}
|
|
367
|
+
|
|
241
368
|
// ===========================================================================
|
|
242
369
|
// Script Registration
|
|
243
370
|
// ===========================================================================
|
|
@@ -587,6 +714,16 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
587
714
|
const indexVar = loop.iterationShape === 'keys'
|
|
588
715
|
? `$${param}`
|
|
589
716
|
: loop.index ? `$${loop.index}` : '$_i'
|
|
717
|
+
// Names this loop binds in body scope. Guard module-const inlining for
|
|
718
|
+
// the whole body (children + key + filter) so a same-named loop variable
|
|
719
|
+
// isn't replaced by the const literal (#1749 review). Ref-counted for
|
|
720
|
+
// nested loops; released after the body lines are assembled below.
|
|
721
|
+
const loopBound = loop.iterationShape === 'keys'
|
|
722
|
+
? [param]
|
|
723
|
+
: [param, loop.index ?? '_i']
|
|
724
|
+
for (const n of loopBound) {
|
|
725
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
726
|
+
}
|
|
590
727
|
const prevInLoop = this.inLoop
|
|
591
728
|
this.inLoop = true
|
|
592
729
|
const renderedChildren = this.renderChildren(loop.children)
|
|
@@ -644,6 +781,13 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
644
781
|
lines.push(children)
|
|
645
782
|
}
|
|
646
783
|
|
|
784
|
+
// Body fully rendered — release the loop-bound names.
|
|
785
|
+
for (const n of loopBound) {
|
|
786
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
787
|
+
if (c <= 0) this.loopBoundNames.delete(n)
|
|
788
|
+
else this.loopBoundNames.set(n, c)
|
|
789
|
+
}
|
|
790
|
+
|
|
647
791
|
lines.push(`% }`)
|
|
648
792
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`)
|
|
649
793
|
|
|
@@ -833,6 +977,32 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
833
977
|
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
834
978
|
return ''
|
|
835
979
|
}
|
|
980
|
+
// Hono-style nullish-attribute omission (#textarea rows): when the
|
|
981
|
+
// attribute value is a BARE reference to an optional, no-default
|
|
982
|
+
// prop (which is `undef` when the caller omits it), guard the
|
|
983
|
+
// attribute with Perl `defined` so it DROPS rather than rendering
|
|
984
|
+
// `attr=""`. The guarded body reuses the exact normal emission, so
|
|
985
|
+
// value escaping (`<%= ... %>`) is unchanged; only the presence is
|
|
986
|
+
// conditional. The `% if`/`% end` line directives surround the
|
|
987
|
+
// attribute inline — the conformance comparator collapses the
|
|
988
|
+
// resulting whitespace, exactly like the existing boolean-attr and
|
|
989
|
+
// hydration-marker patterns. Scope is deliberately narrow (bare
|
|
990
|
+
// identifiers resolving to an optional-no-default prop) so member
|
|
991
|
+
// exprs, calls, concrete/defaulted props, and boolean attrs are
|
|
992
|
+
// unaffected and still emit unconditionally.
|
|
993
|
+
const bareId = value.expr.trim()
|
|
994
|
+
if (
|
|
995
|
+
!isBooleanAttr(name) &&
|
|
996
|
+
!value.presenceOrUndefined &&
|
|
997
|
+
this.nullableOptionalProps.has(bareId)
|
|
998
|
+
) {
|
|
999
|
+
const perl = this.convertExpressionToPerl(value.expr)
|
|
1000
|
+
const body =
|
|
1001
|
+
isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name)
|
|
1002
|
+
? `${name}="<%= bf->bool_str(${perl}) %>"`
|
|
1003
|
+
: `${name}="<%= ${perl} %>"`
|
|
1004
|
+
return `<% if (defined ${perl}) { %>${body}<% } %>`
|
|
1005
|
+
}
|
|
836
1006
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
837
1007
|
// Boolean attributes: render conditionally (present or absent).
|
|
838
1008
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`
|
|
@@ -906,6 +1076,17 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
906
1076
|
)
|
|
907
1077
|
return `<%== bf->spread_attrs({${entries.join(', ')}}) %>`
|
|
908
1078
|
}
|
|
1079
|
+
// Conditional inline-object spread:
|
|
1080
|
+
// `{...(COND ? { 'aria-describedby': describedBy } : {})}`
|
|
1081
|
+
// Emit a Perl inline ternary of hashrefs — Perl truthiness
|
|
1082
|
+
// handles the condition for free, and the falsy `{}` branch
|
|
1083
|
+
// OMITS the key (`bf->spread_attrs` does NOT filter empty
|
|
1084
|
+
// strings, so we cannot always-include it). Mirrors the Go
|
|
1085
|
+
// adapter's IIFE-of-maps lowering (#textarea).
|
|
1086
|
+
const ternaryHashref = this.conditionalSpreadToPerl(trimmed)
|
|
1087
|
+
if (ternaryHashref !== null) {
|
|
1088
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`
|
|
1089
|
+
}
|
|
909
1090
|
const perlExpr = this.convertExpressionToPerl(value.expr)
|
|
910
1091
|
return `<%== bf->spread_attrs(${perlExpr}) %>`
|
|
911
1092
|
},
|
|
@@ -1192,6 +1373,77 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1192
1373
|
}
|
|
1193
1374
|
|
|
1194
1375
|
|
|
1376
|
+
/**
|
|
1377
|
+
* Lower a conditional inline-object spread expression
|
|
1378
|
+
* `(COND ? { 'aria-describedby': describedBy } : {})`
|
|
1379
|
+
* (either branch possibly `{}`) into a Perl inline ternary of
|
|
1380
|
+
* hashrefs for `bf->spread_attrs`:
|
|
1381
|
+
* `$describedBy ? { 'aria-describedby' => $describedBy } : {}`
|
|
1382
|
+
*
|
|
1383
|
+
* The condition is translated via `convertExpressionToPerl` (a bare
|
|
1384
|
+
* prop ident becomes `$describedBy`; Perl truthiness handles the
|
|
1385
|
+
* test). Object literals become Perl hashrefs with `=>`; string-
|
|
1386
|
+
* literal keys are quoted, values resolve via `convertExpressionToPerl`.
|
|
1387
|
+
*
|
|
1388
|
+
* Returns null when the expression is NOT this shape, or when a part
|
|
1389
|
+
* can't be faithfully lowered (non-static key, etc.) so the caller
|
|
1390
|
+
* falls back to the standard `convertExpressionToPerl` path (which
|
|
1391
|
+
* records BF101). Scoped strictly to ternary-of-object-literals so no
|
|
1392
|
+
* other spread shape regresses.
|
|
1393
|
+
*/
|
|
1394
|
+
private conditionalSpreadToPerl(expr: string): string | null {
|
|
1395
|
+
const sf = ts.createSourceFile('__spread.ts', `(${expr})`, ts.ScriptTarget.Latest, true)
|
|
1396
|
+
if (sf.statements.length !== 1) return null
|
|
1397
|
+
const stmt = sf.statements[0]
|
|
1398
|
+
if (!ts.isExpressionStatement(stmt)) return null
|
|
1399
|
+
let node: ts.Expression = stmt.expression
|
|
1400
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
1401
|
+
if (!ts.isConditionalExpression(node)) return null
|
|
1402
|
+
const unwrap = (e: ts.Expression): ts.Expression => {
|
|
1403
|
+
let n = e
|
|
1404
|
+
while (ts.isParenthesizedExpression(n)) n = n.expression
|
|
1405
|
+
return n
|
|
1406
|
+
}
|
|
1407
|
+
const whenTrue = unwrap(node.whenTrue)
|
|
1408
|
+
const whenFalse = unwrap(node.whenFalse)
|
|
1409
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
1410
|
+
return null
|
|
1411
|
+
}
|
|
1412
|
+
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf))
|
|
1413
|
+
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf)
|
|
1414
|
+
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf)
|
|
1415
|
+
if (truePerl === null || falsePerl === null) return null
|
|
1416
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Convert a static object literal into a Perl hashref string for a
|
|
1421
|
+
* conditional spread. Only static string/identifier keys are allowed;
|
|
1422
|
+
* values resolve via `convertExpressionToPerl`. Returns null for any
|
|
1423
|
+
* computed/spread/dynamic key. Empty object → `{}`.
|
|
1424
|
+
*/
|
|
1425
|
+
private objectLiteralToPerlHashref(
|
|
1426
|
+
obj: ts.ObjectLiteralExpression,
|
|
1427
|
+
sf: ts.SourceFile,
|
|
1428
|
+
): string | null {
|
|
1429
|
+
const entries: string[] = []
|
|
1430
|
+
for (const prop of obj.properties) {
|
|
1431
|
+
if (!ts.isPropertyAssignment(prop)) return null
|
|
1432
|
+
let key: string
|
|
1433
|
+
if (ts.isIdentifier(prop.name)) {
|
|
1434
|
+
key = prop.name.text
|
|
1435
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
1436
|
+
key = prop.name.text
|
|
1437
|
+
} else {
|
|
1438
|
+
return null
|
|
1439
|
+
}
|
|
1440
|
+
const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf))
|
|
1441
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`)
|
|
1442
|
+
}
|
|
1443
|
+
return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
|
|
1195
1447
|
private convertExpressionToPerl(expr: string): string {
|
|
1196
1448
|
// Parse-first lowering — parity with the Go adapter's
|
|
1197
1449
|
// `convertExpressionToGo`. Parse the JS expression once, gate it on
|
|
@@ -1307,7 +1559,7 @@ function renderArrayMethod(
|
|
|
1307
1559
|
// runtime's `bf->includes($recv, $elem)` inspects `ref($recv)`
|
|
1308
1560
|
// and dispatches: ARRAY ref scans the list with `eq`, scalar
|
|
1309
1561
|
// falls back to `index(..., ...) != -1`. Helper lives in
|
|
1310
|
-
// packages/adapter-
|
|
1562
|
+
// packages/adapter-perl/lib/BarefootJS.pm.
|
|
1311
1563
|
//
|
|
1312
1564
|
// The `bf->` (no `$`) form matches every other helper emit —
|
|
1313
1565
|
// in real Mojolicious `bf` is a controller helper; the
|
|
@@ -1817,6 +2069,11 @@ class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
1817
2069
|
constructor(private readonly adapter: MojoAdapter) {}
|
|
1818
2070
|
|
|
1819
2071
|
identifier(name: string): string {
|
|
2072
|
+
// Module pure-string const (e.g. `const baseClasses = '...'` used in a
|
|
2073
|
+
// className template literal): inline the literal value rather than emit
|
|
2074
|
+
// `$baseClasses` against a stash variable that is never bound.
|
|
2075
|
+
const inlined = this.adapter.resolveModuleStringConst(name)
|
|
2076
|
+
if (inlined !== null) return inlined
|
|
1820
2077
|
return `$${name}`
|
|
1821
2078
|
}
|
|
1822
2079
|
|
package/src/test-render.ts
CHANGED
|
@@ -11,7 +11,12 @@ import { mkdir, rm } from 'node:fs/promises'
|
|
|
11
11
|
import { resolve } from 'node:path'
|
|
12
12
|
|
|
13
13
|
const RENDER_TEMP_DIR = resolve(import.meta.dir, '../.render-temp')
|
|
14
|
+
// Mojo-specific lib (BarefootJS::Backend::Mojo + the plugin) lives in this
|
|
15
|
+
// package; the engine-agnostic core (BarefootJS.pm) moved to @barefootjs/perl.
|
|
16
|
+
// Both dirs must be on the render script's @INC so `use BarefootJS` and
|
|
17
|
+
// `use BarefootJS::Backend::Mojo` resolve.
|
|
14
18
|
const LIB_DIR = resolve(import.meta.dir, '../lib')
|
|
19
|
+
const PERL_CORE_LIB_DIR = resolve(import.meta.dir, '../../adapter-perl/lib')
|
|
15
20
|
|
|
16
21
|
export class PerlNotAvailableError extends Error {
|
|
17
22
|
constructor(message: string) {
|
|
@@ -216,7 +221,7 @@ use strict;
|
|
|
216
221
|
use warnings;
|
|
217
222
|
use utf8;
|
|
218
223
|
|
|
219
|
-
use lib '${LIB_DIR}';
|
|
224
|
+
use lib '${LIB_DIR}', '${PERL_CORE_LIB_DIR}';
|
|
220
225
|
use Mojolicious;
|
|
221
226
|
use Mojo::Template;
|
|
222
227
|
# Boolean values in spread bags arrive as Mojo::JSON::true /
|
|
@@ -428,6 +433,25 @@ function buildPerlProps(
|
|
|
428
433
|
entries.push(`${param.name} => undef`)
|
|
429
434
|
}
|
|
430
435
|
|
|
436
|
+
// A `{...props}` rest spread means props that aren't declared named
|
|
437
|
+
// params flow through the rest bag (`bf->spread_attrs($<restPropsName>)`),
|
|
438
|
+
// not their own top-level template var. Route them into the bag hashref so
|
|
439
|
+
// a fixture passing e.g. `placeholder` to `Input` (whose declared params
|
|
440
|
+
// are `className` / `type`) renders `placeholder="..."` via the spread
|
|
441
|
+
// rather than silently dropping it into an unused `my $placeholder`. (#1467
|
|
442
|
+
// Phase 2b — mirrors the Go harness fix in the sibling `test-render.ts`.)
|
|
443
|
+
const restPropsName = ir.metadata.restPropsName
|
|
444
|
+
const declaredParams = new Set(ir.metadata.propsParams.map(p => p.name))
|
|
445
|
+
const restBagEntries: Array<[string, unknown]> = []
|
|
446
|
+
if (restPropsName && props) {
|
|
447
|
+
for (const [key, value] of Object.entries(props)) {
|
|
448
|
+
if (key.startsWith('__')) continue
|
|
449
|
+
if (key === restPropsName || declaredParams.has(key)) continue
|
|
450
|
+
restBagEntries.push([key, value])
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const routedKeys = new Set(restBagEntries.map(([k]) => k))
|
|
454
|
+
|
|
431
455
|
// (#1407 follow-up) Default the rest-binding identifier to an
|
|
432
456
|
// empty hashref so `bf->spread_attrs($extras)` in the generated
|
|
433
457
|
// Mojo template doesn't trip Perl's strict-mode "Global symbol
|
|
@@ -436,13 +460,16 @@ function buildPerlProps(
|
|
|
436
460
|
// the COMPILE path on Go, where the bag plumbing matters; the
|
|
437
461
|
// runtime is a no-op when the caller leaves the bag unset, which
|
|
438
462
|
// mirrors the empty-spread case on every adapter).
|
|
439
|
-
|
|
440
|
-
|
|
463
|
+
// When the fixture supplied undeclared props, seed the bag with those
|
|
464
|
+
// routed entries instead of an empty hashref.
|
|
465
|
+
if (restPropsName && !(props && restPropsName in props)) {
|
|
466
|
+
entries.push(`${restPropsName} => ${toPerlLiteral(Object.fromEntries(restBagEntries))}`)
|
|
441
467
|
}
|
|
442
468
|
|
|
443
469
|
// Add user props
|
|
444
470
|
if (props) {
|
|
445
471
|
for (const [key, value] of Object.entries(props)) {
|
|
472
|
+
if (routedKeys.has(key)) continue
|
|
446
473
|
if (typeof value === 'string') {
|
|
447
474
|
entries.push(`${key} => '${value.replace(/'/g, "\\'")}'`)
|
|
448
475
|
} else if (typeof value === 'number') {
|