@barefootjs/mojolicious 0.33.4 → 0.34.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.
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::Backend::Mojo;
2
- our $VERSION = "0.33.3";
2
+ our $VERSION = "0.33.6";
3
3
  use Mojo::Base -base, -signatures;
4
4
 
5
5
  use Mojo::ByteStream qw(b);
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS::DevReload;
2
- our $VERSION = "0.33.3";
2
+ our $VERSION = "0.33.6";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  =head1 NAME
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS;
2
- our $VERSION = "0.33.3";
2
+ our $VERSION = "0.33.6";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  use Mojo::File qw(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.33.4",
3
+ "version": "0.34.0",
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,7 +52,7 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.33.4"
55
+ "@barefootjs/shared": "0.34.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@barefootjs/jsx": ">=0.2.0",
@@ -70,9 +70,9 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@barefootjs/adapter-tests": "0.1.0",
73
- "@barefootjs/jsx": "0.33.4",
74
- "@barefootjs/vite": "0.33.4",
75
- "@barefootjs/client": "0.33.4",
73
+ "@barefootjs/jsx": "0.34.0",
74
+ "@barefootjs/vite": "0.34.0",
75
+ "@barefootjs/client": "0.34.0",
76
76
  "vite": "^6.0.0"
77
77
  }
78
78
  }
@@ -18,7 +18,7 @@
18
18
  * rather than re-exposing the whole adapter.
19
19
  */
20
20
 
21
- import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
21
+ import type { ParsedExpr, CompilerError, IRMetadata, LoweringMatcher } from '@barefootjs/jsx'
22
22
 
