@barefootjs/mojolicious 0.16.0 → 0.17.1

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.
Files changed (53) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +30 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +96 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +85 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +78 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +34 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1482 -1339
  12. package/dist/adapter/lib/constants.d.ts +27 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +33 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/perl-naming.d.ts +29 -0
  17. package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +28 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +34 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/mojo-adapter.d.ts +46 -104
  23. package/dist/adapter/mojo-adapter.d.ts.map +1 -1
  24. package/dist/adapter/props/prop-classes.d.ts +48 -0
  25. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  26. package/dist/adapter/spread/spread-codegen.d.ts +64 -0
  27. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  28. package/dist/adapter/value/parsed-literal.d.ts +26 -0
  29. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  30. package/dist/build.js +1482 -1339
  31. package/dist/index.js +1482 -1339
  32. package/dist/test-render.d.ts.map +1 -1
  33. package/lib/BarefootJS/Backend/Mojo.pm +1 -1
  34. package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
  35. package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
  36. package/package.json +3 -3
  37. package/src/__tests__/mojo-adapter.test.ts +286 -69
  38. package/src/__tests__/query-href.test.ts +94 -0
  39. package/src/adapter/analysis/component-tree.ts +128 -0
  40. package/src/adapter/emit-context.ts +107 -0
  41. package/src/adapter/expr/array-method.ts +429 -0
  42. package/src/adapter/expr/emitters.ts +639 -0
  43. package/src/adapter/expr/operand.ts +55 -0
  44. package/src/adapter/lib/constants.ts +39 -0
  45. package/src/adapter/lib/ir-scope.ts +57 -0
  46. package/src/adapter/lib/perl-naming.ts +36 -0
  47. package/src/adapter/lib/types.ts +31 -0
  48. package/src/adapter/memo/seed.ts +73 -0
  49. package/src/adapter/mojo-adapter.ts +248 -1485
  50. package/src/adapter/props/prop-classes.ts +87 -0
  51. package/src/adapter/spread/spread-codegen.ts +181 -0
  52. package/src/adapter/value/parsed-literal.ts +34 -0
  53. package/src/test-render.ts +2 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `queryHref(base, { … })` → `bf->query(...)` lowering for the Mojolicious
