@barefootjs/mojolicious 0.14.0 → 0.15.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.
@@ -5,7 +5,7 @@
5
5
  * Used by adapter-tests conformance runner.
6
6
  */
7
7
 
8
- import { compileJSX, extractSsrDefaults } from '@barefootjs/jsx'
8
+ import { compileJSX, extractSsrDefaults, augmentInheritedPropAccesses, importsSearchParams } from '@barefootjs/jsx'
9
9
  import type { ComponentIR } from '@barefootjs/jsx'
10
10
  import { mkdir, rm } from 'node:fs/promises'
11
11
  import { resolve } from 'node:path'
@@ -332,8 +332,12 @@ function buildChildRenderers(
332
332
  lines.push(` close $child_fh;`)
333
333
  lines.push(` my $child_mt = Mojo::Template->new(vars => 1, auto_escape => 1);`)
334
334
 
335
+ lines.push(` my $defaults_${snakeName} = ${buildChildDefaultsPerl(childIR)};`)
335
336
  lines.push(` $bf->register_child_renderer('${snakeName}', sub {`)
336
- lines.push(` my ($child_props) = @_;`)
337
+ // `$caller_bf` is the instance whose template invoked render_child
338
+ // (#1897) — nested children chain their scope identity off it.
339
+ lines.push(` my ($child_props, $caller_bf) = @_;`)
340
+ lines.push(` my $host_scope = (defined $caller_bf ? $caller_bf->_scope_id : $bf->_scope_id);`)
337
341
  // (#1652) A child that destructures a rest-spread bag
338
342
  // (`function NativeSelect({ children, ...props })`) emits a
339
343
  // template referencing `$<restPropsName>`
@@ -349,8 +353,26 @@ function buildChildRenderers(
349
353
  if (childIR.metadata.restPropsName) {
350
354
  const rest = childIR.metadata.restPropsName
351
355
  lines.push(` $child_props->{${rest}} = {} unless defined $child_props->{${rest}};`)
356
+ // (#1897) Route non-param props into the rest bag (JSX rest
357
+ // semantics): a caller prop the child didn't destructure belongs
358
+ // in the bag, not as a top-level stash var. Mirrors the Xslate
359
+ // harness and the production isRestProps branch.
360
+ const paramNames = (childIR.metadata.propsParams ?? []).map(p => p.name)
361
+ const keepList = [...new Set([...paramNames, rest, 'children', 'key', '_bf_slot'])]
362
+ .map(n => `'${n.replace(/[\\']/g, m => `\\${m}`)}'`)
363
+ .join(', ')
364
+ lines.push(` {`)
365
+ lines.push(` my %keep = map { $_ => 1 } (${keepList});`)
366
+ lines.push(` for my $k (grep { !$keep{$_} } keys %$child_props) {`)
367
+ lines.push(` $child_props->{${rest}}{$k} = delete $child_props->{$k};`)
368
+ lines.push(` }`)
369
+ lines.push(` }`)
352
370
  }
353
371
  lines.push(` my $child_bf = BarefootJS->new($c, {});`)
372
+ // (#1897) Nested `render_child` calls (a child template rendering
373
+ // another imported component) resolve against THIS instance's
374
+ // registry — share the parent's so they don't fail.
375
+ lines.push(` $child_bf->_child_renderers($bf->_child_renderers);`)
354
376
  // JSX `key` (reserved prop) → data-key on the child's scope root, for
355
377
  // keyed-loop reconciliation parity with Hono.
356
378
  lines.push(` my $data_key = delete $child_props->{key};`)
@@ -362,11 +384,13 @@ function buildChildRenderers(
362
384
  // `<ComponentName>_*`.
363
385
  lines.push(` my $slot = delete $child_props->{_bf_slot};`)
364
386
  lines.push(` if (defined $slot) {`)
365
- lines.push(` $child_bf->_scope_id($bf->_scope_id . "_$slot");`)
387
+ lines.push(` $child_bf->_scope_id($host_scope . "_$slot");`)
366
388
  lines.push(` } else {`)
367
389
  lines.push(` $child_bf->_scope_id('${componentName}_' . substr(rand() =~ s/^0\\.//r, 0, 6));`)
368
390
  lines.push(` }`)
369
- lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$child_props, bf => $child_bf });`)
391
+ // Seed statically-derived defaults under the caller's props (#1897)
392
+ // so undeclared optional props / signals don't abort strict vars.
393
+ lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$defaults_${snakeName}, %$child_props, bf => $child_bf });`)
370
394
  lines.push(` die $rendered->to_string if ref $rendered;`)
371
395
  lines.push(` chomp $rendered;`)
372
396
  lines.push(` return $rendered;`)