23
23
  export interface MojoEmitContext {
24
24
  /**
@@ -27,6 +27,17 @@ export interface MojoEmitContext {
27
27
  */
28
28
  readonly _searchParamsLocals: Set<string>
29
29
 
30
+ /**
31
+ * Registered lowering-plugin matchers (#2057), bound to this component's
32
+ * metadata at init. Read by `MojoTopLevelEmitter`'s `lowering` seam (#2843)
33
+ * so a registered call — the built-in `queryHref`, or any userland plugin —
34
+ * is recognised no matter where it sits in an expression tree (a ternary
35
+ * branch, a template-literal interpolation, …), not only when it's the call
36
+ * the adapter's own top-level conversion entry point
37
+ * (`convertExpressionToPerl`) is asked to lower directly.
38
+ */
39
+ readonly _loweringMatchers: readonly LoweringMatcher[]
40
+
30
41
  /**
31
42
  * Inline a module-scope pure string-literal const by name as the resolved
32
43
  * literal value, or null when the name is not such a const.
@@ -27,6 +27,10 @@ import { groupBinaryOperand,
27
27
  matchSearchParamsMethodCall,
28
28
  sortComparatorFromArrow,
29
29
  asCallbackMethodCall,
30
+ type LoweringEmitter,
31
+ type LoweringNode,
32
+ queryHrefArgs,
33
+ isValidHelperId,
30
34
  } from '@barefootjs/jsx'
31
35
 
32
36
  import type { MojoEmitContext } from '../emit-context.ts'
@@ -323,6 +327,35 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
323
327
  this.ctx = ctx
324
328
  }
325
329
 
330
+ /**
331
+ * Registered-lowering seam (#2843): `emitParsedExpr`'s shared `call` case
332
+ * tries every matcher here BEFORE `call()` itself, so a registered call
333
+ * (the built-in `queryHref`, or any userland plugin) is recognised no
334
+ * matter where it sits in the tree. `render` is what used to live inline
335
+ * in `MojoAdapter.convertExpressionToPerl` before the object-literal
336
+ * support-gate refusal (`checkSupport`'s `call` arm, now itself
337
+ * registry-aware) made the pre-gate special case unnecessary.
338
+ */
339
+ get lowering(): LoweringEmitter {
340
+ return {
341
+ matchers: this.ctx._loweringMatchers,
342
+ render: (node: LoweringNode, emit: (e: ParsedExpr) => string): string | null => {
343
+ // `query` guard-list — `queryHref`-shaped.
344
+ if (node.kind === 'guard-list' && node.helper === 'query') {
345
+ const qArgs = queryHrefArgs(node, emit)
346
+ return `bf->query(${qArgs.join(', ')})`
347
+ }
348
+ // Generic `helper-call` (#2069) — a userland `LoweringPlugin`'s
349
+ // single runtime-helper invocation; `bf-><helper>(args…)` mirrors
350
+ // the `query` helper's own naming convention.
351
+ if (node.kind === 'helper-call' && isValidHelperId(node.helper)) {
352
+ return `bf->${node.helper}(${node.args.map(emit).join(', ')})`
353
+ }
354
+ return null
355
+ },
356
+ }
357
+ }
358
+
326
359
  identifier(name: string): string {
327
360
  // `undefined` / `null` nested inside a larger expression tree
328
361
  // (#1897, pagination's `props.isActive ? 'page' : undefined`) — the
@@ -56,8 +56,6 @@ import {
56
56
  lookupStaticRecordLiteral,
57
57
  searchParamsLocalNames,
58
58
  prepareLoweringMatchers,
59
- queryHrefArgs,
60
- isValidHelperId,
61
59
  sortComparatorFromArrow,
62
60
  isLowerableLoopDestructure,
63
61
  isDangerousInnerHtmlAttr,
@@ -67,6 +65,7 @@ import {
67
65
  resolveStaticLoopSource,
68
66
  derivesScopeFromSlot,
69
67
  BindingScope,
68
+ buildImportAliasMap,
70
69
  } from '@barefootjs/jsx'
71
70
  import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
72
71
  import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
@@ -256,6 +255,18 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
256
255
  */
257
256
  private nullableOptionalProps: Set<string> = new Set()
258
257
 
258
+ /**
259
+ * Local alias -> declared/exported name for imported components (#2822,
260
+ * the SSR-side counterpart of #2777's client-JS registry-key fix). A
261
+ * child referenced under an import alias (`import { Foo as Bar }`,
262
+ * `<Bar/>`) must build its cross-template call against the child's own
263
+ * declared name (`Foo`, what `foo.tsx` registers its Mojo partial as) —
264
+ * never the caller-local binding. Built once per compile from
265
+ * `ir.metadata.imports` via the shared `buildImportAliasMap`
266
+ * (`@barefootjs/jsx`) and read by `toTemplateName`.
267
+ */
268
+ private importAliases: Map<string, string> = new Map()
269
+
259
270
  constructor(options: MojoAdapterOptions = {}) {
260
271
  super()
261
272
  this.options = {
@@ -285,6 +296,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
285
296
  this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
286
297
  this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
287
298
  this.localConstants = ir.metadata.localConstants ?? []
299
+ this.importAliases = buildImportAliasMap(ir.metadata.imports ?? [])
288
300
  this.scope = BindingScope.EMPTY
289
301
  this.errors = []
290
302
  this.childrenCaptureCounter = 0
@@ -390,7 +402,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
390
402
  */
391
403
  parseUndefinedAlternateTernary(
392
404
  expr: string,
393
- ): { condition: string; consequent: string } | null {
405
+ ): { condition: string; consequent: string; testParsed: ParsedExpr; consequentParsed: ParsedExpr } | null {
394
406
  const parsed = parseExpression(expr.trim())
395
407
  if (parsed?.kind !== 'conditional') return null
396
408
  const alt = parsed.alternate
@@ -398,13 +410,16 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
398
410
  (alt.kind === 'identifier' && (alt.name === 'undefined' || alt.name === 'null')) ||
399
411
  (alt.kind === 'literal' && (alt.value === null || alt.value === undefined))
400
412
  if (!isUndef) return null
401
- // Serialise the parsed sub-expressions back to JS source rather than
402
- // slicing `expr` text `indexOf('?')` / `lastIndexOf(':')` would
403
- // mis-split when the consequent itself contains `?` / `:` inside a
404
- // string or nested ternary (`cond ? 'a:b' : undefined`).
413
+ // `condition`/`consequent` are DEBUG text only (`exprToString` renders an
414
+ // unsupported nested shape like an object literal as non-reparseable
415
+ // `[UNSUPPORTED: …]` text) callers that need to re-lower the
416
+ // sub-expression must use `testParsed`/`consequentParsed` (the actual
417
+ // parsed trees) as `preParsed`, not re-parse these strings.
405
418
  return {
406
419
  condition: exprToString(parsed.test),
407
420
  consequent: exprToString(parsed.consequent),
421
+ testParsed: parsed.test,
422
+ consequentParsed: parsed.consequent,
408
423
  }
409
424
  }
410
425
 
@@ -1257,8 +1272,12 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1257
1272
  private presenceVarCounter = 0
1258
1273
 
1259
1274
  private toTemplateName(componentName: string): string {
1275
+ // Resolve an import alias (`import { Foo as Bar }`, `<Bar/>`) back to
1276
+ // the child's own declared name BEFORE snake-casing (#2822) — `Bar`
1277
+ // has no `foo.tsx`-registered partial; only `Foo` does.
1278
+ const declaredName = this.importAliases.get(componentName) ?? componentName
1260
1279
  // Convert PascalCase to snake_case for Mojo template naming
1261
- return componentName
1280
+ return declaredName
1262
1281
  .replace(/([A-Z])/g, '_$1')
1263
1282
  .toLowerCase()
1264
1283
  .replace(/^_/, '')
@@ -1441,8 +1460,8 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1441
1460
  {
1442
1461
  const m = this.parseUndefinedAlternateTernary(value.expr)
1443
1462
  if (m) {
1444
- const cond = this.convertExpressionToPerl(m.condition)
1445
- const val = this.convertExpressionToPerl(m.consequent)
1463
+ const cond = this.convertExpressionToPerl('', m.testParsed)
1464
+ const val = this.convertExpressionToPerl('', m.consequentParsed)
1446
1465
  return `<% if (${cond}) { %>${name}="<%= ${val} %>"<% } %>`
1447
1466
  }
1448
1467
  }
@@ -1764,7 +1783,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1764
1783
  const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe)
1765
1784
  if (!startsAsObjectLiteral && !hasTaggedTemplate) return false
1766
1785
  const parsed = parseExpression(expr.trim())
1767
- const support = isSupported(parsed)
1786
+ const support = isSupported(parsed, { loweringMatchers: this._loweringMatchers })
1768
1787
  if (parsed.kind !== 'unsupported' && support.supported) return false
1769
1788
  // Surface the `isSupported` reason so the diagnostic is as
1770
1789
  // actionable as the Go adapter's BF101 — keeps cross-adapter
@@ -1794,6 +1813,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1794
1813
  private get emitCtx(): MojoEmitContext {
1795
1814
  return {
1796
1815
  _searchParamsLocals: this._searchParamsLocals,
1816
+ _loweringMatchers: this._loweringMatchers,
1797
1817
  resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
1798
1818
  resolveLiteralConst: (name) => this.resolveLiteralConst(name),
1799
1819
  resolveStaticRecordLiteral: (o, k) => this.resolveStaticRecordLiteral(o, k),
@@ -1850,42 +1870,16 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1850
1870
  parsed = parseExpression(trimmed)
1851
1871
  }
1852
1872
 
1853
- // Registered call lowerings (#2057) — including the built-in `queryHref`
1854
- // plugin (#2042), which lowers `queryHref(base, { … })` to a neutral
1855
- // `guard-list` on the `query` helper → `bf->query(base, <triples>)`.
1856
- // Recognised before the support gate because the object-literal arg is
1857
- // otherwise `unsupported` (BF101). The `bf->query` helper includes a pair iff
1858
- // its guard is truthy AND its value is a non-empty string (the client's
1859
- // `if (value)`): a plain `key: v` passes guard `1`, a conditional
1860
- // `key: cond ? v : undefined` passes the lowered cond. Only the `query`
1861
- // helper renders to `bf->query`; another guard-list helper must not be
1862
- // silently mis-rendered as a query.
1863
- if (parsed.kind === 'call') {
1864
- for (const matcher of this._loweringMatchers) {
1865
- const node = matcher(parsed.callee, parsed.args)
1866
- if (node?.kind === 'guard-list' && node.helper === 'query') {
1867
- const argsGo = queryHrefArgs(node, n => this.renderParsedExprToPerl(n))
1868
- return `bf->query(${argsGo.join(', ')})`
1869
- }
1870
- // Generic `helper-call` (#2069) — the neutral vocabulary's escape
1871
- // hatch for a userland `LoweringPlugin` that lowers to a single
1872
- // runtime-helper invocation. `bf-><helper>(args…)` mirrors the
1873
- // `query` helper's own naming convention exactly: the framework
1874
- // renders the call, the plugin author registers `<helper>` as a
1875
- // method on their own Mojolicious `bf` helper object — same
1876
- // contract as `bf->query` itself, just not built in.
1877
- if (node?.kind === 'helper-call' && isValidHelperId(node.helper)) {
1878
- const argsX = node.args.map(a => this.renderParsedExprToPerl(a))
1879
- return `bf->${node.helper}(${argsX.join(', ')})`
1880
- }
1881
- }
1882
- }
1883
-
1884
1873
  // `pos` distinguishes a derived-seed RHS (an assignment, checked via
1885
1874
  // `isSupportedValue`) from every other, genuinely rendered call site —
1886
1875
  // the rendered gate would otherwise re-refuse a tree the seed plan
1887
1876
  // already classified `derived` at value position (#2696 review).
1888
- const support = pos === 'value' ? isSupportedValue(parsed) : isSupported(parsed)
1877
+ // Registered call lowerings (#2057) including the built-in `queryHref`
1878
+ // plugin — are consulted by this gate no matter where the call sits in
1879
+ // the tree (`MojoTopLevelEmitter`'s `lowering` seam does the actual
1880
+ // rendering below, via `emitParsedExpr`'s shared `call` case).
1881
+ const supportOpts = { loweringMatchers: this._loweringMatchers }
1882
+ const support = pos === 'value' ? isSupportedValue(parsed, supportOpts) : isSupported(parsed, supportOpts)
1889
1883
  if (!support.supported) {
1890
1884
  this.errors.push({
1891
1885
  code: 'BF101',
@@ -10,6 +10,10 @@
10
10
  import type { ConformancePins } from '@barefootjs/jsx'
11
11
 
12
12
  export const conformancePins: ConformancePins = {
13
+ // #2843: graduated — a registered lowering call inside a ternary
14
+ // attribute branch (or any nested value position) is now recognised via
15
+ // `MojoTopLevelEmitter`'s `lowering` seam + the registry-aware support
16
+ // gate, matching the direct-call attribute path exactly.
13
17
  'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
14
18
  'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
15
19
  'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
@@ -56,4 +60,11 @@ export const conformancePins: ConformancePins = {
56
60
  // `rich-prop-client-read` above.
57
61
  'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
58
62
  'jsx-element-prop-array': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
63
+ // #2771: a reactive primitive invoked through a namespace import
64
+ // (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
65
+ // that the analyzer's checker-less fast path cannot recognize refuses
66
+ // loudly (BF013) instead of silently dropping the declaration — fired
67
+ // in the shared analyzer pass ahead of any adapter's `adapter.generate()`,
68
+ // so all nine adapters (including Hono) pin this identically.
69
+ 'namespace-import-primitive': [{ code: 'BF013', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2771' }],
59
70
  }
@@ -28,4 +28,6 @@ import type { RenderDivergences } from '@barefootjs/jsx'
28
28
  export const renderDivergences: RenderDivergences = {
29
29
  'children-passthrough-renamed':
30
30
  'A renamed `children` destructure emits the local alias as a template variable; the stash defines only `children`, so the Perl render dies (#2788).',
31
+ 'aliased-loop-source':
32
+ 'A `.map()` loop whose source is a local const alias of a signal getter (`const items__alias = items`) dies inside `Mojo::Template::process` on real Mojolicious — the seeded loop data is keyed by the signal\'s real name (`items`), and the alias hop is never resolved when deciding what to seed under `items__alias`. This is the SSR-side twin of #2778 (fixed for the CSR client-JS template in the same PR that added this fixture) — that fix only touches client-JS emission, not SSR data-seeding. Tracked at https://github.com/piconic-ai/barefootjs/issues/2813; graduate by resolving the alias hop at SSR-seeding time using the same `resolveAliasOrigin`/`resolveGetterAliases` mechanism #2778 introduced, rather than a third alias-hop walker.',
31
33
  }