3
+ * adapter (#2042). Parity with the go-template `bf_query` lowering: the call +
4
+ * object literal are structured IR, so it lowers directly. The `bf->query`
5
+ * runtime helper (BarefootJS.pm) includes a pair iff its guard is truthy AND its
6
+ * value is a non-empty string, so a plain `key: v` passes guard `1` and a
7
+ * conditional `key: cond ? v : undefined` passes the lowered condition.
8
+ */
9
+ import { describe, test, expect } from 'bun:test'
10
+ import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
11
+ import { MojoAdapter } from '../adapter/mojo-adapter'
12
+
13
+ function template(src: string): string {
14
+ const a = new MojoAdapter()
15
+ const r = compileJSX(src.trimStart(), 'T.tsx', { adapter: a, outputIR: true })
16
+ const ir = JSON.parse(r.files.find(f => f.type === 'ir')!.content) as ComponentIR
17
+ return a.generate(ir).template
18
+ }
19
+
20
+ describe('queryHref → bf->query (Mojo, #2042)', () => {
21
+ test('a plain value passes guard 1', () => {
22
+ const t = template(`
23
+ 'use client'
24
+ import { queryHref } from '@barefootjs/client'
25
+ export function P(props: { base: string; tag: string }) {
26
+ return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
27
+ }
28
+ `)
29
+ expect(t).toContain("bf->query($base, 1, 'tag', $tag)")
30
+ })
31
+
32
+ test('a conditional include passes the lowered condition as the guard', () => {
33
+ const t = template(`
34
+ 'use client'
35
+ import { queryHref } from '@barefootjs/client'
36
+ export function P(props: { base: string; sort: string; tag: string }) {
37
+ return <a href={queryHref(props.base, { sort: props.sort !== 'date' ? props.sort : undefined, tag: props.tag })}>x</a>
38
+ }
39
+ `)
40
+ expect(t).toContain("bf->query($base, ($sort ne 'date'), 'sort', $sort, 1, 'tag', $tag)")
41
+ })
42
+
43
+ // A bare-value guard (`flag ? v : undefined`) is JS *string* truthiness — `'0'`
44
+ // is a truthy string in JS but false under Perl's `unless`. The lowering must
45
+ // normalise it to a non-empty-string test so SSR matches the client / go (where
46
+ // `lowerUrlGuard` emits `ne <value> ""`), not pass the bare value as the guard.
47
+ test('a bare-value guard is normalised to a non-empty-string test', () => {
48
+ const t = template(`
49
+ 'use client'
50
+ import { queryHref } from '@barefootjs/client'
51
+ export function P(props: { base: string; flag: string; val: string }) {
52
+ return <a href={queryHref(props.base, { q: props.flag ? props.val : undefined })}>x</a>
53
+ }
54
+ `)
55
+ expect(t).toContain("bf->query($base, ($flag ne ''), 'q', $val)")
56
+ })
57
+
58
+ // An array value (`{ tag: props.tags }`) lowers to the bare slice expression;
59
+ // the shared Perl `query` helper detects the arrayref at runtime and appends
60
+ // one pair per non-empty member (#2048). No adapter-side change beyond passing
61
+ // the value through.
62
+ test('an array value passes the slice expression for the helper to append', () => {
63
+ const t = template(`
64
+ 'use client'
65
+ import { queryHref } from '@barefootjs/client'
66
+ export function P(props: { base: string; tags: string[] }) {
67
+ return <a href={queryHref(props.base, { tag: props.tags })}>x</a>
68
+ }
69
+ `)
70
+ expect(t).toContain("bf->query($base, 1, 'tag', $tags)")
71
+ })
72
+
73
+ test('an aliased import is recognised', () => {
74
+ const t = template(`
75
+ 'use client'
76
+ import { queryHref as qh } from '@barefootjs/client'
77
+ export function P(props: { base: string; tag: string }) {
78
+ return <a href={qh(props.base, { tag: props.tag })}>x</a>
79
+ }
80
+ `)
81
+ expect(t).toContain("bf->query($base, 1, 'tag', $tag)")
82
+ })
83
+
84
+ test('a dynamic (non-literal) params object falls back (no bf->query)', () => {
85
+ const t = template(`
86
+ 'use client'
87
+ import { queryHref } from '@barefootjs/client'
88
+ export function P(props: { base: string; q: Record<string, string> }) {
89
+ return <a href={queryHref(props.base, props.q)}>x</a>
90
+ }
91
+ `)
92
+ expect(t).not.toContain('bf->query')
93
+ })
94
+ })
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Component-tree analysis for the Mojolicious EP template adapter.
3
+ *
4
+ * Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure functions over the IR — they read no adapter instance
6
+ * state. `collectImportedLoopChildComponentErrors` returns its diagnostics
7
+ * instead of pushing onto the adapter's error list, so the adapter stays the
8
+ * sole owner of `errors`.
9
+ *
10
+ * SHARED CANDIDATE: `hasClientInteractivity` is byte-identical to the Xslate
11
+ * adapter's copy and adapter-agnostic; the BF103 loop-child check is the same
12
+ * structural walk in both, differing only in the Perl/Kolon diagnostic text —
13
+ * both are groundwork for a shared Perl-family codegen module (issue #2018
14
+ * track D).
15
+ */
16
+
17
+ import type {
18
+ ComponentIR,
19
+ IRNode,
20
+ IRComponent,
21
+ IRElement,
22
+ IRFragment,
23
+ IRConditional,
24
+ IRLoop,
25
+ IRIfStatement,
26
+ IRProvider,
27
+ IRAsync,
28
+ CompilerError,
29
+ } from '@barefootjs/jsx'
30
+
31
+ /**
32
+ * Whether the component needs the client runtime — it owns reactive state
33
+ * (signals / effects / onMount) or the analyzer flagged it as needing init.
34
+ */
35
+ export function hasClientInteractivity(ir: ComponentIR): boolean {
36
+ return (
37
+ ir.metadata.signals.length > 0 ||
38
+ ir.metadata.effects.length > 0 ||
39
+ ir.metadata.onMounts.length > 0 ||
40
+ (ir.metadata.clientAnalysis?.needsInit ?? false)
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Build a `BF103` diagnostic for every component reference inside a loop body
46
+ * whose name is imported from a relative-path module. Mirror of the Go
47
+ * adapter's check — the Mojo adapter has the same cross-template-registration
48
+ * constraint at request time. Returns the diagnostics so the caller pushes
49
+ * them onto its own error list.
50
+ */
51
+ export function collectImportedLoopChildComponentErrors(
52
+ ir: ComponentIR,
53
+ componentName: string,
54
+ ): CompilerError[] {
55
+ const errors: CompilerError[] = []
56
+ // Collect every name imported from a relative-path module (no
57
+ // case filter — `IRComponent` nodes only exist for PascalCase JSX
58
+ // usages, so a lowercase utility import in the set can't match
59
+ // anyway, and any heuristic on the import name itself would be
60
+ // strictly less robust than the structural IR check below).
61
+ const relativeImports = new Set<string>()
62
+ for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
63
+ if (!imp.source.startsWith('./') && !imp.source.startsWith('../')) continue
64
+ if (imp.isTypeOnly) continue
65
+ for (const spec of imp.specifiers) {
66
+ relativeImports.add(spec.alias ?? spec.name)
67
+ }
68
+ }
69
+ if (relativeImports.size === 0) return errors
70
+
71
+ const loc = { file: componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
72
+ const visit = (node: IRNode, inLoop: boolean): void => {
73
+ switch (node.type) {
74
+ case 'component': {
75
+ const comp = node as IRComponent
76
+ if (inLoop && relativeImports.has(comp.name)) {
77
+ errors.push({
78
+ code: 'BF103',
79
+ severity: 'error',
80
+ message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Mojo adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
81
+ loc: comp.loc ?? loc,
82
+ suggestion: {
83
+ message:
84
+ `Options:\n` +
85
+ ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Mojo template alongside the parent at render time.\n` +
86
+ ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.\n` +
87
+ ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`,
88
+ },
89
+ })
90
+ }
91
+ for (const child of comp.children) visit(child, inLoop)
92
+ break
93
+ }
94
+ case 'element':
95
+ for (const child of (node as IRElement).children) visit(child, inLoop)
96
+ break
97
+ case 'fragment':
98
+ for (const child of (node as IRFragment).children) visit(child, inLoop)
99
+ break
100
+ case 'conditional': {
101
+ const cond = node as IRConditional
102
+ visit(cond.whenTrue, inLoop)
103
+ if (cond.whenFalse) visit(cond.whenFalse, inLoop)
104
+ break
105
+ }
106
+ case 'loop':
107
+ for (const child of (node as IRLoop).children) visit(child, true)
108
+ break
109
+ case 'if-statement': {
110
+ const stmt = node as IRIfStatement
111
+ visit(stmt.consequent, inLoop)
112
+ if (stmt.alternate) visit(stmt.alternate, inLoop)
113
+ break
114
+ }
115
+ case 'provider':
116
+ for (const child of (node as IRProvider).children) visit(child, inLoop)
117
+ break
118
+ case 'async': {
119
+ const a = node as IRAsync
120
+ visit(a.fallback, inLoop)
121
+ for (const child of a.children) visit(child, inLoop)
122
+ break
123
+ }
124
+ }
125
+ }
126
+ visit(ir.root, false)
127
+ return errors
128
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The contract the extracted expression-emitter modules depend on instead of
3
+ * the concrete `MojoAdapter`.
4
+ *
5
+ * The Mojo adapter's top-level expression lowering is mutually recursive with
6
+ * the adapter's own const/record resolution and its filter-predicate emitter,
7
+ * so the extracted `MojoTopLevelEmitter` still needs to call back into shared
8
+ * per-compile state and recursive entry points. `MojoEmitContext` is that
9
+ * seam: the emitter takes a `MojoEmitContext` built by the adapter's private
10
+ * `emitCtx` getter (the adapter does NOT `implements` this interface, so the
11
+ * wrapped members stay private and off its exported public type — matching the
12
+ * Go adapter's `emitCtx`). The emitter depends on this narrow interface rather
13
+ * than the full ~3k-line class, so the coupling is explicit and it's
14
+ * unit-testable against a stub.
15
+ *
16
+ * Keep this surface minimal: add a member only when an extracted module
17
+ * genuinely needs it, so the seam documents the real cross-module coupling
18
+ * rather than re-exposing the whole adapter.
19
+ */
20
+
21
+ import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
22
+
23
+ export interface MojoEmitContext {
24
+ /**
25
+ * (#1922) Local binding names the request-scoped `searchParams()` env signal
26
+ * is imported under. Non-empty enables the env-signal method-call lowering.
27
+ */
28
+ readonly _searchParamsLocals: Set<string>
29
+
30
+ /**
31
+ * Inline a module-scope pure string-literal const by name as the resolved
32
+ * literal value, or null when the name is not such a const.
33
+ */
34
+ resolveModuleStringConst(name: string): string | null
35
+
36
+ /** Resolve a literal const (`const totalPages = 5`) to its Perl value, or null. */
37
+ resolveLiteralConst(name: string): string | null
38
+
39
+ /**
40
+ * Resolve a static property access on a module object-literal const
41
+ * (`variantClasses.ghost`) to its Perl value at compile time, or null.
42
+ */
43
+ resolveStaticRecordLiteral(objectName: string, key: string): string | null
44
+
45
+ /** Whether a getter/prop name resolves to a string-typed SSR value. */
46
+ _isStringValueName(name: string): boolean
47
+
48
+ /** Record a BF101 unsupported-expression diagnostic. */
49
+ _recordExprBF101(message: string, reason?: string): void
50
+
51
+ /** Lower a filter/predicate body to its Perl form, bound to `param`. */
52
+ _renderPerlFilterExprPublic(expr: ParsedExpr, param: string): string
53
+ }
54
+
55
+ /**
56
+ * The contract the extracted object-literal / conditional-spread lowering
57
+ * (`spread/spread-codegen.ts`) depends on. The spread lowering recurses into
58
+ * the core expression lowering and records its own BF101 diagnostics, so it
59
+ * needs the recursive entry point plus the per-compile bookkeeping the
60
+ * adapter owns. Declared separately from `MojoEmitContext` so each extracted
61
+ * module's real coupling is documented precisely.
62
+ */
63
+ export interface MojoSpreadContext {
64
+ /** Component name, for diagnostic source locations. */
65
+ readonly componentName: string
66
+
67
+ /** Per-compile diagnostic list the spread lowering appends to. */
68
+ readonly errors: CompilerError[]
69
+
70
+ /** Local-constant metadata, for resolving `Record[key]` spread values. */
71
+ readonly localConstants: IRMetadata['localConstants']
72
+
73
+ /** Prop params, for classifying a bare-identifier index as a prop. */
74
+ readonly propsParams: { name: string }[]
75
+
76
+ /**
77
+ * Lower a JS expression to its Perl form (the core recursive entry).
78
+ *
79
+ * When the IR already carries a structured `ParsedExpr` tree, pass it as
80
+ * `preParsed` so the converter threads it straight through instead of
81
+ * re-parsing `expr` — mirrors go-template's
82
+ * `convertExpressionToGo(jsExpr, out?, preParsed?)`. With `preParsed` set,
83
+ * `expr` is unused for parsing (the converter derives any diagnostic text
84
+ * from the tree), so callers may pass `''`.
85
+ */
86
+ convertExpressionToPerl(expr: string, preParsed?: ParsedExpr): string
87
+ }
88
+
89
+ /**
90
+ * The contract the extracted in-template memo / context seeding
91
+ * (`memo/seed.ts`) depends on. The seed lowering recurses into the core
92
+ * expression lowering to compute a derived signal/memo value or a context
93
+ * default; that recursive entry is its only adapter coupling.
94
+ */
95
+ export interface MojoMemoContext {
96
+ /**
97
+ * Lower a JS expression to its Perl form (the core recursive entry).
98
+ *
99
+ * When the IR already carries a structured `ParsedExpr` tree, pass it as
100
+ * `preParsed` so the converter threads it straight through instead of
101
+ * re-parsing `expr` — mirrors go-template's
102
+ * `convertExpressionToGo(jsExpr, out?, preParsed?)`. With `preParsed` set,
103
+ * `expr` is unused for parsing (the converter derives any diagnostic text
104
+ * from the tree), so callers may pass `''`.
105
+ */
106
+ convertExpressionToPerl(expr: string, preParsed?: ParsedExpr): string
107
+ }