@@ -391,6 +415,59 @@ function toSnakeCase(name: string): string {
391
415
  /**
392
416
  * Build a Perl hash literal from props.
393
417
  */
418
+ /**
419
+ * Statically-derived default template vars for a CHILD component
420
+ * (#1897): every declared prop (its JSX default or `undef`), inherited
421
+ * `props.<x>` accesses, signal initial values, and memo ssrDefaults.
422
+ * Without these, a child template referencing an optional prop the
423
+ * caller didn't pass (`$id`, `$className`) or its own signal (`$open`)
424
+ * aborts under Mojo::Template's strict vars. Caller-passed props win at
425
+ * merge time (`{ %$defaults, %$child_props }`). Mirrors the root-side
426
+ * seeding in `buildPerlProps` and the production plugin's
427
+ * `ssrDefaults` consumption.
428
+ */
429
+ function buildChildDefaultsPerl(ir: ComponentIR): string {
430
+ // Surface inherited `props.<x>` reads hidden inside template-literal
431
+ // attr parts / conditional branches as propsParams (idempotent; the
432
+ // same shared pass the adapters apply — without it TabsContent's
433
+ // `${props.className ?? ''}` class read never seeds `$className`).
434
+ augmentInheritedPropAccesses(ir)
435
+ const entries: string[] = []
436
+ const declared = new Set<string>()
437
+ for (const param of ir.metadata.propsParams) {
438
+ declared.add(param.name)
439
+ if (param.defaultValue) {
440
+ const perlValue = jsToPerlValue(param.defaultValue)
441
+ if (perlValue !== null) {
442
+ entries.push(`${param.name} => ${perlValue}`)
443
+ continue
444
+ }
445
+ }
446
+ entries.push(`${param.name} => undef`)
447
+ }
448
+ if (ir.metadata.propsObjectName) {
449
+ for (const name of collectPropsObjectAccesses(ir, ir.metadata.propsObjectName)) {
450
+ if (declared.has(name)) continue
451
+ declared.add(name)
452
+ entries.push(`${name} => undef`)
453
+ }
454
+ }
455
+ for (const signal of ir.metadata.signals) {
456
+ const value = evaluateSignalInit(signal.initialValue.trim(), undefined)
457
+ entries.push(`${signal.getter} => ${value !== null ? toPerlLiteral(value) : 'undef'}`)
458
+ }
459
+ const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {}
460
+ for (const memo of ir.metadata.memos) {
461
+ const entry = ssrDefaults[memo.name]
462
+ const value =
463
+ entry && typeof entry === 'object' && 'value' in entry ? entry.value : undefined
464
+ entries.push(
465
+ `${memo.name} => ${value !== undefined && value !== null ? toPerlLiteral(value) : 'undef'}`,
466
+ )
467
+ }
468
+ return `{${entries.join(', ')}}`
469
+ }
470
+
394
471
  function buildPerlProps(
395
472
  _componentName: string,
396
473
  props: Record<string, unknown> | undefined,
@@ -515,12 +592,15 @@ function buildPerlProps(
515
592
  }
516
593
  }
517
594
 
518
- // Add signal values evaluated from props (must come after user props)
595
+ // Add signal values evaluated from props (must come after user props).
596
+ // Seed `undef` for a null / unevaluable initial (e.g. a
597
+ // `createSignal<SortKey>(null)` whose getter is read in a child-prop
598
+ // ternary) rather than skipping it — an unseeded getter faults strict
599
+ // vars with `Global symbol "$x" requires explicit package name`. Same
600
+ // rule `buildChildDefaultsPerl` applies to child signals (#1897).
519
601
  for (const signal of ir.metadata.signals) {
520
602
  const value = evaluateSignalInit(signal.initialValue.trim(), props)
521
- if (value !== null) {
522
- entries.push(`${signal.getter} => ${toPerlLiteral(value)}`)
523
- }
603
+ entries.push(`${signal.getter} => ${value !== null ? toPerlLiteral(value) : 'undef'}`)
524
604
  }
525
605
 
526
606
  // Add memo values. The production Mojo plugin seeds these from the
@@ -536,6 +616,16 @@ function buildPerlProps(
536
616
  entries.push(`${memo.name} => ${toPerlLiteral(value ?? 0)}`)
537
617
  }
538
618
 
619
+ // (#1922) Request-scoped `searchParams()`: bind `$searchParams` to an
620
+ // empty-query reader via the lazy-loading factory (so the render script
621
+ // needn't `use BarefootJS::SearchParams`). The conformance harness issues no
622
+ // query string (the production Mojo plugin builds this from
623
+ // `$c->req->query_params`), so `.get(k)` resolves to undef and the author's
624
+ // `?? default` renders. Only when the component imports `searchParams`.
625
+ if (importsSearchParams(ir.metadata)) {
626
+ entries.push(`searchParams => BarefootJS->search_params('')`)
627
+ }
628
+
539
629
  return `{${entries.join(', ')}}`
540
630
  }
541
631