@barefootjs/mojolicious 0.7.0 → 0.9.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/LICENSE +21 -0
- package/dist/adapter/index.js +107 -8
- package/dist/adapter/mojo-adapter.d.ts +66 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +107 -8
- package/dist/index.js +107 -8
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +11 -52
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +4 -4
- package/src/__tests__/mojo-adapter.test.ts +198 -59
- package/src/adapter/mojo-adapter.ts +256 -7
- package/src/test-render.ts +58 -1
|
@@ -47,6 +47,9 @@ import {
|
|
|
47
47
|
emitParsedExpr,
|
|
48
48
|
emitIRNode,
|
|
49
49
|
emitAttrValue,
|
|
50
|
+
augmentInheritedPropAccesses,
|
|
51
|
+
parseRecordIndexAccess,
|
|
52
|
+
evalStringArrayJoin,
|
|
50
53
|
} from '@barefootjs/jsx'
|
|
51
54
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result'
|
|
52
55
|
|
|
@@ -142,7 +145,20 @@ function parsePureStringLiteral(source: string): string | null {
|
|
|
142
145
|
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
143
146
|
return init.text
|
|
144
147
|
}
|
|
145
|
-
|
|
148
|
+
// `[<literals>].join(' ')` module consts (e.g. Switch's `trackStateClasses`)
|
|
149
|
+
// → inline the flattened string byte-for-byte. See `evalStringArrayJoin`.
|
|
150
|
+
return evalStringArrayJoin(source)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* (#checkbox) Quote a `render_child` named-arg / hashref key when it isn't a
|
|
155
|
+
* bare Perl identifier. A JSX attribute name like `data-slot` would otherwise
|
|
156
|
+
* emit `data-slot => '...'`, which Perl parses as the subtraction
|
|
157
|
+
* `data - slot`. Identifier-safe names (`className`, `size`, `_bf_slot`) pass
|
|
158
|
+
* through unquoted to keep the generated template readable.
|
|
159
|
+
*/
|
|
160
|
+
function perlHashKey(name: string): string {
|
|
161
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRenderCtx> {
|
|
@@ -198,6 +214,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
198
214
|
* module-scope pure string literals qualify (see `collectModuleStringConsts`).
|
|
199
215
|
*/
|
|
200
216
|
private moduleStringConsts: Map<string, string> = new Map()
|
|
217
|
+
/**
|
|
218
|
+
* Full local-constant metadata from the entry IR, kept so spread
|
|
219
|
+
* lowering can resolve a bare-identifier spread (`{...sizeAttrs}`) to
|
|
220
|
+
* its initializer text and a `Record[propKey]` spread value to the
|
|
221
|
+
* module-const object literal it indexes (#checkbox / icon). Populated
|
|
222
|
+
* at `generate()` entry alongside `moduleStringConsts`.
|
|
223
|
+
*/
|
|
224
|
+
private localConstants: IRMetadata['localConstants'] = []
|
|
201
225
|
/**
|
|
202
226
|
* Names currently bound by an enclosing loop body — the `my $<param>` and
|
|
203
227
|
* `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
|
|
@@ -207,6 +231,23 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
207
231
|
* loop-var shadowing guards). (#1749 review)
|
|
208
232
|
*/
|
|
209
233
|
private loopBoundNames: Map<string, number> = new Map()
|
|
234
|
+
/**
|
|
235
|
+
* Prop names whose value is `undef` in the template body when the caller
|
|
236
|
+
* omits them — so a bare-reference attribute should be dropped rather
|
|
237
|
+
* than rendered as `attr=""`. The actual population criterion (see
|
|
238
|
+
* `generate()`) is: NO destructure default (`defaultValue === undefined`)
|
|
239
|
+
* AND non-rest (`!isRest`) AND non-primitive type (`type.kind !==
|
|
240
|
+
* 'primitive'`). It deliberately does NOT consult `p.optional`: the
|
|
241
|
+
* analyzer derives `optional` from the presence of a default initializer,
|
|
242
|
+
* not the `?` token, so it's not the right witness here. Excluding
|
|
243
|
+
* concrete primitives (`string`/`number`/`boolean`) mirrors the Go
|
|
244
|
+
* adapter's scope, which guards only `interface{}` (nillable) fields.
|
|
245
|
+
* Used by `elementAttrEmitter.emitExpression` to guard such an attribute
|
|
246
|
+
* with a Perl `defined $x` check (`<textarea>` omits `rows`), matching
|
|
247
|
+
* Hono's nullish-attribute omission. Concrete/defaulted props are
|
|
248
|
+
* excluded and always emit unconditionally.
|
|
249
|
+
*/
|
|
250
|
+
private nullableOptionalProps: Set<string> = new Set()
|
|
210
251
|
|
|
211
252
|
constructor(options: MojoAdapterOptions = {}) {
|
|
212
253
|
super()
|
|
@@ -219,7 +260,40 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
219
260
|
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput {
|
|
220
261
|
this.componentName = ir.metadata.componentName
|
|
221
262
|
this.propsObjectName = ir.metadata.propsObjectName ?? null
|
|
263
|
+
// (#checkbox) Enumerate inherited-attribute accesses for the props-object
|
|
264
|
+
// pattern (`function Checkbox(props: CheckboxProps)`) before deriving
|
|
265
|
+
// `nullableOptionalProps`, so a bare optional attribute like
|
|
266
|
+
// `id={props.id}` gets the Perl `defined`-guard (Hono-style omission).
|
|
267
|
+
// Shared with the Go adapter (single source of truth in `@barefootjs/jsx`).
|
|
268
|
+
// The harness separately declares the matching stash vars (Pass-1 IR
|
|
269
|
+
// serialization happens before `generate`, so this mutation doesn't reach it).
|
|
270
|
+
augmentInheritedPropAccesses(ir)
|
|
222
271
|
this.propsParams = ir.metadata.propsParams.map(p => ({ name: p.name }))
|
|
272
|
+
// No-destructure-default props → `undef` when the caller omits them
|
|
273
|
+
// → guard their bare-reference attribute emission with Perl `defined`
|
|
274
|
+
// so the attribute drops instead of rendering `attr=""` (Hono-style
|
|
275
|
+
// nullish omission). A prop WITH a destructure default (`value = ''`)
|
|
276
|
+
// is never `undef` in the body and must stay unconditional, so it is
|
|
277
|
+
// excluded. This mirrors the Go adapter's nillable-field guard: there
|
|
278
|
+
// the witness is the resolved `interface{}` field type; here it is
|
|
279
|
+
// the absence of a default (the analyzer reports `rows` — a
|
|
280
|
+
// `TextareaHTMLAttributes` member destructured without a default — as
|
|
281
|
+
// no-default, `type.kind: 'unknown'`).
|
|
282
|
+
// Excludes concrete-primitive types (`string`/`number`/`boolean`)
|
|
283
|
+
// to match the Go adapter's scope, which guards only `interface{}`
|
|
284
|
+
// (nillable) fields and leaves concrete fields unconditional. So a
|
|
285
|
+
// required, no-default `string` prop still emits `attr=""` like Hono,
|
|
286
|
+
// and only nillable (`unknown`/object/array) no-default props guard.
|
|
287
|
+
this.nullableOptionalProps = new Set(
|
|
288
|
+
ir.metadata.propsParams
|
|
289
|
+
.filter(
|
|
290
|
+
p =>
|
|
291
|
+
p.defaultValue === undefined &&
|
|
292
|
+
!p.isRest &&
|
|
293
|
+
p.type?.kind !== 'primitive',
|
|
294
|
+
)
|
|
295
|
+
.map(p => p.name),
|
|
296
|
+
)
|
|
223
297
|
// Record string-typed signals and props so equality comparisons against
|
|
224
298
|
// them lower to `eq`/`ne` (#1672). A signal is string-typed when its
|
|
225
299
|
// inferred type is `string` (the analyzer infers this from a string-literal
|
|
@@ -235,6 +309,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
235
309
|
if (isStringTypeInfo(p.type)) this.stringValueNames.add(p.name)
|
|
236
310
|
}
|
|
237
311
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
312
|
+
this.localConstants = ir.metadata.localConstants ?? []
|
|
238
313
|
this.loopBoundNames.clear()
|
|
239
314
|
this.errors = []
|
|
240
315
|
this.childrenCaptureCounter = 0
|
|
@@ -766,7 +841,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
766
841
|
* `render_child` named-arg list.
|
|
767
842
|
*/
|
|
768
843
|
private readonly componentPropEmitter: AttrValueEmitter = {
|
|
769
|
-
emitLiteral: (value, name) => `${name} => '${value.value}'`,
|
|
844
|
+
emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
|
|
770
845
|
emitExpression: (value, name) => {
|
|
771
846
|
// The IR producer collapses component-prop `template` kinds
|
|
772
847
|
// into `expression` for client-runtime reasons but preserves
|
|
@@ -775,9 +850,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
775
850
|
// `${MAP[KEY]}` shapes (the JS object literal leaks into the
|
|
776
851
|
// Perl template).
|
|
777
852
|
if (value.parts) {
|
|
778
|
-
return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`
|
|
853
|
+
return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`
|
|
779
854
|
}
|
|
780
|
-
return `${name} => ${this.convertExpressionToPerl(value.expr)}`
|
|
855
|
+
return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`
|
|
781
856
|
},
|
|
782
857
|
emitSpread: (value) => {
|
|
783
858
|
// Perl has no JS-style spread — emit the source as a hash
|
|
@@ -790,9 +865,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
790
865
|
return perlExpr.startsWith('%') ? perlExpr : `%{${perlExpr}}`
|
|
791
866
|
},
|
|
792
867
|
emitTemplate: (value, name) =>
|
|
793
|
-
`${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
|
|
794
|
-
emitBooleanAttr: (_value, name) => `${name} => 1`,
|
|
795
|
-
emitBooleanShorthand: (_value, name) => `${name} => 1`,
|
|
868
|
+
`${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
|
|
869
|
+
emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
|
|
870
|
+
emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
|
|
796
871
|
// JSX children flow through Mojo's `begin %>…<% end` capture
|
|
797
872
|
// below; they're not part of the named-arg list.
|
|
798
873
|
emitJsxChildren: () => '',
|
|
@@ -935,6 +1010,41 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
935
1010
|
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
936
1011
|
return ''
|
|
937
1012
|
}
|
|
1013
|
+
// Hono-style nullish-attribute omission (#textarea rows): when the
|
|
1014
|
+
// attribute value is a BARE reference to an optional, no-default
|
|
1015
|
+
// prop (which is `undef` when the caller omits it), guard the
|
|
1016
|
+
// attribute with Perl `defined` so it DROPS rather than rendering
|
|
1017
|
+
// `attr=""`. The guarded body reuses the exact normal emission, so
|
|
1018
|
+
// value escaping (`<%= ... %>`) is unchanged; only the presence is
|
|
1019
|
+
// conditional. The `% if`/`% end` line directives surround the
|
|
1020
|
+
// attribute inline — the conformance comparator collapses the
|
|
1021
|
+
// resulting whitespace, exactly like the existing boolean-attr and
|
|
1022
|
+
// hydration-marker patterns. Scope is deliberately narrow (bare
|
|
1023
|
+
// identifiers resolving to an optional-no-default prop) so member
|
|
1024
|
+
// exprs, calls, concrete/defaulted props, and boolean attrs are
|
|
1025
|
+
// unaffected and still emit unconditionally.
|
|
1026
|
+
const bareId = value.expr.trim()
|
|
1027
|
+
// Normalize a props-object access (`props.id`) to its bare prop name
|
|
1028
|
+
// (`id`) so the nullable-optional set — keyed by bare name — matches the
|
|
1029
|
+
// SolidJS props-object pattern, not just destructured params (#checkbox
|
|
1030
|
+
// `id={props.id}`).
|
|
1031
|
+
const normalizedBareId =
|
|
1032
|
+
this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`)
|
|
1033
|
+
? bareId.slice(this.propsObjectName.length + 1)
|
|
1034
|
+
: bareId
|
|
1035
|
+
if (
|
|
1036
|
+
!isBooleanAttr(name) &&
|
|
1037
|
+
!value.presenceOrUndefined &&
|
|
1038
|
+
/^[A-Za-z_$][\w$]*$/.test(normalizedBareId) &&
|
|
1039
|
+
this.nullableOptionalProps.has(normalizedBareId)
|
|
1040
|
+
) {
|
|
1041
|
+
const perl = this.convertExpressionToPerl(value.expr)
|
|
1042
|
+
const body =
|
|
1043
|
+
isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name)
|
|
1044
|
+
? `${name}="<%= bf->bool_str(${perl}) %>"`
|
|
1045
|
+
: `${name}="<%= ${perl} %>"`
|
|
1046
|
+
return `<% if (defined ${perl}) { %>${body}<% } %>`
|
|
1047
|
+
}
|
|
938
1048
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
939
1049
|
// Boolean attributes: render conditionally (present or absent).
|
|
940
1050
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`
|
|
@@ -1008,6 +1118,37 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1008
1118
|
)
|
|
1009
1119
|
return `<%== bf->spread_attrs({${entries.join(', ')}}) %>`
|
|
1010
1120
|
}
|
|
1121
|
+
// Conditional inline-object spread:
|
|
1122
|
+
// `{...(COND ? { 'aria-describedby': describedBy } : {})}`
|
|
1123
|
+
// Emit a Perl inline ternary of hashrefs — Perl truthiness
|
|
1124
|
+
// handles the condition for free, and the falsy `{}` branch
|
|
1125
|
+
// OMITS the key (`bf->spread_attrs` does NOT filter empty
|
|
1126
|
+
// strings, so we cannot always-include it). Mirrors the Go
|
|
1127
|
+
// adapter's IIFE-of-maps lowering (#textarea).
|
|
1128
|
+
const ternaryHashref = this.conditionalSpreadToPerl(trimmed)
|
|
1129
|
+
if (ternaryHashref !== null) {
|
|
1130
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`
|
|
1131
|
+
}
|
|
1132
|
+
// Function-scope local const holding a conditional inline-object
|
|
1133
|
+
// `const sizeAttrs = size ? {…} : {}` then `{...sizeAttrs}`
|
|
1134
|
+
// (#checkbox / icon). Resolve the bare identifier to its
|
|
1135
|
+
// initializer text and route through the same conditional-spread
|
|
1136
|
+
// lowering. Only function-scope (`!isModule`) consts whose value is
|
|
1137
|
+
// NOT itself a bare identifier (loop guard) are considered.
|
|
1138
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
1139
|
+
const localConst = this.localConstants.find(
|
|
1140
|
+
c => c.name === trimmed && !c.isModule,
|
|
1141
|
+
)
|
|
1142
|
+
if (localConst?.value !== undefined) {
|
|
1143
|
+
const initTrimmed = localConst.value.trim()
|
|
1144
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
1145
|
+
const resolved = this.conditionalSpreadToPerl(initTrimmed)
|
|
1146
|
+
if (resolved !== null) {
|
|
1147
|
+
return `<%== bf->spread_attrs(${resolved}) %>`
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1011
1152
|
const perlExpr = this.convertExpressionToPerl(value.expr)
|
|
1012
1153
|
return `<%== bf->spread_attrs(${perlExpr}) %>`
|
|
1013
1154
|
},
|
|
@@ -1294,6 +1435,114 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1294
1435
|
}
|
|
1295
1436
|
|
|
1296
1437
|
|
|
1438
|
+
/**
|
|
1439
|
+
* Lower a conditional inline-object spread expression
|
|
1440
|
+
* `(COND ? { 'aria-describedby': describedBy } : {})`
|
|
1441
|
+
* (either branch possibly `{}`) into a Perl inline ternary of
|
|
1442
|
+
* hashrefs for `bf->spread_attrs`:
|
|
1443
|
+
* `$describedBy ? { 'aria-describedby' => $describedBy } : {}`
|
|
1444
|
+
*
|
|
1445
|
+
* The condition is translated via `convertExpressionToPerl` (a bare
|
|
1446
|
+
* prop ident becomes `$describedBy`; Perl truthiness handles the
|
|
1447
|
+
* test). Object literals become Perl hashrefs with `=>`; string-
|
|
1448
|
+
* literal keys are quoted, values resolve via `convertExpressionToPerl`.
|
|
1449
|
+
*
|
|
1450
|
+
* Returns null when the expression is NOT this shape, or when a part
|
|
1451
|
+
* can't be faithfully lowered (non-static key, etc.) so the caller
|
|
1452
|
+
* falls back to the standard `convertExpressionToPerl` path (which
|
|
1453
|
+
* records BF101). Scoped strictly to ternary-of-object-literals so no
|
|
1454
|
+
* other spread shape regresses.
|
|
1455
|
+
*/
|
|
1456
|
+
private conditionalSpreadToPerl(expr: string): string | null {
|
|
1457
|
+
const sf = ts.createSourceFile('__spread.ts', `(${expr})`, ts.ScriptTarget.Latest, true)
|
|
1458
|
+
if (sf.statements.length !== 1) return null
|
|
1459
|
+
const stmt = sf.statements[0]
|
|
1460
|
+
if (!ts.isExpressionStatement(stmt)) return null
|
|
1461
|
+
let node: ts.Expression = stmt.expression
|
|
1462
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
1463
|
+
if (!ts.isConditionalExpression(node)) return null
|
|
1464
|
+
const unwrap = (e: ts.Expression): ts.Expression => {
|
|
1465
|
+
let n = e
|
|
1466
|
+
while (ts.isParenthesizedExpression(n)) n = n.expression
|
|
1467
|
+
return n
|
|
1468
|
+
}
|
|
1469
|
+
const whenTrue = unwrap(node.whenTrue)
|
|
1470
|
+
const whenFalse = unwrap(node.whenFalse)
|
|
1471
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
1472
|
+
return null
|
|
1473
|
+
}
|
|
1474
|
+
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf))
|
|
1475
|
+
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf)
|
|
1476
|
+
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf)
|
|
1477
|
+
if (truePerl === null || falsePerl === null) return null
|
|
1478
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Convert a static object literal into a Perl hashref string for a
|
|
1483
|
+
* conditional spread. Only static string/identifier keys are allowed;
|
|
1484
|
+
* values resolve via `convertExpressionToPerl`. Returns null for any
|
|
1485
|
+
* computed/spread/dynamic key. Empty object → `{}`.
|
|
1486
|
+
*/
|
|
1487
|
+
private objectLiteralToPerlHashref(
|
|
1488
|
+
obj: ts.ObjectLiteralExpression,
|
|
1489
|
+
sf: ts.SourceFile,
|
|
1490
|
+
): string | null {
|
|
1491
|
+
const entries: string[] = []
|
|
1492
|
+
for (const prop of obj.properties) {
|
|
1493
|
+
if (!ts.isPropertyAssignment(prop)) return null
|
|
1494
|
+
let key: string
|
|
1495
|
+
if (ts.isIdentifier(prop.name)) {
|
|
1496
|
+
key = prop.name.text
|
|
1497
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
1498
|
+
key = prop.name.text
|
|
1499
|
+
} else {
|
|
1500
|
+
return null
|
|
1501
|
+
}
|
|
1502
|
+
const initNode = (() => {
|
|
1503
|
+
let n: ts.Expression = prop.initializer
|
|
1504
|
+
while (ts.isParenthesizedExpression(n)) n = n.expression
|
|
1505
|
+
return n
|
|
1506
|
+
})()
|
|
1507
|
+
const indexed = this.recordIndexAccessToPerl(initNode)
|
|
1508
|
+
const valPerl =
|
|
1509
|
+
indexed !== null
|
|
1510
|
+
? indexed
|
|
1511
|
+
: this.convertExpressionToPerl(prop.initializer.getText(sf))
|
|
1512
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`)
|
|
1513
|
+
}
|
|
1514
|
+
return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Lower a spread-object VALUE of the form `IDENT[KEY]` where:
|
|
1519
|
+
* - `IDENT` resolves via `localConstants` to a MODULE-scope object
|
|
1520
|
+
* literal whose property values are all scalar (number/string)
|
|
1521
|
+
* literals under static (string-literal or identifier) keys
|
|
1522
|
+
* (a `Record<staticKeys, scalar>` map like `sizeMap`), AND
|
|
1523
|
+
* - `KEY` is a bare identifier that is a prop.
|
|
1524
|
+
* Emits an inline indexed Perl hashref:
|
|
1525
|
+
* `{ 'sm' => 16, 'md' => 20, ... }->{$size}`
|
|
1526
|
+
*
|
|
1527
|
+
* Returns the Perl string when convertible, else `null` so the caller
|
|
1528
|
+
* falls back to its normal value lowering (which records BF101 for an
|
|
1529
|
+
* unsupported shape). Mirror of the Go adapter's `recordIndexAccessToGoMap`.
|
|
1530
|
+
*/
|
|
1531
|
+
private recordIndexAccessToPerl(val: ts.Expression): string | null {
|
|
1532
|
+
// Shared structural parse (single source of truth in `@barefootjs/jsx`);
|
|
1533
|
+
// this wrapper only does the Perl-specific emit (single-quote escaping)
|
|
1534
|
+
// from the structured result.
|
|
1535
|
+
const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams)
|
|
1536
|
+
if (!parsed) return null
|
|
1537
|
+
const entries = parsed.entries.map(e => {
|
|
1538
|
+
const mapVal =
|
|
1539
|
+
e.value.kind === 'number' ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`
|
|
1540
|
+
return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`
|
|
1541
|
+
})
|
|
1542
|
+
return `{ ${entries.join(', ')} }->{$${parsed.indexPropName}}`
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
|
|
1297
1546
|
private convertExpressionToPerl(expr: string): string {
|
|
1298
1547
|
// Parse-first lowering — parity with the Go adapter's
|
|
1299
1548
|
// `convertExpressionToGo`. Parse the JS expression once, gate it on
|
package/src/test-render.ts
CHANGED
|
@@ -349,7 +349,12 @@ function buildChildRenderers(
|
|
|
349
349
|
}
|
|
350
350
|
lines.push(` ${slotIdsPerl}`)
|
|
351
351
|
lines.push(` my $child_bf = BarefootJS->new($c, {});`)
|
|
352
|
-
|
|
352
|
+
// Child scope id derives from the PARENT's live scope id, not a literal
|
|
353
|
+
// `test_` prefix — so `<ReactiveProps_test>_s5` matches Hono / CSR (and
|
|
354
|
+
// survives an explicit `__instanceId`). Mirrors xslate's
|
|
355
|
+
// `rootChildScopePrefix` (`$bf->_scope_id`); `$bf` is the parent instance
|
|
356
|
+
// captured by this renderer closure.
|
|
357
|
+
lines.push(` $child_bf->_scope_id($bf->_scope_id . "_$sid");`)
|
|
353
358
|
lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$child_props, bf => $child_bf });`)
|
|
354
359
|
lines.push(` die $rendered->to_string if ref $rendered;`)
|
|
355
360
|
lines.push(` chomp $rendered;`)
|
|
@@ -433,6 +438,26 @@ function buildPerlProps(
|
|
|
433
438
|
entries.push(`${param.name} => undef`)
|
|
434
439
|
}
|
|
435
440
|
|
|
441
|
+
// (#checkbox) SolidJS props-object pattern: `function Checkbox(props:
|
|
442
|
+
// CheckboxProps)` only enumerates `CheckboxProps`'s own members in
|
|
443
|
+
// `propsParams`; inherited `ButtonHTMLAttributes` members the component
|
|
444
|
+
// reads as bare template vars (`$id`, `$disabled`, `$className`) are not.
|
|
445
|
+
// The generated template references them, so the stash must declare them or
|
|
446
|
+
// Perl strict mode aborts with `Global symbol "$id" requires explicit
|
|
447
|
+
// package name`. Scan the IR for `props.<name>` accesses and seed any not
|
|
448
|
+
// already declared (and not supplied by the caller below) as `undef`.
|
|
449
|
+
// Mirrors the Go adapter's `augmentInheritedPropAccesses`.
|
|
450
|
+
if (ir.metadata.propsObjectName) {
|
|
451
|
+
const propsObj = ir.metadata.propsObjectName
|
|
452
|
+
const declared = new Set(ir.metadata.propsParams.map(p => p.name))
|
|
453
|
+
for (const name of collectPropsObjectAccesses(ir, propsObj)) {
|
|
454
|
+
if (declared.has(name)) continue
|
|
455
|
+
if (props && name in props) continue // emitted by the user-props loop
|
|
456
|
+
entries.push(`${name} => undef`)
|
|
457
|
+
declared.add(name)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
436
461
|
// A `{...props}` rest spread means props that aren't declared named
|
|
437
462
|
// params flow through the rest bag (`bf->spread_attrs($<restPropsName>)`),
|
|
438
463
|
// not their own top-level template var. Route them into the bag hashref so
|
|
@@ -524,6 +549,38 @@ function buildPerlProps(
|
|
|
524
549
|
return `{${entries.join(', ')}}`
|
|
525
550
|
}
|
|
526
551
|
|
|
552
|
+
/**
|
|
553
|
+
* (#checkbox) Collect `<propsObj>.<name>` accesses across a component's memos,
|
|
554
|
+
* signal initializers, init statements, and template attribute expressions —
|
|
555
|
+
* the inherited-attribute reads (`props.className`, `props.id`, `props.disabled`)
|
|
556
|
+
* a SolidJS props-object component makes but that aren't enumerated in
|
|
557
|
+
* `propsParams`. Used to declare matching stash variables.
|
|
558
|
+
*/
|
|
559
|
+
function collectPropsObjectAccesses(ir: ComponentIR, propsObj: string): Set<string> {
|
|
560
|
+
const out = new Set<string>()
|
|
561
|
+
const re = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, 'g')
|
|
562
|
+
const scan = (s: string | undefined): void => {
|
|
563
|
+
if (!s) return
|
|
564
|
+
for (const m of s.matchAll(re)) out.add(m[1])
|
|
565
|
+
}
|
|
566
|
+
for (const memo of ir.metadata.memos) scan(memo.computation)
|
|
567
|
+
for (const sig of ir.metadata.signals) scan(sig.initialValue)
|
|
568
|
+
for (const stmt of ir.metadata.initStatements ?? []) scan(stmt.body)
|
|
569
|
+
const walk = (node: unknown): void => {
|
|
570
|
+
if (!node || typeof node !== 'object') return
|
|
571
|
+
const el = node as { attrs?: Array<{ value?: { kind?: string; expr?: string } }>; children?: unknown[] }
|
|
572
|
+
for (const attr of el.attrs ?? []) {
|
|
573
|
+
if (attr.value?.kind === 'expression') scan(attr.value.expr)
|
|
574
|
+
}
|
|
575
|
+
for (const child of el.children ?? []) {
|
|
576
|
+
const c = child as { element?: unknown }
|
|
577
|
+
walk(c.element ?? child)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
walk(ir.root)
|
|
581
|
+
return out
|
|
582
|
+
}
|
|
583
|
+
|
|
527
584
|
/**
|
|
528
585
|
* Evaluate a signal initializer expression using provided props.
|
|
529
586
|
* Handles patterns like: props.initial ?? 0, props.value, literal values.
|